Merging TranslateReturnCode into wolfSSL_LastError dropped the
IP_SOCK_getsockopt(SO_ERROR) lookup emNET integrations need to retrieve
the canonical IP_ERR_* for a failed recv/send, leaving a broken branch
that returned the raw value and mishandled the POSIX-facade convention.
Restore the historic lookup (fixing the optlen pointer-vs-int typo
along the way) and add a CI test that builds wolfSSL with
-DWOLFSSL_EMNET against a clean-room shim providing an emNET-faithful
IP_SOCK_getsockopt (SO_ERROR-then-errno fallback, since Linux does not
stash EAGAIN in SO_ERROR); recv/send fall through to glibc.
The emNET `wolfSSL_LastError` branches were incorrect. The second one
was never hit and would never compile. The first one inverts error codes
that should not be inverted.
This fixes that code and adds a test with a shim layer to test emNET
calls without using emNET.
Replace the liboqs-based pre-standardization SPHINCS+ implementation
with the native FIPS 205 SLH-DSA implementation across the
certificate / ASN.1 / X.509 layers, and add SLH-DSA-rooted test
certificates plus TLS 1.3 .conf scenarios that exercise the new
verification path. All liboqs SPHINCS+ code is removed.
This enables SLH-DSA for certificate chain authentication: CA
certificates signed with SLH-DSA, certificate signature verification
against an SLH-DSA root. TLS 1.3 entity authentication via
CertificateVerify with SLH-DSA will be added in a follow-up PR.
Follows RFC 9909 (X.509 Algorithm Identifiers for SLH-DSA) and
NIST FIPS 205. Supports both SHAKE and SHA-2 parameter families
across all twelve standardized variants.
DER codec:
- New PrivateKeyDecode, PublicKeyDecode, KeyToDer, PrivateKeyToDer,
PublicKeyToDer with RFC 9909 encoding (bare OCTET STRING containing
4*n raw bytes = SK.seed || SK.prf || PK.seed || PK.root, no nested
wrapper). OID auto-detection across all twelve SHAKE / SHA-2 variants.
- PublicKeyDecode raw-bytes fast path mirrors wc_Falcon_PublicKeyDecode
and wc_Dilithium_PublicKeyDecode so callers (notably
wolfssl_x509_make_der and ConfirmSignature, which pass the raw
BIT STRING contents stashed by StoreKey) decode correctly. Honours
the caller's *inOutIdx start offset.
- Error paths in Private/PublicKeyDecode preserve params/flags/
inOutIdx and only ForceZero the buffer half each helper actually
writes; skip the wipe entirely on BAD_LENGTH_E (no bytes touched).
- ImportPublic uses |= on flags so a Private-then-Public import
sequence retains FLAG_PRIVATE.
OID dispatch:
- 12 standardized NIST OIDs (6 SHAKE + 6 SHA-2) per RFC 9909. The
pre-standardization OID-collision mechanism is removed since NIST
OIDs do not collide.
- wc_SlhDsaOidToParam / wc_SlhDsaOidToCertType return NOT_COMPILED_IN
(rather than -1) for recognised SLH-DSA OIDs whose parameter set
isn't built; wc_IsSlhDsaOid recognises both. The x509 dispatch
surfaces this as a precise diagnostic instead of the generic
"No public key found".
- wc_GetKeyOID picks a placeholder parameter from whatever variant is
compiled in and #errors at compile time if none is.
- asn_orig.c EncodeCert / EncodeCertReq accept SHA-2 SLH-DSA keyTypes
alongside SHAKE.
Tests and fixtures:
- Test cert chain in certs/slhdsa/: SLH-DSA-SHAKE-128s and
SLH-DSA-SHA2-128s self-signed roots that sign reused ML-DSA-44
entity keys (server + client), plus the gen script
(gen-slhdsa-mldsa-certs.sh, OpenSSL >= 3.5).
- New TLS 1.3 .conf scenarios under tests/suites.c dispatch:
test-tls13-slhdsa-shake.conf, test-tls13-slhdsa-sha2.conf, and a
wrong-CA negative test test-tls13-slhdsa-fail.conf.
- DER round-trip and on-disk decode tests; bench_slhdsa_*_key.der
fixtures regenerated with wolfSSL's own encoder so the codec is
pinned to RFC 9909.
- New unit test test_wc_slhdsa_x509_i2d_roundtrip exercises the raw
PublicKeyDecode entry point that wolfssl_x509_make_der relies on.
- test_wc_slhdsa_check_key now tests both Public-then-Private and
Private-then-Public import orderings.
Build / ABI:
- DYNAMIC_TYPE_SPHINCS = 98 kept as RESERVED with a tombstone comment
for ABI stability; new code should use DYNAMIC_TYPE_SLHDSA (107).
- All build system / IDE project files updated; SPHINCS+ sources,
headers, and test data removed.
- Dead bench_slhdsa_*_key arrays removed from gencertbuf.pl and
certs_test.h; the .der files on disk drive the decode tests.
* configure.ac: --enable-dtls13 auto-enables --enable-dtls and TLS 1.3,
with a targeted error if either is explicitly --disabled, plus a
post-finalization sanity check that errors out if a later
prerequisite test forces ENABLED_TLS13 back to "no" while
ENABLED_DTLS13 is yes.
* src/internal.c, src/wolfio.c, wolfssl/wolfio.h: new WOLFSSL_DTLS_ONLY
compile-time flag elides the EmbedReceive / EmbedSend default
callbacks. The DTLS_MAJOR runtime check stays in SetSSL_CTX so a
TLS-method ctx in a DTLS-only build doesn't get datagram callbacks
by default, and WriteSEQ keeps its ssl->options.dtls branch. A
#error in settings.h refuses WOLFSSL_DTLS_ONLY without WOLFSSL_DTLS.
* wolfcrypt/src/aes.c: add HAVE_AES_DECRYPT to the inv_col_mul
definition gate to match its only caller; without it the function is
emitted dead under WOLFSSL_AES_DIRECT && NO_AES_DECRYPT and
-Werror=unused-function fails the build.
* .github/workflows/os-check.yml: matrix entry for a minimal DTLS 1.3
client-only build.
The CRL refactor broke nginx's ssl_cache.t (and the wolfSSL/wolfssl
nginx_check matrix on 1.24.0/1.25.0/1.28.1) because nginx loads the test
CRL through a FIFO. wolfSSL_PEM_X509_X509_CRL_X509_PKEY_read_bio() asks
wolfSSL_BIO_get_len() for the BIO size up front; for a FIFO the
underlying ftell() returns ESPIPE, wolfssl_file_len() reports
WOLFSSL_BAD_FILETYPE, and BIO_get_len() returns 0. The function then hit
the l <= pem_struct_min_sz guard and bailed with ASN_NO_PEM_HEADER
before reading a byte, so the caller's loop saw "no CRL" and nginx
emitted "PEM_read_bio_X509_CRL() failed".
Treat l == 0 as "streaming source, size unknown" and allocate up to
MAX_BIO_READ_BUFFER (the same cap ReadPemFromBioToBuffer used for this
case before the refactor). The existing byte-by-byte reader already
stops at the END marker or at EOF, so this is enough; if the upstream
short-reads we still surface ASN_NO_PEM_HEADER from the
pem_struct_min_sz read below. Keep rejecting tiny non-zero lengths
since those are real "buffer too small" cases.
RFC 8446 section 8 requires any server instance to accept 0-RTT for a
given ClientHello at most once. Prior to this change wolfSSL's behaviour
diverged from that requirement in several ways:
* ctx->maxEarlyDataSz defaulted to MAX_EARLY_DATA_SZ whenever the
library was built with WOLFSSL_EARLY_DATA, so servers auto-
advertised 0-RTT in NewSessionTicket without the application
asking. RFC 8446 E.5 says 0-RTT MUST NOT be enabled unless
specifically requested.
* The post-accept eviction is compiled out under NO_SESSION_CACHE,
so builds without the cache accepted 0-RTT with no replay defence.
* Stateless self-encrypted tickets do not carry a session ID on the
stateless DoClientTicket decrypt path, so wolfSSL_SSL_CTX_remove_
session could not locate them to evict.
* wolfSSL_SSL_CTX_remove_session always returned 0 on success
regardless of whether the session was actually in the cache,
diverging from OpenSSL's SSL_CTX_remove_session (1 on success,
0 on not-found).
Changes:
* src/internal.c: ctx->maxEarlyDataSz defaults to 0; applications
must opt in with wolfSSL_CTX_set_max_early_data.
* src/tls13.c: #error when WOLFSSL_EARLY_DATA is built with
HAVE_SESSION_TICKET and NO_SESSION_CACHE. Escape hatch
WOLFSSL_EARLY_DATA_NO_ANTI_REPLAY for deployments that take
application-layer responsibility.
* wolfssl/internal.h: imply WOLFSSL_TICKET_HAVE_ID from
WOLFSSL_EARLY_DATA so stateless-ticket issuance populates the
cache under an ID that eviction can find.
* src/ssl_sess.c: wolfSSL_SSL_CTX_remove_session returns 1 when the
session was found (internal-cache hit, or ctx->rem_sess_cb fired
for an external cache), 0 otherwise. Matches OpenSSL semantics.
* src/tls13.c: the 0-RTT acceptance condition in CheckPreSharedKeys
now calls wolfSSL_SSL_CTX_remove_session and checks its return:
the eviction is the check. If the session was in the cache, 0-RTT
is accepted and the single-use requirement is satisfied. If not,
the early_data extension is rejected through the normal path so
the record layer correctly skips in-flight 0-RTT records.
WOLFSSL_MSG at each rejection site.
* doc/dox_comments/header_files/ssl.h: document runtime opt-in.
* tests: four new tests —
test_tls13_0rtt_default_off (fails without default-to-0 fix),
test_tls13_0rtt_stateless_replay (fails without TICKET_HAVE_ID
implication and remove_session gate),
test_tls13_remove_session_return (fails without return-value fix),
test_tls13_0rtt_ext_cache_eviction (fails without ext-cache
counts-as-found fix).
test_tls13_early_data explicitly opts in via
wolfSSL_CTX_set_max_early_data.
tests/api.c: two SSL_CTX_remove_session == 0 assertions updated
to == 1.
ReadPemFromBioToBuffer slurps the entire BIO in one shot, so iterative
callers like wolfSSL_PEM_read_bio_X509_CRL (and by extension
wolfSSL_X509_load_crl_file's BIO branch) saw EOF after the first block
and silently dropped every CRL after the first in a multi-CRL bundle.
Refactor wolfSSL_PEM_read_bio_X509_CRL to delegate to
wolfSSL_PEM_X509_X509_CRL_X509_PKEY_read_bio, which already reads one
PEM BEGIN/END pair per call and leaves the BIO positioned just past the
END line. Loop over it so we skip past intervening cert/key blocks and
return the next CRL in the stream — matching OpenSSL's
PEM_read_bio_X509_CRL, verified against OpenSSL 3.0.13 with cases
{cert,CRL}, {CRL,cert}, {CRL,cert,CRL}, {key,CRL}, {CRL,key,CRL}: in
each case OpenSSL skips non-CRL blocks until EOF.
When the caller passes a non-NULL `x` whose `*x` is already populated,
free the previous CRL before overwriting the slot — matching the
d2i_X509_CRL reuse contract the old body relied on.
To keep both helpers visible at the new call site, drop their `static`
qualifier (wolfSSL_PEM_X509_X509_CRL_X509_PKEY_read_bio for the per-block
read, wolfSSL_X509_PKEY_free to free defensively-allocated keys parsed
from intervening non-CRL blocks). Their definitions in src/x509.c and
declarations in wolfssl/internal.h are widened from OPENSSL_ALL to
OPENSSL_EXTRA || OPENSSL_ALL so the OPENSSL_EXTRA-only build (which
compiles wolfSSL_PEM_read_bio_X509_CRL) links cleanly. The unrelated
INFO_read_bio / INFO_read_bio_X509_INFO group below them keeps its
OPENSSL_ALL gate because it depends on wolfSSL_X509_INFO_new/free that
are still OPENSSL_ALL-only.
Also register the previously-orphaned test_wolfSSL_X509_load_crl_file
(its slot in TEST_OSSL_X509_LOOKUP_DECLS was a duplicated
test_wolfSSL_X509_LOOKUP_ctrl_hash_dir entry), update its assertion for
crl2.pem (which already contains two CRLs) to expect 2 instead of 1, and
add a multi-CRL bundle case that builds a memory BIO from
crl.pem + server-cert.pem + crl2.pem and asserts that the reader walks
past the cert and returns all 3 CRLs before NULL.
- tls.c: TLSX_CertWithExternPsk_GetSize takes word16*, but length was
widened to word32 in TLSX_GetSize. Use the hsz staging variable like
the other cases so WOLFSSL_CERT_WITH_EXTERN_PSK builds compile.
- tls.c: silence -Wunused-variable for hsz in builds where every case
that consumes it (TLS 1.3, PSK, ETM, early data, PHA, cookie, cert
with extern PSK) is compiled out, e.g. user_settings_tls12.h.
- test_tls_ext.c: assert session->ticketLen > 0 before mutating
ticketAdd in the ticket-age out-of-window test so it fails loudly if
no NewSessionTicket was received.
The CA Names extension size accumulator was a word16. With enough
CA entries (or large DER-encoded names) the running total can wrap
silently, leaving TLSX_CA_Names_Write to overflow an undersized
extension buffer. Match TLSX_SNI_GetSize: use a word32 accumulator
and return 0 when the total exceeds WOLFSSL_MAX_16BIT.