When a subjectAltName otherName has an empty value, SetDNSEntry() called
XMEMCPY(dst, NULL, 0). Passing NULL to memcpy is undefined behavior and
aborts under UBSan. Skip the copy unless there is real data to copy.
Allow a quantum-safe-only TLS 1.3 stack - ML-KEM key exchange with ML-DSA or
SLH-DSA authentication - with RSA, ECC and DH all disabled (the CNSA 2.0 /
EO 14412 posture).
- Treat a usable standalone ML-KEM group as a valid key exchange and
Falcon/ML-DSA/SLH-DSA as valid authentication, so the "no cipher suites"
#errors no longer fire (ssl.c, internal.h), and extend the CopyDecodedToX509
pkCurveOID guard to match the struct member guards (internal.c).
- asn.c: guard the RSA/ECC-only certificate signing helpers so cert generation
compiles with RSA and ECC disabled, while keeping MakeSignatureCb available
for the WOLFSSL_CERT_SIGN_CB (TPM/HSM) offload path.
- tls.c: advertise the psk_dhe_ke exchange mode when a standalone ML-KEM group
is usable on the client side, that is when the build has both key generation
and decapsulation. Without it an ML-KEM-only build fell back to psk_ke, the
resumption key schedules diverged, and the first AEAD record failed to
authenticate (AES_GCM_AUTH_E), breaking session-ticket resumption.
- tls13.c: hoist the hashAlgo void cast in EncodeSigAlg out of the Ed25519 and
Ed448 cases so a build with no classic signature algorithm still marks the
parameter used.
- Examples and tests: add ML-DSA and SLH-DSA certificate selection (gated on
NO_RSA so it only applies in true PQC-only builds) and default to TLS 1.3
when no classic auth is built in and a post-quantum signature is available
(test.h, client/server/echo*, test_tls13.c). SLH-DSA uses the SHAKE-128s leaf
certificates and their shared 128s root.
- CI: add PQC-only entries to the pq-all.json config list (ML-DSA and SLH-DSA),
each exercising the build, a full TLS 1.3 handshake, and ML-KEM-only
session-ticket resumption.
Whether a build can authenticate with a post-quantum certificate is three
separate questions - is the algorithm compiled in, are test credentials wired
up for the enabled parameter set, and can this build sign - so test.h answers
them with three tiers of macro rather than testing WOLFSSL_HAVE_MLDSA and
WOLFSSL_HAVE_SLHDSA directly:
- TEST_HAVE_MLDSA_CERT_FILES / TEST_HAVE_SLHDSA_CERT_FILES: the credential
paths name real files. ML-DSA needs an enabled parameter level; SLH-DSA needs
a 128s parameter set, since both families share a 128s root and only those
certificates are wired up, and PEM decoding, since the entity certificates
ship as PEM only. Loading a certificate needs verification support alone, so
test_tls13.c uses this tier - it only needs a certificate on the context.
- TEST_HAVE_MLDSA_CERTS / TEST_HAVE_SLHDSA_CERTS: the above, plus the ability
to sign. A verify-only build derives WOLFSSL_MLDSA_NO_SIGN and drops private
key support entirely, so it can neither load the key nor produce a
CertificateVerify. The example credential ladders use this tier.
- TEST_HAVE_PQC_CERT_AUTH: either of those, plus WOLFSSL_TLS13. Both algorithms
are TLS 1.3 only, so folding the version into the macro keeps the version
defaults and the PSK fallbacks in the client, server and echo examples from
disagreeing. Falcon is deliberately absent, having no credentials in the
ladders, so a Falcon-only build keeps the historic TLS 1.2 PSK behavior.
A build that fails a gate now falls back to PSK instead of naming credentials
it cannot use, which removes two hard #errors that were reachable from
configure: --enable-slhdsa with only non-128s parameter sets broke every build
that includes test.h, even ones with RSA and ECC enabled that would never touch
an SLH-DSA credential.
Also in the credential material and the configurations this opens up:
- test.h: pair the DER ML-DSA certificate with mldsa<N>-key.der. The
mldsa<N>_priv-only.der files are standalone PKCS#8 encoding vectors from an
unrelated key pair, so a build without PEM decoding presented a certificate
and signed with a key that did not match it, and the peer rejected the
CertificateVerify.
- echoserver.c: load the SLH-DSA key with CERT_FILETYPE like every neighboring
branch, rather than hardcoding WOLFSSL_FILETYPE_PEM against a certificate
loaded in the build-dependent format.
- internal.c, tls13.c, ssl_load.c: mark ssl, sigOut, keyType and keySz used.
Every reader of these sits in a per-algorithm block, so a build with no
signing algorithm at all left them unused and -Werror rejected the library.
- tests/api.c: skip test_wolfSSL_PKCS8 unless RSA, ECC or PEM decoding is
available. With none of them every key-loading body compiles out, leaving the
file locals unused and nothing for the test to do.
The mp API variant of wc_PKCS12_PBKDF_ex builds a working buffer holding
D || S || P, but totalLen was computed as dLen + sLen and so left out the
password block entirely. The fill loop writes pLen bytes starting at
S + sLen, which is past the end of the buffer, and DoPKCS12Hash was called
with the short length so the password was never absorbed into the digest.
A build with this variant enabled therefore derived the same key for every
password over a given salt, and overflowed the heap allocation or the
static buffer by pLen bytes depending on the salt length.
Sum dLen with iLen instead, matching the non-mp variant. That corrects the
allocation size, the hash input length, the I update bound and the
ForceZero coverage in one place.
Fixes F-7306.
The quad decoding loop broke out on a NUL byte without clearing the
remaining input length, unlike the equivalent loops in Base64_Decode and
Base64_Decode_nonCT. Control then fell into the block that buffers the
leftover input in the context, which copied the full remaining byte count
into the 48 byte ctx->data with an unbounded index, and read one byte past
the end of the caller's buffer. An application decoding attacker supplied
base64 with an explicit, binary safe length could write attacker
controlled data past the end of a heap allocated encode context.
Clear the length before breaking, and bound the leftover copy loop at one
decode block, which is the most that loop can legitimately buffer. That
also keeps ctx->remaining below the block size, so a reused context cannot
underflow the copy size in the next update call or over-read ctx->data in
EVP_DecodeFinal.
Add a negative test feeding a NUL byte followed by more non-whitespace
data than the context buffer can hold.
Fixes F-7445.
wc_CryptoCb_CryptInitRenesasCmn() derived the slot index from a
free-running device id counter and wrote gCbCtx[] without checking the
index against MAX_FSPSM_CBINDEX. Since the function is called once per
WOLFSSL session, the sixth registration in the lifetime of a process
wrote a pointer past the end of the array.
Look for a free slot inside the hardware lock instead and derive the
device id from that slot, so a full table is rejected with
INVALID_DEVID. wc_CryptoCb_CleanupRenesasCmn() now clears the slot,
which makes it available to a later registration and drops the stale
pointer to a context the application may free. Also correct the upper
bound checks in Renesas_cmn_GetCbCtxBydevId() and
wc_CryptoCb_CleanupRenesasCmn(), which both accepted an index one past
the last element.
Fixes F-7438.
wc_LmsKey_Sign computes the signature with the one-time key at the
current leaf and only afterwards advances the leaf index and writes it
to non-volatile storage. When that write failed the signature was erased
but the key state was left as OK, so the same in-memory key could sign
again while storage still recorded the already consumed leaf. A restart
followed by wc_LmsKey_Reload would then resume from that leaf and sign a
second message with the same LM-OTS private key.
Set the key state to bad before signing and only restore it to OK once
the advanced private key has been committed to storage, matching what
wc_xmsskey_signupdate already does for XMSS. Exhausted keys keep their
existing NOSIGS state.
Fixes F-7411.
The NETOS implementation of wolfSSL_JoinThread released the buffer that
tx_thread_create had been given as the thread's execution stack without
first waiting for the thread to complete, and never unregistered the
thread from ThreadX. A join issued while the callback was still running
handed the running thread's stack back to the allocator.
Waiting was not possible with the previous THREAD_TYPE layout, because
it embedded the TX_THREAD control block by value and wolfSSL_JoinThread
takes THREAD_TYPE by value, so the join only ever saw a stale copy of
the control block. Store a pointer to a heap allocated TX_THREAD
instead, the way the Zephyr port already does, so the join operates on
the block ThreadX is scheduling.
wolfSSL_JoinThread now polls tx_thread_info_get until the thread reports
TX_COMPLETED or TX_TERMINATED, calls tx_thread_delete and checks its
result, and only then frees the stack and the control block.
Fixes F-7413.
The DigestInfo length extraction in wc_SignatureVerifyHash was compiled
only when WOLFSSL_RSA_PUBLIC_ONLY was not defined, but the block depends
on ASN.1 support, not on private key support. In a public only build the
RSA verify path stays reachable and wc_SignatureVerify still DER encodes
the digest, so the length check fell through to the plain digest size
comparison and rejected every valid signature with BAD_LENGTH_E. The
same guard also broke compilation with NO_ASN and RSA enabled, since
asn.h is not included in that case and GetSequence and GetOctetString
are undeclared.
Guard the block on NO_ASN instead, matching the DER encode call site in
wc_SignatureVerify.
Fixes F-7412.
Added documentation of the defines for algorithms and assembly code and math.
user_settings_embedded.h has configuration defines at the top that are set to
0 for off and 1 for on. Setting these turns on the appropriate defines.
Curve448/Ed448 build with the CURVE448_SMALL / ED448_SMALL byte-array
field backend (the default fe_448 backend needs __uint128_t for the
sc448 mod-order arithmetic, which the C28x toolchain lacks). The SMALL
fe448 carry-stores wrote each limb through a (word8) cast that does not
truncate to an octet when a C byte is wider than 8 bits, so the next
carry re-read saw a corrupted limb; mask each carry-store with WC_OCTET
(a no-op on the usual 8-bit-byte targets).
d2i_make_pkey() replaces the key data, size and type of a caller-supplied
WOLFSSL_EVP_PKEY, but left pkcs8HeaderSz, mldsaOID, pkey_curve and
save_type describing the key the object held before.
A stale pkcs8HeaderSz is the damaging one. pkcs8_encode() and
wolfssl_i_evp_pkey_get_der() both encode from pkey.ptr + pkcs8HeaderSz,
so after
d2i_PrivateKey(EVP_PKEY_RSA, NULL, &p, pkcs8RsaDer);
d2i_PrivateKey_EVP(&pkey, &q, traditionalEccDer);
PEM_write_bio_PKCS8PrivateKey() reports success while wrapping the ECC
key with its first 26 bytes cut off, and the resulting PEM cannot be
read back.
Only the d2i_PUBKEY and d2i_PrivateKey_EVP routes are affected;
d2i_PrivateKey and d2i_AutoPrivateKey go through d2i_evp_pkey(), which
allocates a fresh object and recomputes the header size.
The same branch also drops the data and the key object of the previous
key without releasing either. pkey.ptr is overwritten with a fresh
allocation, and wolfSSL_EVP_PKEY_free() only disposes of the object
matching the type currently set, so the object of a key whose type has
since changed is never freed. The sequence above leaks the 1219 byte RSA
encoding together with the WOLFSSL_RSA and its bignums, 13 allocations
in all. The data is released after the new encoding has been copied in,
since the caller may be decoding out of it.
Clear the metadata and dispose of the previous key on the reuse branch,
so a reused object decodes to the same state as a new one, and add a
regression test comparing the PKCS#8 output of a reused key against a
freshly decoded one.
This is hardening rather than a fix for a reachable defect. Both branches
below are wrong as written, but no entry point tested reaches them:
wolfSSL_d2i_PrivateKey() and wolfSSL_d2i_PUBKEY() both leave pkey->ecc
populated, so wolfSSL_EVP_PKEY_get1_EC_KEY() always takes its up_ref path.
wolfSSL_EVP_PKEY_get1_EC_KEY() has a branch that builds an EC_KEY when the
pkey does not carry one, caches it on the pkey and returns it. It did that
without taking a second reference and without setting ownEcc, so the
single reference the key was created with was handed to the caller while
the pkey kept an unowned pointer to it. A caller releasing what get1 gave
it, as the contract requires, would leave pkey->ecc dangling. The pkey now
keeps the reference the key was created with and the caller gets one of
its own.
The same branch freed the key when neither DER load succeeded but left
pkey->ecc pointing at it. That pointer is now cleared.
wolfSSL_EVP_PKEY_keygen() set ownEcc on the EC path whether or not it had
created the key, so a key placed on the pkey by something that did not
transfer ownership would gain a second owner. Ownership is now claimed
where the key is created.
Adds test_wolfSSL_EVP_PKEY_get1_EC_KEY_reuse(), which releases the
reference get1 returns and then calls get1 again. It covers the path a
decoded pkey actually takes and pins the reference contract; it passes
with and without the change above, which the comment on the test says
plainly so it is not mistaken for a regression test.
The DH case of wolfSSL_EVP_PKEY_keygen() assigned straight over
pkey->dh:
case WC_EVP_PKEY_DH:
pkey->dh = wolfSSL_DH_new();
A caller supplied EVP_PKEY can already hold a DH object.
wolfSSL_EVP_PKEY_set1_DH() takes a reference and sets ownDh, and
wolfSSL_EVP_PKEY_assign_DH() installs one outright, so keygen on such a
pkey dropped the only pointer the EVP_PKEY had to that object without
releasing its reference, and nothing freed it afterwards.
The case now generates into a temporary and frees the previous key when
the pkey owned it, which is the shape the RSA case in the same switch
uses.
Adds test_wolfSSL_EVP_PKEY_keygen_dh_reuse(), which loads DH parameters,
puts them on an EVP_PKEY with set1_DH so the pkey holds a reference, and
then runs keygen on that same pkey. The leak itself is not asserted by the
test: it needs an allocation tracker, and the smoke-test sanitize-asan job
provides one, since it builds with AddressSanitizer and sets no
ASAN_OPTIONS, so LeakSanitizer runs by default there. What the test does
locally is drive the path and show it stays free of double frees under
AddressSanitizer.
Under WOLFSSL_NO_REALLOC, PopulateRSAEvpPkeyDer() and
ECC_populate_EVP_PKEY() emulate XREALLOC by allocating a buffer sized for
the NEW encoding and then copying pkey_sz bytes, the size of the OLD one,
into it:
derBuf = (byte*)XMALLOC((size_t)derSz, pkey->heap, DYNAMIC_TYPE_DER);
if (derBuf != NULL) {
XMEMCPY(derBuf, pkey->pkey.ptr, (size_t)pkey->pkey_sz);
Whenever the replacement key encodes shorter than the one already on the
EVP_PKEY the copy runs past the end of the new allocation. Putting a
public key on a pkey holding a 2048-bit private key copies 1192 bytes
into a 294 byte buffer.
The copy serves no purpose: both functions fill the new buffer with a
fresh encoding immediately afterwards. It is removed rather than bounded.
ECC_populate_EVP_PKEY() also gains the pkey_sz reset that
PopulateRSAEvpPkeyDer() already has, so a failure between the allocation
and the encoding cannot leave the size describing a buffer that holds no
encoding.
The outgoing buffer is now wiped with ForceZero() before it is
reallocated or freed. On a private key it holds a complete RSA or ECC DER,
so returning it to the allocator intact leaves the key recoverable from
the free pool through a later heap over-read, a core dump or a swap page.
wolfSSL_RSA_To_Der_ex() establishes the same convention two frames away.
wolfSSL_EVP_PKEY_free() gets the same treatment, since it releases that
buffer on every normal teardown, as does the PKCS#8 branch of
PopulateRSAEvpPkeyDer(), which frees the unwrapped PKCS#1 key on its
success path once the wrapped copy has been built.
In ECC_populate_EVP_PKEY() that covers all three sites which release the
previous encoding, the two private-key branches as well as the public
one. clearEVPPkeyKeys() leaves pkey.ptr in place, so a pkey decoded from
a private key still carries that DER when a public-only key replaces it.
The wipe there happens before the allocation, since XREALLOC consumes the
old pointer, and pkey_sz and pkcs8HeaderSz are dropped with the contents
so a failed allocation cannot leave either describing a buffer that no
longer holds an encoding.
Where the allocation of the new buffer fails, pkcs8HeaderSz is cleared
along with pkey_sz for the reason given in the previous commit.
ECC_populate_EVP_PKEY() clears pkcs8HeaderSz when it installs a public
key. A SubjectPublicKeyInfo has no PKCS#8 wrapper, but neither
wolfSSL_EVP_PKEY_set1_EC_KEY() nor clearEVPPkeyKeys() resets the field, so
putting a public key on a pkey decoded from a PKCS#8 EC key left the
export paths starting that many bytes inside the new encoding and
returning it short under a success return.
The traditional private-key branch needs the same reset. It runs whenever
the incoming EC key carries no header size of its own, a generated key for
instance, and writes a bare SEC1 ECPrivateKey. Seeding an EVP_PKEY from
certs/ecc-keyPkcs8.der and then calling wolfSSL_EVP_PKEY_set1_EC_KEY()
with a generated key made wolfSSL_i2d_PrivateKey() return 92 bytes
beginning in the middle of the private scalar rather than the 121 byte
encoding. Every export path is affected, including the PKCS#8 encryption
in wolfSSL_PEM_write_bio_PKCS8PrivateKey(), which encrypts that same
misaligned slice.
Adds test_wolfSSL_EVP_PKEY_set1_shrinking_der(), which replaces the key
on an EVP_PKEY with a public-only one for both RSA and ECC and requires
the stored encoding to shrink. The smoke-test job
opensslextra-norealloc-asan builds exactly this configuration under
AddressSanitizer, which is where the over-copy is caught.
The test gates each algorithm on its own prerequisites rather than on one
shared list. WOLFSSL_KEY_TO_DER is defined by settings.h only when RSA is
enabled, so requiring it for the whole test compiled the ECC half out of
any build without RSA, and that half is the only coverage the ECC
over-copy has. The ECC half is seeded from a PKCS#8 wrapped key so that
pkcs8HeaderSz starts non-zero, and its size assertion is exact rather than
a comparison against the previous size, so an export starting at a stale
header shows up as a mismatch rather than passing.
test_wolfSSL_EVP_PKEY_set1_EC_KEY_no_pkcs8() covers the private-key case.
It compares the encoding exported after the replacement against the one a
pkey that never held a wrapped key produces from the same EC key, so a
carried over header size shows up as a size and content mismatch.