Support post-quantum-only (ML-KEM + ML-DSA/SLH-DSA) TLS 1.3 builds - #11096
Support post-quantum-only (ML-KEM + ML-DSA/SLH-DSA) TLS 1.3 builds#11096Frauschi wants to merge 2 commits into
Conversation
|
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #11096
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #11096
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 5
5 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
SendTls13Certificate keeps its chain walk cursor in the function locals len, idx, offset and p, but the only state that survives the WANT_WRITE return of a non-blocking send is ssl->fragOffset, and that is consulted for the leaf certificate alone. A send that blocked part way through the chain therefore re-primed the walk on the next call and copied the chain from its first byte again. The byte count still matched the announced payload size, so the message stayed well formed on the wire while the tail of the chain was replaced by a repeat of its head, and the peer rejected it with BUFFER_ERROR. Rebuild the cursor from ssl->fragOffset when a resume lands inside the chain. NextCert reads each entry's three byte length prefix and skips it, so passing over the entries already sent costs one hop per certificate and only happens on a resume. Guard the extension index bump the same way the send loop does, so builds without certificate status request keep the leaf extension size. Track the size of the chain entry being written in its own variable rather than folding the extension size into len once the entry completes. The send loop detected completion with offset == len + OPAQUE16_LEN and kept that check honest by adding extSz[extIdx] - OPAQUE16_LEN to len at the end of an entry, so until then len held the raw certificate length and the check read as complete whenever a fragment boundary landed exactly OPAQUE16_LEN bytes into a real extension. The walk then jumped to the next certificate in the middle of the current one. entrySz records len + extSz[extIdx] when the entry is picked up, len keeps the raw certificate length AddCertExt expects, and both the resume and the ordinary multi fragment path test the same condition. The stapled chain in scripts/ocsp-stapling_tls13multi.test reproduces the entry size case with the server records held to 1482 bytes: the boundary falls two bytes into an 1837 byte OCSP extension and the handshake fails, while 1480, 1481, 1483 and 1484 all pass. Reaching this needs a certificate message larger than one record, which is why it stayed dormant with classic certificates. Add SLH-DSA scenarios with simulated WANT_WRITE, for server and for mutual authentication, to tests/test-tls13-slhdsa-entity-128s.conf. The same resume path mishandles the stapled OCSP responses. WriteCSRToBuffer fills extSz[] only for the entries whose buffer it allocates, so on a resumed call every entry that still held a buffer, the one being written and all that follow it, kept the OPAQUE16_LEN default of an empty extension. The message length, the entry sizes and the extension bytes written for those entries were all derived from that default. Recover the size from the extension length already written into the buffer instead. SetupOcspResp appends a fresh request per certificate on every call, so a message that resumed often enough exhausted the extension array and the handshake ended with MAX_CERT_EXTENSIONS_ERR. Look the responses up once, when the message starts, and reuse them for the rest of it. A resumed call also reallocates the extension buffers of the entries it has already sent, and the walk passes over those entries without writing them again, so free them there. Free the array in wolfSSL_ResourceFree as well: nothing released it when a connection ended part way through a Certificate message, which leaked one OCSP response per unsent entry. Test case 8 of scripts/ocsp-stapling_tls13multi.test covers all three. A maximum fragment length of 512 bytes splits the stapled message over about twenty records and the server blocks on every one of them; without these fixes the handshake fails with MAX_CERT_EXTENSIONS_ERR.
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.
| c16toa(extSz[extIdx] - OPAQUE16_LEN, | ||
| der->buffer); | ||
| } | ||
| else if (certExts[extIdx] != NULL) { |
There was a problem hiding this comment.
WriteCSRToBuffer infers "resumed send" from a non-NULL certExts[] pointer, which is also true for a buffer left over from a completed message
Consider using ssl->fragOffset != 0 instead.
Recommendation: Key the branch off ssl->fragOffset != 0, which is the actual resume signal and is already what gates SetupOcspResp at src/tls13.c:9970. Separately, relax the resume block's condition at src/tls13.c:10024 from > to >= so certExts[0] is always released once the cursor is past the leaf; I traced that change and it is safe (with chainPos == 0 the while loop does not execute, and idx/offset/entrySz/extIdx all stay at the values the send loop needs).
| && !defined(HAVE_CURVE25519) && !defined(HAVE_CURVE448) | ||
| && !defined(HAVE_CURVE25519) && !defined(HAVE_CURVE448) \ | ||
| && (!defined(WOLFSSL_HAVE_MLKEM) || \ | ||
| defined(WOLFSSL_TLS_NO_MLKEM_STANDALONE)) |
There was a problem hiding this comment.
"No cipher suites defined" #error escape for ML-KEM does not require TLS 1.3
Recommendation: Add a !defined(WOLFSSL_TLS13) || term to the ML-KEM escape so the diagnostic still fires for a TLS 1.2-only build. Consider also gating configure.ac's standalone auto-enable on ENABLED_TLS13 — it currently prints "auto-enabling standalone ML-KEM for TLS 1.3" even when --disable-tls13 was given.
| !defined(HAVE_ED25519) && !defined(HAVE_ED448) | ||
| !defined(HAVE_ED25519) && !defined(HAVE_ED448) && \ | ||
| !defined(HAVE_FALCON) && !defined(WOLFSSL_HAVE_MLDSA) && \ | ||
| !defined(WOLFSSL_HAVE_SLHDSA) |
There was a problem hiding this comment.
"No cipher suites available" #error accepts Falcon/ML-DSA/SLH-DSA auth without requiring TLS 1.3
Recommendation: Gate the three post-quantum terms on WOLFSSL_TLS13 so a TLS 1.2-only build with only PQC signatures still gets the compile-time diagnostic.
|
|
||
| /* The same credentials, plus the ability to sign: a verify-only build can | ||
| * neither load the key nor produce a CertificateVerify. */ | ||
| #if defined(TEST_HAVE_MLDSA_CERT_FILES) && \ |
There was a problem hiding this comment.
Add && !defined(WOLFSSL_MLDSA_NO_VERIFY) — the examples need both halves, since they sign their own CertificateVerify and verify the peer's chain. Reproduce with ./configure --disable-rsa --disable-ecc --disable-dh --enable-mlkem --enable-tls-mlkem-standalone --enable-psk --enable-mldsa=44,make,sign.
| -p "$port5" -v 4 -6 & | ||
| server_pid5=$! | ||
| wait_for_readyFile "$ready_file5" "$server_pid5" "$port5" | ||
| ./examples/client/client -C -A certs/ocsp/root-ca-cert.pem -W 1 -v 4 \ |
There was a problem hiding this comment.
New OCSP fragmented-send test silently no-ops in builds without HAVE_MAX_FRAGMENT
Recommendation: Detect HAVE_MAX_FRAGMENT with the script's existing client -? grep idiom and skip the case loudly rather than running a no-op that reports PASSED. The comment "Both options are ignored by builds that do not support them" documents the hole but does not make the coverage loss visible in CI output.
| && !defined(HAVE_CURVE25519) && !defined(HAVE_CURVE448) | ||
| && !defined(HAVE_CURVE25519) && !defined(HAVE_CURVE448) \ | ||
| && (!defined(WOLFSSL_HAVE_MLKEM) || \ | ||
| defined(WOLFSSL_TLS_NO_MLKEM_STANDALONE)) |
There was a problem hiding this comment.
Consider moving tls.c:4677 into internal.h or settings.h and using that gate.
/* src/tls.c:4677 */
#if defined(WOLFSSL_HAVE_MLKEM) && !defined(WOLFSSL_MLKEM_NO_MAKE_KEY) && \
!defined(WOLFSSL_MLKEM_NO_DECAPSULATE)
#define WOLFSSL_HAVE_MLKEM_CLIENT_SUPPORT
#endif
Description
This PR adds support for 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. That is the CNSA 2.0 / EO 14412 posture, and until now such a configure line did not build: the "no cipher suites"
#errorfired before anything else, and several files failed-Werroronce every classic algorithm was compiled out. This is based on initial patches published in our blog.It also fixes a latent TLS 1.3 bug that only shows up once certificates get large enough to span multiple TLS records, which is exactly what post-quantum certificates do.
Library changes
src/ssl.c,wolfssl/internal.h). Treat a usable standalone ML-KEM group as a valid key exchange, and Falcon/ML-DSA/SLH-DSA as valid authentication, so the two "no cipher suites"#errors no longer fire in a PQC-only build.src/tls.c. Advertise thepsk_dhe_keexchange mode when a usable standalone ML-KEM group is available. Without it an ML-KEM-only build fell back topsk_ke, the resumption key schedules diverged, and the first AEAD record failed to authenticate (AES_GCM_AUTH_E), which broke session-ticket resumption.src/internal.c. Extend theCopyDecodedToX509pkCurveOIDguard so it matches the struct member guards.wolfcrypt/src/asn.c. Guard the RSA/ECC-only certificate signing helpers so certificate generation still compiles with RSA and ECC disabled, while keepingMakeSignatureCbavailable for theWOLFSSL_CERT_SIGN_CB(TPM/HSM) offload path.src/tls13.c. Hoist thehashAlgovoid cast inEncodeSigAlgout of the Ed25519 and Ed448 cases so a build with no classic signature algorithm still marks the parameter used.src/internal.c,src/tls13.c,src/ssl_load.c. Markssl,sigOut,keyTypeandkeySzused. Every reader of these sits inside a per-algorithm block, so a build with no signing algorithm at all left them unused and-Werrorrejected the library.Certificate chain send fix (
src/tls13.c)SendTls13Certificatekeeps its chain walk cursor in the function localslen,idx,offsetandp, but the only state that survives theWANT_WRITEreturn of a non-blocking send isssl->fragOffset, and that is consulted for the leaf certificate alone. A send that blocked part way through the chain therefore re-primed the walk on the next call and copied the chain from its first byte again. The byte count still matched the announced payload size, so the message stayed well formed on the wire while the tail of the chain was replaced by a repeat of its head, and the peer rejected it withBUFFER_ERROR.The fix rebuilds the cursor from
ssl->fragOffsetwhen a resume lands inside the chain.NextCertreads each entry's three byte length prefix and skips it, so passing over the entries already sent costs one hop per certificate and only happens on a resume. No new per-connection state is added, which keeps the embedded footprint unchanged.This is not a regression from this branch.
git log -S "offset = OPAQUE16_LEN" -- src/tls13.cdates the code to2b1e9973e(2016). Reaching it needs a Certificate message larger than one record on a non-blocking socket, and classic chains stay under one record, which is why it lay dormant. CI never caught it either: the SLH-DSA test configurations carry no-6cases, and every other SLH-DSA configuration keeps RSA and ECC on, so the-6cases intests/test-tls13.confrun with small RSA certificates. The newslhdsa-pqc-onlyconfiguration is the first to combine large certificates with a non-blocking send.It is kept as a separate commit so it can be reviewed apart from the post-quantum feature work.
Example and test credentials (
wolfssl/test.h,examples/,tests/)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.
test.hnow answers them with three tiers of macro rather than testingWOLFSSL_HAVE_MLDSAandWOLFSSL_HAVE_SLHDSAdirectly.TEST_HAVE_MLDSA_CERT_FILES/TEST_HAVE_SLHDSA_CERT_FILEStest_tls13.cuses this tier.TEST_HAVE_MLDSA_CERTS/TEST_HAVE_SLHDSA_CERTSWOLFSSL_MLDSA_NO_SIGNand 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_AUTHWOLFSSL_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. That removes two hard
#errors that were reachable straight from configure:--enable-slhdsawith only non-128s parameter sets broke every build that includestest.h, even ones with RSA and ECC enabled that would never touch an SLH-DSA credential.Two credential bugs fixed along the way:
test.h. Pair the DER ML-DSA certificate withmldsa<N>-key.der. Themldsa<N>_priv-only.derfiles 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.examples/echoserver/echoserver.c. Load the SLH-DSA key withCERT_FILETYPElike every neighboring branch, rather than hardcodingWOLFSSL_FILETYPE_PEMagainst a certificate loaded in the build-dependent format.tests/api.cskipstest_wolfSSL_PKCS8unless 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.Testing
New CI entries in
.github/configs/pq-all.json, one per signature algorithm:Each exercises the build, a full TLS 1.3 handshake, and ML-KEM-only session-ticket resumption.
tests/test-tls13-slhdsa-entity-128s.confgains two scenarios that drive the certificate chain send under simulatedWANT_WRITE(-6), one server-auth and one mutual-auth, so the resume path is covered in both directions. Without thesrc/tls13.chunk./tests/unit.testexits 1 at the new case; with it, exit 0.