Clean up includes single return point from every function, consistent formatting function block comment and tests added.
ssl_api_rw.c: Cleanup; 6 helpers extracted from write_dup/shutdown; fixed a SendBuffered corner case that returned 0 instead of an error.
ssl_api_ext.c: Cleanup; extracted wolfssl_ticket_key_cb_process, wolfssl_rehandshake_prepare.
ssl_api_hs.c: Cleanup + de-indent; 9 helpers extracted, incl. shared wolfssl_handshake_flush/_done between connect and accept (~100 duplicated lines removed). wolfSSL_connect/accept left multi-exit.
ssl_api_cert.c: Cleanup of newer functions at end of file; extracted PushPeerCertToChain; fixed a double free in CreatePeerCertChain; fixed 5 wrong @param names.
ssl_api_crl_ocsp.c: Full cleanup of all 49 functions; wolfSSL_OCSP_parse_url rewritten; fixed a URL with no host returning success with the rest of the URL as the host; added IPv6 literal support; fixed 4 wrong WOLFSSL_ENTER names.
ssl.c: Moved the x509GetIssuerFromCM forward declaration here for clarity.
linuxkm/x86_vector_register_glue.c: in wc_save_vector_registers_x86(), properly squelch the hard-IRQ call warning if flags & WC_SVR_FLAG_MAYBE_INHIBIT;
src/internal.c: in AllocKey(), properly set key_inited = 1 if wc_SlhDsaKey_Init() succeeds;
.github/workflows/fips-dev-no-post.yml: test -DWC_FIPS_AESGCM_NO_SHORT_NONCES.
.github/workflows/fips-dev-no-post.yml: add reporting of fuzzing seed.
wolfcrypt/src/aes.c, tests/api/test_aes.c, .wolfssl_known_macro_extras:
* change FIPS AES-GCM nonce size restrictions from from opt-out (WC_FIPS_AESGCM_ALLOW_SHORT_NONCES) to opt-in (WC_FIPS_AESGCM_NO_SHORT_NONCES).
* apply restrictions only on encryption operations, never on decryption.
configure.ac: fix HAVE_FIPS_VERSION of fips-ready; fix enable_dh setup in KERNEL_MODE_DEFAULTS setup; fix help message for --enable-dh.
linuxkm/x86_vector_register_glue.c: add dump_stack() on each BUG/WARNING message that didn't already have it.
src/tls.c: fix a couple leaks in TLSX_KeyShare_GenDhKey().
tests/swdev/swdev.c: gate src->sha_method access in swdev_sha256_copy_state() and swdev_sha512_copy_state() appropriately.
tests/unit.c: conditionally include dh.h, to assure wc_dh_enable() is available.
wolfcrypt/src/dh.c:
* in wc_InitDhKey_ex(), zero the key at entry unless null, remove duplicate key->trustedGroup = 0, and call wc_FreeDhKey() on error at end.
* add missing wc_dh_enabled checks in wc_DhGeneratePublic() and wc_DhGenerateParams().
wolfcrypt/src/error.c: fix missing space in FIPS_UNAPPROVED_E string.
b/wolfssl/wolfcrypt/settings.h: sense __SIZEOF_INT128__ and if defined, but HAVE___INT128_T and/or HAVE___UINT128_T are undefined, define them.
wolfcrypt/src/falcon.c: tweak the gate on __uint128_t availability to lean solely on HAVE___UINT128_T.
wolfcrypt/src/random.c: fix a couple missed WC_NO_ERR_TRACE() wrappers.
wolfcrypt/src/rng_bank.c: properly tolerate WC_ACCEL_INHIBIT_E as a retval from bank->affinity_lock_cb().
wolfcrypt/src/sha256.c, wolfcrypt/src/sha512.c: move #undef WC_C_DYNAMIC_FALLBACK for WOLFSSL_AESNI without USE_INTEL_SPEEDUP to follow all includes, assuring no struct layout conflict.
wolfcrypt/src/sha512.c: fix wrong call in intelasm Transform_Sha512() !WC_C_DYNAMIC_FALLBACK SHA512_C path.
wolfcrypt/test/test.c: fix double-WC_TEST_RET_ENC_EC() in mldsa_param_*_vfy_test().
* in AllocKey capture and propagate the return of the per-type wc_*_init_ex
calls (ed25519, ed448, falcon, ML-DSA, ML-KEM, ...) instead of discarding it,
so an init failure surfaces rather than leaving a partially-constructed key
for later use.
* The `default:` arm of the type switch now sets `ret = BAD_FUNC_ARG` and
breaks, instead of returning directly, so it reaches the common cleanup.
* The failure cleanup distinguishes the two states: if the key was initialized,
FreeKey(); otherwise XFREE(*pKey) and NULL the caller's pointer -- previously
an allocation that failed before init leaked.
* Two mis-copied #endif comments corrected: HAVE_CURVE25519 -> HAVE_ED25519 and
HAVE_CURVE448 -> HAVE_ED448.
and AM_CONDITIONAL BUILD_FIPS_NO_POST. Refactor FIPS dev/ready version setup:
hoist ENABLED_FIPS_DEV / ENABLED_FIPS_READY to set -DWOLFSSL_FIPS_DEV /
-DWOLFSSL_FIPS_READY centrally, and switch the FIPS AS_CASE arms from
`test "$FIPS_VERSION" != "dev"` to `test "$ENABLED_FIPS_DEV" != "yes"` so the
dev semantics extend to v5-dev/v6-dev/lean-aesgcm-dev.
src/include.am: under !BUILD_FIPS_NO_POST, drop fips.c / fips_test.c /
wolfcrypt_first.c / wolfcrypt_last.c from the build (dev-no-post uses no fips
repo content).
wolfssl/wolfcrypt/settings.h, wolfssl/wolfcrypt/wc_compat.h: under
WOLFSSL_FIPS_DEV_NO_POST, squat WOLF_CRYPT_FIPS_H to inhibit fips.h, and change
the FIPS_READY/DEV version block guard to `!defined(HAVE_FIPS_VERSION)`
(required so an externally supplied version is not clobbered).
wolfssl/wolfcrypt/fips_test.h: add WOLFSSL_FIPS_DEV_NO_POST stub block
(fipsCastStatus_get, the PRIVATE_KEY macros) so master builds without the fips
repo.
linuxkm/linuxkm_wc_port.h, linuxkm/module_hooks.c: accommodate
WOLFSSL_FIPS_DEV_NO_POST (guard verifyCore / CAST / fencepost paths that the
fips repo would otherwise provide; force WC_USE_PIE_FENCEPOSTS_FOR_FIPS).
wolfcrypt/test/test.c: in hmac_sha256_test(), don't expect HMAC_KAT_FIPS_E in
WOLFSSL_FIPS_DEV_NO_POST builds.
wolfssl/wolfcrypt/types.h: add stub macro for
wolfCrypt_SetPrivateKeyReadEnable_fips() when WOLFSSL_FIPS_DEV_NO_POST.
EccVerify() dereferenced key unconditionally in both of its async spots:
ret = wolfSSL_AsyncInit(ssl, &key->asyncDev, WC_ASYNC_FLAG_CALL_AGAIN);
...
ret = wolfSSL_AsyncPush(ssl, &key->asyncDev);
Its siblings EccSign() and VerifyRsaSign() wrap both calls in if (key),
because with HAVE_PK_CALLBACKS the private key can live only in the
callback and is never decoded into ssl->hsKey. The self verify step that
WOLFSSL_CHECK_SIG_FAULTS performs after signing passes that same
ssl->hsKey to EccVerify(), so an --enable-asynccrypt build dereferences
NULL there: at the init on entry, and again at the push if EccVerifyCb
returns WC_PENDING_E. Note wc_ecc_verify_hash() cannot produce that
second case, since it rejects a NULL key before starting any async work.
The check on the push goes inside the pending branch rather than into its
condition, because unlike EccSign() this block has an else that maps a
non zero ret onto a verification failure. Extending the condition would
send a pending result down that else and log a bogus
WOLFSSL_ERROR_VERBOSE() for it.
Reproduced with --enable-asynccrypt --enable-pkcallbacks
--enable-faultharden --enable-ecc --enable-supportedcurves and
-DTEST_PK_PRIVKEY, which is what makes examples/server.c leave the
private key to the callback. Running examples/server against
examples/client with ECC certificates, the server takes SIGSEGV without
this change and exits cleanly with it, reaching the verify callback with
keySz 0 instead. The push needed one more step to reach: the in tree
myEccVerify() is synchronous, so it was patched to return WC_PENDING_E
once, after which the server dies in wolfAsync_EventQueuePush() at the
offset of ecc_key.asyncDev, reached from EccVerify() by way of
SendTls13CertificateVerify().
Note the crash is only reachable once the two callers stop dereferencing
ssl->buffers.key->length on the line just above the EccVerify() call,
which is what PR 11000 fixes. Both sites were patched locally to
reproduce. This guard is needed in addition to that fix, not instead of
it, and only matters for asynchronous builds.
Second review follow-up on the async record layer series.
Stop the probe reselecting the cipher side. BuildMessage()'s BUILD_MSG_BEGIN
case can call SetKeysSide() for DTLS with secure renegotiation, which swaps
the active encryption state and clears recordSzOverhead. That is not part of a
size calculation, and after the previous commit the suspended build survives
to resume against whatever side the probe last chose, so a DTLS 1.2 record
suspended for PREV_ORDER could resume against the renegotiation keys. Skip it
when sizeOnly is set; the sizes are the same either way.
The probe itself has to keep running. Not re-entering BuildMessage at all
while a build is suspended looks tidier, but wolfssl_local_GetMaxPlaintextSize()
derives the DTLS fragment size from this result, so falling back to the upper
bound there shrinks fragments inconsistently between calls and the MTU
reproducer fails its buffer comparison. Saving and restoring the two fields is
what keeps the answer exact.
Resume inside the record when handshake content is left. The previous commit
declined to skip the padding for a fragmented or coalesced
certificate_request, which was right, but left processReply at doProcessInit
with the index inside the record, so the resume still started a fresh record
parse in the middle of one. Mirror both halves of the end of record block
instead: set runProcessingOneMessage when content remains, advance past the
padding only at the boundary.
Note the shared state at the source. BuildMessage() and BuildTls13Message()
write ssl->options.buildMsgState even for a sizeOnly probe with asyncOkay
clear, where everything else goes to the caller's own arguments. Nothing said
so at those sites, so the next sizeOnly caller would reintroduce this.
Record why only one of the three wc_ecc_make_key_ex() calls in eccsi.c needs
a wait: the other two are preceded by wc_ecc_free(), which clears the marker
their pending path is gated on. Moving either free would make them pend.
Test changes. Force the overhead cache cold before probing, otherwise an AEAD
suite answers from the cache without ever calling BuildMessage and the
assertions hold no matter what the probe did. Compare against BuildMessage's
own figure rather than only checking the size is positive, and run the whole
thing for TLS 1.3 as well as TLS 1.2, since BuildTls13Message() clobbers the
state by a different route: its sizeOnly return bypasses exit_buildmsg
entirely. Checked by stubbing the restore out again, which fails the test.
Also spell the new guard in cryptocb_test() as #if defined(WOLFSSL_ASYNC_CRYPT)
to match the rest of that file, which uses that form 170 times against 4.
Review follow-up on the two preceding commits.
Only skip the padding when the record content is actually consumed. The
post-handshake auth advance ran on any pending return that left processReply
at doProcessInit, which assumes the certificate_request was the last message
in its record. Two states break that: a fragmented certificate_request makes
DoTls13HandShakeMsg() rewind inOutIdx so the fragment can be reprocessed, and
RFC 8446 section 5.1 lets a peer coalesce several handshake messages into one
record, leaving the index inside the record. Adding padSz in either case
points the index at record content. Both states are already mishandled
without this series, since post-handshake auth forces processReply to
doProcessInit regardless, so this is a narrower guard rather than a
regression, and there is no memory safety consequence either way: the largest
possible index is the end of the record. The new test mirrors the end of
record check that follows the message handlers.
Also restore buildArgsSet across the record size probe. The probe borrows
buildMsgState and buildArgsSet from the suspended build; the previous commit
put back only the first. FreeBuildMsgArgs() clears buildArgsSet
unconditionally on the way out, and the resumed call skips the block that
would set it again, so the flag stays clear for the rest of the record. That
is currently harmless, because the only thing it guards is freeing a
dynamically allocated IV and no cipher in the tree needs one, but it leaves
the fix one field short of its own premise.
Add test_record_size_preserves_build_msg_state, which parks a connection at
BUILD_MSG_ENCRYPT with the arguments flag set, runs the probe, and requires
both fields to survive. It fails without the restore and passes with it.
Guard the post-handshake auth block on WOLFSSL_POST_HANDSHAKE_AUTH as well.
The only code that leaves processReply at doProcessInit from inside
DoTls13HandShakeMsg() is the post-handshake auth branch, which is itself
behind that macro, so the check cannot fire without it.
Say that the oversized record is rejected rather than that it overflows the
buffer. BuildMessage() catches it and returns BUFFER_E; the old wording read
like memory corruption.
Verified with --enable-all --enable-asynccrypt-sw and with plain --enable-all,
both warning free under the os-check CFLAGS. The two reproducers from the
preceding commits still complete.
Writing application data over DTLS fails with -132 BUFFER_E in an
asynchronous build once a record goes pending. It is reproducible with
the shipped examples:
./examples/server/server -e -u -f -v 3 -l ECDHE-RSA-AES128-SHA -2 -p 11151 &
./examples/client/client -B 4000,1310 -u -f -v 3 -l ECDHE-RSA-AES128-SHA -2 -p 11151
The client reports "Oops, want to write past output buffer size" and the
write fails. This is the DTLS MTU configuration from tests/test-dtls-mtu.conf.
SendData() sizes the output buffer by calling wolfssl_local_GetRecordSize(),
which runs BuildMessage() with sizeOnly set and asyncOkay clear. That probe
keeps its own arguments on the stack, but the state machine it drives lives
in ssl->options.buildMsgState, which is shared with the asynchronous
BuildMessage() that may still be in flight for the same record.
Because SendData() re-probes the size on every retry, the sequence is:
the real BuildMessage() suspends at BUILD_MSG_ENCRYPT, the retry's probe
resets the shared state to BUILD_MSG_BEGIN and leaves it there, and the
resumed call then re-runs the begin and size stages against arguments that
already carry the header and cipher overhead. For a 1310 byte payload with
ECDHE-RSA-AES128-SHA that takes the header from 13 to 21 bytes and the
record from 1361 to 1417, which no longer fits the 1361 byte buffer the
first probe correctly sized.
Save the state around the probe and put it back afterwards. The probe still
needs to start from BUILD_MSG_BEGIN to compute a size, so restoring is the
narrowest fix; the arguments themselves are already private to the probe.
Verified with --enable-all --enable-asynccrypt-sw: the reproducer above goes
from a failed write to completing its benchmark. Full make check in that
configuration passes, 17 passed and 6 skipped with no failures, though that
run also needs the wolfcrypt test fixes for ECIES, ECCSI and the crypto
callback test, which that configuration trips over first and which are not
part of this change.
Non-async builds are unaffected, since the saved value is only read and
written under WOLFSSL_ASYNC_CRYPT.
A TLS 1.3 client doing post-handshake authentication fails the next read
with -326 VERSION_ERROR in an asynchronous build, and the connection dies.
It is reproducible with the shipped examples:
./examples/server/server -v 4 -l TLS13-AES128-GCM-SHA256 -Q -2 -p 11119 &
./examples/client/client -v 4 -l TLS13-AES128-GCM-SHA256 -Q -2 -p 11119
DoTls13CertificateRequest() handles a post-handshake CertificateRequest by
resetting the handshake states, setting processReply back to doProcessInit
and calling wolfSSL_connect_TLSv13() to send the certificate, certificate
verify and finished flight. It runs that from inside DoTls13HandShakeMsg(),
which is itself inside DoProcessReplyEx().
When the flight goes pending, WC_PENDING_E travels back out of
DoTls13HandShakeMsg() and DoProcessReplyEx() returns immediately, so the
end of record accounting that follows never runs and the record is left
short by ssl->keys.padSz bytes. The client resumes through
wolfSSL_negotiate() from ReceiveData() rather than by reprocessing the
record, so nothing ever advances past those bytes. The next ProcessReply()
starts a fresh record at that offset and parses the record's own MAC as a
record header, which fails the version check in GetRecordHeader().
Measured on the failing read: idx 50, length 67, padSz 17, with the header
bytes decoding as type 0x16 and version 0x70 0xEA.
Advance past the padding when the handler returns pending having already
marked the record complete. processReply is what distinguishes the two
cases: post-handshake auth leaves it at doProcessInit, while an ordinary
pending message leaves it at runProcessingOneMessage and must not be
advanced, since that one really is reprocessed on resume. Confirmed by
tracing every pending return in the failing run: three ordinary ones at
runProcessingOneMessage, and only the post-handshake one at doProcessInit,
where idx plus padSz lands exactly on the buffer length.
This mirrors what the early data path a few lines below already does when
it returns early after marking the record done.
Synchronous builds are unaffected. wolfSSL_connect_TLSv13() completes in
place there, DoTls13HandShakeMsg() returns 0, and the normal accounting
runs.
Implement SLH-DSA (SPHINCS+, FIPS 205) as an entity authentication
algorithm for the TLS 1.3 and DTLS 1.3 handshake, following
draft-reddy-tls-slhdsa. All twelve parameter sets (SHAKE and SHA2 families,
128/192/256 in the f and s variants) are wired into the handshake for
signing and verifying the CertificateVerify message; test certificates and
configs cover the 128f and 128s sets.
Handshake integration:
- Map the SLH-DSA signature schemes to and from the wire in the
signature_algorithms extension and CertificateVerify. The mapping,
advertisement, and OID handling are gated per parameter set so a build
only offers, accepts, and maps the variants actually compiled in
(including partial SHA2 builds).
- Sign and verify the CertificateVerify with an SLH-DSA entity key, and
load SLH-DSA private keys and certificates (ssl_load.c, ssl.c,
ssl_api_pk.c, asn.c).
- Preserve the verify return code on a failed SLH-DSA CertificateVerify
rather than flattening every non-zero result to SIG_VERIFY_E.
wc_SlhDsaKey_Verify already returns SIG_VERIFY_E on a real mismatch, so
the failure semantics are unchanged while WC_PENDING_E (async crypto
callbacks) and hard errors now propagate, matching ML-DSA and Falcon.
Protocol version gating:
- SLH-DSA is defined for TLS 1.3 only, so the schemes are no longer offered
to a TLS 1.2 peer, and MatchSigAlgo and PickHashSigAlgo pin an SLH-DSA
certificate both to the scheme for its exact parameter set and to
TLS 1.3.
- Reject a Falcon, ML-DSA or SLH-DSA key in the TLS 1.2 CertificateVerify
with SIG_TYPE_E. No signature scheme below TLS 1.3 covers a post-quantum
key, the record is reserved for a classic signature, and the signing
switches have no post-quantum case, so continuing would have sent the
reserved buffer's uninitialized tail.
Streamed CertificateVerify send:
- SLH-DSA signatures are large (up to ~50 KB). When the CertificateVerify
body exceeds a single record, generate the signature into a
connection-level buffer and emit it one record at a time so the output
buffer never has to hold the whole signature. This keeps peak memory near
one signature plus a single fragment and resumes correctly across a
non-blocking WANT_WRITE without recomputing the randomized signature.
Gated by WOLFSSL_TLS13_STREAM_CERT_VERIFY (TLS 1.3, non-async, PQC
signatures); DTLS and WOLFSSL_ASYNC_CRYPT keep the existing in-place
fragmented path.
- Drop a half-sent streamed CertificateVerify in wolfSSL_clear. Left in
place, the resume guard would fire on the next handshake and re-send the
previous one's signature into a different transcript.
- Dual-algorithm (WOLFSSL_DUAL_ALG_CERTS, BOTH) CertificateVerify bodies are
streamed as well. The combined two-signature body may include a
variable-length signature, so the body buffer is sized from the
per-signature upper bounds and the exact length is recorded after signing;
the small trailing slack is never sent.
Buffer sizing:
- Keep MAX_X509_SIZE a fixed 9 KB for post-quantum builds. It sizes a
static per-certificate slot embedded by value in every cached session, so
it must not scale with a post-quantum signature; nor may it derive from
the enabled ML-DSA level, or a level-restricted build would silently drop
certificates that a full build keeps.
- Add MAX_CERT_WIRE_SZ for the largest certificate that may appear in a
handshake message, sized from the enabled post-quantum signatures, and
derive MAX_CERTIFICATE_SZ from it instead of from MAX_X509_SIZE.
- Add MAX_CERT_MSG_DEPTH for the chain depth assumed when sizing the
certificate message. MAX_CHAIN_DEPTH bounds how deep a chain may be
verified, while this sizes a buffer an unauthenticated peer can make us
allocate, so it is trimmed to 5 when a post-quantum certificate has
inflated the per-certificate size. Classic builds are unchanged.
- Size the CertificateVerify buffers from the actual signature length
instead of the worst-case WC_MAX_CERT_VERIFY_SZ, which balloons with
SLH-DSA. WC_MAX_CERT_VERIFY_SZ is retained for API compatibility and its
growth is documented in README.md.
- Order Scv13Args widest member first so it carries no interior padding and
still fits ssl->async->args under WOLFSSL_ASYNC_CRYPT together with
WOLFSSL_DUAL_ALG_CERTS.
Dual-algorithm certificates:
- Reserve the two signature length prefixes in the in-place
CertificateVerify sizing that the streamed path already accounted for.
- Build the PreTBS for an alternative signature check from the certificate
size minus both signatures, and retry once at a size the canonical
re-encode cannot exceed when that estimate turns out short. The estimate
keeps the allocation small on constrained targets, and wc_GeneratePreTBS
reports an encoder failure as WOLFSSL_FAILURE, which is zero, so a
non-positive result is now an error instead of silently skipping
ConfirmSignature and reading as a verified signature.
Device held private keys:
- Support an SLH-DSA private key that lives in a device and is referenced
by id or label. The parameter set cannot be recovered from a device side
identifier, so it is carried from the key type down to
wc_SlhDsaKey_Init_id and wc_SlhDsaKey_Init_label, and the key is released
with wc_SlhDsaKey_Free once the certificate and key pair is checked.
Robustness:
- Check the SlhDsaParamToType, wc_SlhDsaKey_PublicSizeFromParam and
wc_SlhDsaParamToOid results in the certificate and key load paths.
- Zeroize an SLH-DSA key before wc_SlhDsaKey_Init, which can return
NOT_COMPILED_IN before it clears the object, in both the certificate load
path and AllocKey.
- Take the alternative key's parameter set from the certificate's sapkiOID
rather than keyOID, which describes the native key.
- Re-initialise across hash families in wc_SlhDsaKey_PublicKeyDecode as
wc_SlhDsaKey_PrivateKeyDecode already does. The hash objects share a union
selected by family, so importing across families writes the new family's
state over the old one's and orphans it.
- Copy pkCurveOID in SetSSL_CTX when only SLH-DSA is enabled, matching the
struct member guard. Without it the field stayed zero and the signature
scheme matching above was dead in exactly that build.
- Derive the per parameter set WOLFSSL_SLHDSA_PARAM_NO_* macros from the
group level exclusions, and select WC_SLHDSA_DEFAULT_PARAM with those
same macros, so the parameter table and the TLS mappings cannot disagree.
- Add SLH-DSA to the lean build WOLFSSL_MAX_SIGALGO carve-out, since twelve
more entries no longer fit the small list.
- Prefix the new SLHDSA_ALL_NO_* macros in the installed header with WC_.
Tests and certificates:
- Add SLH-DSA entity (client and server) certificates for the SHAKE and
SHA2 128f and 128s parameter sets, and update the generation script.
- Add TLS 1.3 and DTLS 1.3 entity-cert CertificateVerify test configs
covering the fragmented (128f) and single-record (128s) send paths for
both hash families, wired into suites.c. These sign with the entity key,
so they are excluded from verify-only builds.
- Interrupt the streamed CertificateVerify with one WANT_WRITE and with
several on the same record, and assert the handshake still completes and
re-emits identical bytes, which the blocking .conf handshakes never
exercise. The record to interrupt is counted first, because the server's
record batching differs between builds. Where the flight is flushed as a
single write the send is retried below SendTls13CertificateVerify, so
these do not by themselves cover the fragOffset resume path.
- Drive the streamed path with an ML-DSA leaf under a negotiated
max_fragment_length, covering it for a non SLH-DSA algorithm.
- Reject a TLS 1.2 handshake that presents an SLH-DSA client certificate.
- Map every compiled-in scheme from its wire code point to the key OID, and
extend the exhaustive SaToNid coverage with the twelve new algorithms.
- Accept an SLH-DSA private key referenced by id and by label.
Build configuration:
- configure.ac: --enable-slhdsa now keeps the certificate/ASN code enabled
(as --enable-mldsa does), so an SLH-DSA-only build with RSA, ECC and DH
disabled configures instead of erroring that ASN is off.
- Guard the WOLFSSL, WOLFSSL_CTX and WOLFSSL_X509 pkCurveOID members for
WOLFSSL_HAVE_SLHDSA, so an SLH-DSA-only build declares the field the
handshake and CopyDecodedToX509 already reference under an SLH-DSA guard.
- Mark checkKeySz used in the SLH-DSA branch of ProcessBufferCertPublicKey;
SLH-DSA is the only certificate signature algorithm with no minimum-size
check, so an SLH-DSA-only build otherwise tripped -Wunused-parameter.
- Propagate haveSlhDsaSig in wolfSSL_set_SSL_CTX, which copied the Falcon
and ML-DSA flags but not the SLH-DSA one.
- CI: add a SHA2-only SLH-DSA build (--enable-slhdsa=sha2) so the
SHAKE-disabled combined-maxima guards are exercised, and an async crypto
build with dual-algorithm certificates, which is the only configuration
that compiles the in-place fragmented CertificateVerify send.
With WOLFSSL_RW_THREADED the read path performs no scheduled work, because
transmitting from the reader would race the write thread over the output
buffer and the sending key schedule, neither of which is covered by a lock.
Post-handshake the only remaining consumers are on the write path, and
wolfSSL_dtls_retransmit() only helps while the handshake is unfinished. An
application that reads without writing therefore never acknowledges a
NewSessionTicket, KeyUpdate or connection ID message, and the peer keeps
retransmitting what it is waiting to have acknowledged. RFC 9147 relies on
those ACKs, so this is a protocol level break rather than a missed
optimisation.
Add wolfSSL_dtls13_do_scheduled_work() so such an application can send that
work from its write thread, and wolfSSL_dtls13_pending_work() so it can tell
when there is any.
Both entry points ask the same helpers rather than each testing conditions of
their own, so they cannot drift into the predicate promising work the pump
then declines or silently discards, which would leave a drain loop spinning or
mislead the caller about what happened. That covers key updates in particular:
none is sent while one of ours is unacknowledged, since DTLS must not have two
in flight and Tls13UpdateKeys() drops a locally scheduled one in that state,
and a peer request is kept rather than dropped until it can be answered. The
predicate also errs towards reporting work when it cannot tell, so a loop
surfaces the error rather than stopping silently.
Refusing an object is treated as a usage error and leaves ssl->error alone.
That field is sticky, since SendData() only clears it for WANT_WRITE, pending
async work and the DTLS MAC and decrypt cases, and wolfSSL_write() skips its
write-dup drain while it is set, so recording one would disable the very drain
a write-dup application depends on.
Write-dup pairs are out of scope on both sides. They park the read side's work
in the shared WriteDup struct, which only wolfSSL_write() reconciles, so they
already have a drain. Completing a key update we started ourselves is out of
scope too: that needs the peer's acknowledgement processed, which rotates the
sending keys and creates an epoch, and the epoch table has no locking while
the read thread mutates it as well.
The declaration is gated to match where the definitions live, so a lean build
is not promised a symbol it does not get.
The encap and decap paths create a temporary RNG, install it into a key that
belongs to the caller so the shared secret computation can blind, and then
free it without taking it back out. Both wc_ecc_set_rng and
wc_curve25519_set_rng only record the pointer, and nothing else ever writes
that field, so the caller's key was left pointing at freed memory. Encap does
this to the ephemeral key and decap to the receiver key, including the
curve25519 branch. Repeated HPKE calls hide it because each one installs a
fresh RNG first, but any other use of the key that consults it, such as an
ECDH or a signature under timing resistance, reads the freed object. ECH
holds exactly such a long lived key.
Save whatever RNG the key already had and put it back before freeing the
temporary one, so the key is handed back to the caller unchanged. Clearing
the field instead would silently drop an RNG the caller had installed for
blinding of their own.
Extend the HPKE round trip test to give both keys an RNG of its own and to
require them to still have it once the seal and open have finished.
Fixes F-7083.
SSL_clear recycles a WOLFSSL object for a new connection, which is the usual
pattern in connection pooling servers, and wolfSSL_shutdown calls it on
success as well. It reset the option and state fields but left every piece of
key material from the previous connection in place. The teardown path in
SSL_ResourceFree is careful here and force zeroes the keys struct and the TLS
1.3 traffic secrets, so a reused object ended up holding material that a
freed one would not.
The keys struct keeps the write keys, MAC secrets and IVs, clientSecret and
serverSecret keep the TLS 1.3 traffic secrets, the DTLS 1.3 epoch table keeps
traffic keys, IVs and sequence number keys for every epoch, and the handshake
arrays keep the master secret, the pre master secret, the PSK key and the TLS
1.3 key schedule secret. The tls-unique fields keep the Finished values of the
connection that just ended, so the next caller could bind to the wrong
session. The buffers are sized for the largest supported algorithm, so a later
handshake that negotiates something smaller only overwrites a prefix and the
tail survives.
Force zero all of it. A freshly created object has these zeroed already, with
two exceptions that are put back after the wipe: the multicast peer identifier
sentinel, and the unprotected DTLS 1.3 epoch 0 together with the epoch
pointers aimed at it, which only InitSSL sets up and without which the next
handshake has no valid epoch.
Wipe the handshake arrays in place rather than releasing them. They have to
stay allocated because wolfSSL_set_secret, the exporter and the accessors that
run after a connection all read from them on an object that is being recycled
rather than freed, and because the key agreement routines take preMasterSz as
the size of the buffer they may write, so that is restored to what a freshly
allocated Arrays carries. An application that asked to keep the arrays still
gets back everything the API can hand it, so the master secret and the
exporter secret only go when it did not ask, while the pre master secret, the
PSK key and the key schedule secret always do because nothing reads those
back.
wolfSSL_set_secret and wolfSSL_make_eap_keys both reached into the arrays
without checking that they are there, which the ordinary handshake teardown
can already leave them not to be, so both now report a bad argument instead.
Add a regression test that runs a handshake, clears the object with the arrays
kept, and requires the write keys, both traffic secrets and the pre master
secret to be gone while the master secret, the exporter secret and the client
random survive. It then takes that request back, clears again, and requires
the master and exporter secrets to be gone with the arrays themselves still
present.
Fixes F-7258.
WOLFSSL_HMAC_CTX keeps a copy of the inner and outer pads outside the
embedded wolfCrypt HMAC object so that a later init with a NULL key can
restore the key. Those pads are the key combined with the fixed padding, so
for any key no longer than the hash block size the key falls out of a single
exclusive or. Cleanup only called wc_HmacFree on the embedded object, which
zeroes what it is given but cannot reach the enclosing context, so the saved
pads survived. HMAC_CTX_free then returned that heap block to the allocator
with the key material still in it, where it stayed until some later
allocation happened to overwrite it.
Wipe both saved pads in wolfSSL_HMAC_cleanup, which HMAC_CTX_cleanup,
HMAC_CTX_reset and HMAC_CTX_free all reach. Do the same on the set key
failure path in the init function, since the context is reported as unkeyed
there while the previous key's pads would otherwise remain.
The session ticket key callback had the same leak for the same reason. It
holds a WOLFSSL_HMAC_CTX on the stack, hands it to the application to be
keyed with the long lived ticket HMAC key, and then only freed the embedded
object, leaving the pads on the stack after every ticket encrypt and every
ticket decrypt including the error paths. Have it clean up through
wolfSSL_HMAC_CTX_cleanup so it picks up the wipe.
Add a regression test that keys a context, checks the pads were populated,
runs cleanup and requires both arrays to be zero.
Fixes F-7256 and F-7257.
wolfSSL_X509_check_host takes an explicit length and its own validation
accepts a buffer with no NUL terminator, since only an embedded NUL is
rejected and a trailing one is merely stripped when present. The iPAddress
check then called CheckIPAddr, which drops the length and measures the
buffer with XSTRLEN, reading past the end of a caller supplied buffer that
is length delimited rather than terminated. This ran on every call, not
only when checking an IP address, and is compiled in whenever
WOLFSSL_IP_ALT_NAME is defined, which OPENSSL_ALL and WOLFSSL_QT enable.
Call CheckHostName directly with the caller's length and the IP flag set.
That is what CheckIPAddr does internally, minus the length being recomputed.
Behaviour is unchanged for NUL terminated input, because the normalization
above already leaves chklen equal to the string length in that case. It
also fixes a matching bug, since a length delimited IP address sitting in a
longer buffer no longer fails to match an iPAddress entry.
Add a regression test covering an interior slice of a longer buffer and a
buffer sized exactly to the name with no terminator.
Fixes F-7248.
X509PrintDirType derived the length of the alt name payload with XSTRLEN.
A directoryName entry holds raw DER, which routinely contains zero bytes,
and under WC_ASN_NO_HEAP the buffer is not NUL terminated at all, so the
computed length was wrong in general and reading it was already out of
bounds in the no-heap case. The length then fed an unsigned loop bound of
src_len - 5, so any entry that XSTRLEN measured as shorter than five bytes
wrapped the bound to nearly UINT32_MAX and the tag scan read far past the
end of the entry. A certificate with a short or empty directoryName alt
name reaches this from the ordinary certificate printing path.
Take the length from entry->len, which the parser already fills in, and
return early when the entry is too short to hold an OID, a tag and a
length. The scan is unchanged for entries of five bytes or more, and the
ASN parsing helpers now receive the true buffer length as their bound.
Add a regression test that prints a certificate carrying a directoryName
with an embedded zero byte before the common name, and one carrying a
directoryName too short for the tag scan.
Fixes F-7223.
wolfSSL_dtls_set_pending_peer acquired the DTLS peer lock for reading and
then mutated the shared state under it. It frees and clears pendingPeer,
calls SockAddrSet which frees and reallocates the address buffer, and
writes processingPendingRecord. A read lock allows several holders at once,
so two threads in this function can both free pendingPeer.sa, and a reader
in wolfSSL_dtls_get_peer or the wolfio send path can observe a dangling
pointer.
Acquire the lock for writing instead, matching wolfSSL_dtls_set_peer, which
already does so for the same class of mutation. Only affects builds that
define WOLFSSL_RW_THREADED, since the locking is compiled out otherwise.
Fixes F-7222.
TimingPadVerify passes (pLen - macSz - padLen - 1) to ssl->hmac and relies
on the callee recovering the record length by modular addition. TLS_hmac now
does that addition with overflow checking and returns BUFFER_E before hashing
anything, so a record whose padding length byte exceeds pLen - macSz - 1 is
rejected without a MAC being computed at all, while a smaller padding byte
gets the full constant time HMAC. The padding length byte is taken straight
from the decrypted record, so this hands an attacker a Lucky13 style timing
oracle worth an entire HMAC.
Clamp the padding length in constant time before it is used, so the length
handed to ssl->hmac never wraps and every value of the padding length byte
results in the same amount of hashing. The rejection decision is unchanged,
since MaskPadding already flags an out of range padding length. The overflow
check in TLS_hmac stays as a backstop for genuinely bogus sizes.
Add a regression test that drives TimingPadVerify over every padding length
byte with a recording MAC callback and asserts the callback is always invoked
with a length that does not wrap.
Fixes F-7240.
wolfSSL_CertManagerEnableCRL, wolfSSL_CertManagerEnableOCSP,
wolfSSL_CertManagerEnableOCSPStapling and wolfSSL_d2i_X509_CRL called
FreeCRL() or FreeOCSP() on an object that InitCRL() or InitOCSP() had
just failed to initialize. The other callers of those two functions,
wolfSSL_X509_crl_new(), wolfSSL_X509_CRL_new(), SwapLists() and
wc_NewOCSP(), only dispose of the memory, which is the contract the
init functions are written to.
Freeing the object is not harmless. InitCRL() releases the read/write
lock and the condition variable itself before returning an error from
the reference count path, so FreeCRL() destroys both a second time, and
on the two earlier failure paths it destroys primitives that were never
created at all. InitOCSP() only fails when it cannot create its mutex,
which FreeOCSP() then destroys. Destroying a synchronization object
twice, or one that was never created, is undefined behaviour on every
platform and a real double free on the ports where the object holds a
handle to allocated storage, such as vSemaphoreDelete() on FreeRTOS and
CloseHandle() for Windows condition variables.
Dispose of only the memory in all four places. In the certificate
manager that also removes the reason to defer the disposal until after
caLock is released, so the extra local and the second free block go
with it.
Complete InitCRL()'s own cleanup while here: when wc_InitRwLock() fails
it returned without releasing the condition variable it had created
just above, leaking it for every caller.
The lazy creation paths in wolfSSL_CertManagerEnableCRL,
wolfSSL_CertManagerEnableOCSP and wolfSSL_CertManagerEnableOCSPStapling
stored the freshly allocated object in the shared certificate manager
before zeroing and initializing it. A certificate manager is shared by
every WOLFSSL created from a CTX, so another thread could observe the
non-NULL pointer and operate on uninitialized memory, for example by
taking crl->crlLock before InitCRL had created it.
Build each object in a local, initialize it, and store it in the
certificate manager only on success. The CRL lookup callback is set on
the local as well, so a thread that picks the object up cannot find it
without one and fall back to CRL_MISSING.
Serialize the creation with caLock and re-check the pointer after
locking, so that two concurrent Enable calls cannot both allocate and
leak one of the objects. This does not make every writer of the pointer
safe: wolfSSL_X509_STORE_add_crl() still publishes cm->crl with no lock,
and readers observe it without one.
Dispose of a half-built object after releasing caLock rather than under
it. Neither free can actually block here: InitCRL() sets tid to
INVALID_THREAD_VAL before any of its failure returns so FreeCRL() skips
the monitor join, and FreeOCSP() takes no lock at all. The point is to
keep the critical section down to the decision of what to publish, and
to keep caLock out of the CRL free path as a rule: FreeCRL() on a
published object joins the CRL monitor thread, which takes crlLock,
while the verification path already takes crlLock (CheckCertCRLList())
before caLock (GetCA()).
Fixes F-7235.
The OcspRequest carried a "void* ssl" back-pointer that the stapling
paths wrote just before handing the request to the OCSP layer. For the
request cached on the WOLFSSL_CTX that field is shared by every
connection using it, so concurrent handshakes raced on it. Drop the
field and pass the connection to CheckOcspRequest() and
CheckOcspResponse() as an argument instead, which is the only thing it
was ever read for.
Ownership of the cached request was equally implicit. Publication moves
out of CreateOcspRequest() into CreateOcspResponse(), and callers now
learn whether the CTX took ownership from a "ctxOwnsRequest" flag rather
than by comparing pointers against ssl->ctx->certOcspRequest, which was
read without the lock that guards it. The flag and the request are
handed back together on success and both left untouched on failure, so a
caller never decides ownership against a request it is not holding.
The cache is a field of the WOLFSSL_CTX, so serialize it with a lock
scoped to the CTX. SSL_CM(ssl) can resolve to a per-SSL cert manager
when WOLFSSL_LOCAL_X509_STORE is defined, which left two connections on
one CTX taking different locks for a check-then-set on the same pointer.
GetCtxOcspLock() keys off ssl->ctx->cm for both the reader and the
publisher, and a failure to take it is logged instead of silently
disabling the cache.
CheckOcspRequest() also loses its heap argument. It was only ever the
hint for the response buffer it hands back, which the caller frees
against the connection, so take it from the connection rather than from
a parameter every caller had to keep in step with its own free.
Smaller fixes in the same paths: zero the caller's response buffer
before the argument check can return, since SendCertificateStatus()
frees it without checking the return code; fold the ocsp_stapling NULL
check into the single early skip so the later uses need no guard;
gate the SetupOcspResp() free on success like the other two callers;
split the three differently owned requests in the
WOLFSSL_CSR2_OCSP_MULTI case into separate variables; and let that
case's allocation failures fall through to its shared cleanup instead of
returning, which leaked an already built leaf response.
Add test_ocsp_ctx_request_cache, which runs three handshakes over one
CTX pair and checks that the later ones reuse the cached request rather
than building another. The responder callback answers with a canned good
response, so stapling runs all the way through and the ownership
decision each connection makes is actually acted on: a connection that
freed the shared request shows up as a use after free on the next pass
and a double free at CTX teardown. The cached request is marked before
the last pass and the encoded request the callback sees is compared,
since a request rebuilt from the same certificate would otherwise be
identical byte for byte. The test is gated on !WOLFSSL_COPY_CERT:
OPENSSL_ALL implies it, and it gives every WOLFSSL its own certificate
copy, which takes the cache out of play. A new ocsp.yml job covers the
plain stapling build, an --enable-all build with the copy turned back
off, and an ASan build.
Also gate test_tls13_pha_status_request on KEEP_PEER_CERT. It checks the
received client certificate with wolfSSL_get_peer_certificate(), which is
only built when that macro is defined, so a post-handshake auth build with
stapling but without the OpenSSL compatibility layer failed to link
tests/unit.test.
Fixes F-7230 and F-7231.
RFC 8446 Section 4.4.2.1 deprecates the status_request_v2 extension for
TLS 1.3. The server side already avoided it; on the client side, reject
it in every message type but ClientHello once TLS 1.3 is negotiated, so
TLSX_CSR2_Parse() can no longer record it. ClientHello stays allowed
because the peer may still negotiate a lower version, where the
extension does apply.
Also align the pending signer registration in the chain verification
loop with the CA checks AddCA() performs on the normal path, so the same
conditions apply on both. Register the signer as WOLFSSL_CHAIN_CA rather
than CA_TYPE while doing so. TLSX_CSR2_MergePendingCA() promotes it into
the certificate manager, and wolfSSL_CertManagerUnloadIntermediateCerts()
selects entries by that type, so a chain CA learned over a
status_request_v2 multi handshake could never be unloaded again.
Add test_TLSX_CSR2_tls13_msg_type_validation, which feeds the extension
to TLSX_Parse() in the TLS 1.3 message types that must not carry it.
Fixes F-7227.
Cover wolfSSL_add1_chain_cert, which funnels through add0. Assert both the
chain count and the X509 reference bump so the caller's own free is proven
safe. Free the X509 when add0 fails instead of leaking it.
Place certChainCnt++ exactly as the missing_openssl_functionality branch
does. That branch carries the same fix, and with the two spellings apart
git merges both insertions without a conflict, incrementing twice.