Add SLH-DSA support for the TLS 1.3 and DTLS 1.3 handshake#10901
Add SLH-DSA support for the TLS 1.3 and DTLS 1.3 handshake#10901Frauschi wants to merge 1 commit into
Conversation
|
cf4f9cd to
417c06f
Compare
|
Jenkins retest this please |
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10901
Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-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.
417c06f to
e7100ab
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10901
Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 3
2 finding(s) posted as inline comments (see file-level comments below)
Info (1)
SHAKE-variant SLH-DSA cases in EncodeSigAlg lack per-parameter-set guards
Function: EncodeSigAlg
Category: Code Consistency / Defensive Programming
The SHA2-family SLH-DSA cases in EncodeSigAlg are individually gated (e.g. #ifdef WOLFSSL_SLHDSA_PARAM_SHA2_128S) whereas the SHAKE-family cases fall through under the single #ifdef WOLFSSL_HAVE_SLHDSA umbrella. This is an internal inconsistency: if WOLFSSL_HAVE_SLHDSA is defined but a specific SHAKE parameter set (e.g. WOLFSSL_SLHDSA_PARAM_128F) is not, the corresponding case statement is still compiled in even though it can never be reached. AddSuiteHashSigAlgo, which feeds the negotiated suite, is correctly per-variant gated, so the enum value would never appear in args->sigAlgo. Because AddSuiteHashSigAlgo, SlhDsaSigMinorToType, and ProcessBufferTryDecodeSlhDsa all enforce per-parameter-set guards at the point where an algorithm is admitted into the session, a SHAKE variant that is not compiled in cannot appear in a live handshake.
Recommendation: For consistency with the SHA2 branch and to make dead-code elimination explicit, add per-parameter-set preprocessor guards around each SHAKE case in EncodeSigAlg. No functional or security change results; the improvement is defensive consistency.
This review was generated automatically by Fenrir. Findings are non-blocking.
e7100ab to
ac897d5
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10901
Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
ac897d5 to
f759960
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10901
Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
f759960 to
9e96534
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10901
Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
ac897d5 to
8bdb3d0
Compare
8bdb3d0 to
728d60e
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10901
Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
a2e3d4f to
6d1867a
Compare
|
The one CI failure (Multiple compilers and versions / Compiler test) will be resolved by #10951 . |
fd59398 to
0977af6
Compare
|
Jenkins retest this please |
0977af6 to
848d32f
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10901
Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-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.
848d32f to
f4fe428
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10901
Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 4
2 finding(s) posted as inline comments (see file-level comments below)
Low (1)
SLH-DSA alternative-signature not verified in WOLFSSL_CKS_SIGSPEC_BOTH mode
File: src/tls13.c:12305-12415
Function: DoTls13CertificateVerify
Category: Missing Authentication Step
When WOLFSSL_DUAL_ALG_CERTS is enabled and the negotiated CKS extension value is WOLFSSL_CKS_SIGSPEC_BOTH, the code at DoTls13CertificateVerify TLS_ASYNC_DO decodes the alternative SLH-DSA key via decodeSlhDsaKey (tls13.c:11807-11821) into ssl->peerSlhDsaKey. However, the subsequent alternative-signature verification block (tls13.c:12305-12415) handles RSA, ECC, and ML-DSA alternative signatures but contains no WOLFSSL_HAVE_SLHDSA case for verifying the alternative SLH-DSA signature. Consequently args->altPeerAuthGood is never set to 1. At TLS_ASYNC_FINALIZE (tls13.c:12466-12470), ssl->options.peerAuthGood is overwritten with args->altPeerAuthGood (0), so the handshake is rejected. The security outcome is conservative: authentication fails rather than being bypassed. However, any legitimate dual-algorithm session where SLH-DSA is the alternative key and SIGSPEC_BOTH is negotiated will always be rejected by a wolfSSL endpoint compiled with WOLFSSL_DUAL_ALG_CERTS, rendering that configuration non-functional. The primary (native) signature IS correctly verified and is what prevents any authentication bypass; the missing alt-sig verify step merely means the required second check never passes.
Recommendation: Add a WOLFSSL_HAVE_SLHDSA case inside the WOLFSSL_DUAL_ALG_CERTS alternative-signature verification block in DoTls13CertificateVerify (around tls13.c:12409, after the ML-DSA block). The case should:
- Call
wc_SlhDsaKey_Verifywithsigadvanced past the primary signature (as done at line 12309) - Pass
args->altSignatureSz,args->altSigData/altSigDataSz - Set
args->altPeerAuthGood=1on success - Free
peerSlhDsaKeyappropriately - Add a parameter-set consistency check analogous to tls13.c:12273-12276
Alternatively, if SLH-DSA as an alternative key is intentionally unsupported, the decode switch at tls13.c:11807-11821 should return ALGO_ID_E for all SLH-DSA cases to fail fast rather than allocating a key that is never verified.
Referenced code: src/tls13.c:12305-12317 (13 lines)
Info (1)
decodeSlhDsaKey sets peerSlhDsaKeyPresent before wc_SlhDsaKey_Init succeeds
Function: decodeSlhDsaKey
Category: Key Lifecycle / Resource Management
In decodeSlhDsaKey, ssl->peerSlhDsaKeyPresent is set to 1 (line 11572) before the re-initialization sequence: wc_SlhDsaKey_Free (line 11576) then wc_SlhDsaKey_Init (line 11577). If wc_SlhDsaKey_Init fails, the function returns PEER_KEY_ERROR with peerSlhDsaKeyPresent=1 but the key's params field is NULL (set by wc_SlhDsaKey_Free at wc_slhdsa.c:6880). Subsequent cleanup paths (exit_dcv → wolfSSL_ResourceFree → FreeKey → wc_SlhDsaKey_Free → XFREE) are safe: wc_SlhDsaKey_Free guards all hash-object frees with if (key->params != NULL), making it a no-op on the NULL-params key before XFREE frees the struct. No double-free, no use-after-free, no memory leak results. Noted here because the ordering (flag set before Init completes) could confuse future maintenance.
Recommendation: Move ssl->peerSlhDsaKeyPresent = 1 to after the successful wc_SlhDsaKey_Init call (i.e., after the if (keyRet != 0) return PEER_KEY_ERROR check at line 11580). This makes the invariant explicit: the Present flag is true only when the key is fully initialized. This pattern is already used correctly in the analogous decodeMlDsaKey and decodeFalconKey functions.
This review was generated automatically by Fenrir. Findings are non-blocking.
f4fe428 to
3e7da8e
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10901
Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
3e7da8e to
913ea97
Compare
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). 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. - 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: - Make MAX_X509_SIZE parameter-aware for post-quantum certificates, sizing it from the largest enabled SLH-DSA or ML-DSA signature rather than a flat 8 KB that cannot hold an SLH-DSA-signed certificate. - Size the CertificateVerify and PreTBS 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. 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. 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. - SLH-DSA CertificateVerify: preserve the verify return code on failure 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 the ML-DSA/Falcon verify blocks. - CI: add a SHA2-only SLH-DSA build (--enable-slhdsa=sha2) to pq-all.yml so the SHAKE-disabled combined-maxima guards are exercised - a SHAKE-off build must still size the FORS/XMSS buffers for the 192/256-bit SHA2 parameters. - Tests: add an API-level memio test (test_slhdsa_tls13_certverify_want_write) that forces a WANT_WRITE partway through a fragmented SLH-DSA-SHAKE-128f CertificateVerify and asserts the handshake still completes, covering the streamed-CertificateVerify resume branch (fragOffset != 0 re-entry) that the blocking .conf handshakes never exercise.
913ea97 to
7ed7f80
Compare
|
Jenkins retest this please |
Summary
Adds 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. An SLH-DSA leaf key can now be used to sign (and a peer's SLH-DSA leaf to verify) theCertificateVerifymessage, for both server and mutual authentication.All twelve parameter sets are wired into the handshake — the SHAKE and SHA2 hash families across 128/192/256 in both the f (fast) and s (small) variants — with per-parameter-set gating so a build only offers, accepts, and maps the variants actually compiled in. Test certificates and configs cover the 128f and 128s sets for both hash families.
Motivation
SLH-DSA is a stateless hash-based signature scheme standardized in FIPS 205. Its appeal is a conservative security foundation (hash functions only), but its signatures are large — from ~7.8 KB (128s) up to ~49 KB (256f) — which stresses several fixed-size buffers in the TLS/cert code that were sized for classical signatures. This PR adds the protocol support and the buffer-sizing work needed to make it viable, including on memory-constrained targets.
What's included
Handshake integration
signature_algorithmsextension andCertificateVerify.CertificateVerifywith an SLH-DSA entity key.ssl_load.c,ssl.c,ssl_api_pk.c,asn.c,wc_slhdsa.h).--enable-slhdsa=sha2-128s) never advertise or map a variant they cannot perform.Streamed CertificateVerify send (large signatures)
When the
CertificateVerifybody exceeds a single record, the signature is generated once into a connection-level buffer and emitted one record at a time, so the output buffer never has to hold the whole signature.WANT_WRITEwithout recomputing the randomized signature (the already-sent records are bound into the transcript, so the resumed send must emit the identical bytes — this is a correctness requirement, not an optimization).WOLFSSL_TLS13_STREAM_CERT_VERIFY(TLS 1.3, non-async, PQC signatures). It is algorithm-neutral — it also applies to ML-DSA (or any scheme) whenever the body exceeds the negotiatedmax_fragment_length.WOLFSSL_ASYNC_CRYPTbuilds keep the existing in-place fragmented path (see Scope / carve-outs).WOLFSSL_DUAL_ALG_CERTS, BOTH) bodies stream as well: the combined two-signature body is sized from per-signature upper bounds and the exact length is recorded after signing (trailing slack is never sent).Peak send-side memory is ≈ one signature + one record (the record is at most 16 KB, or the negotiated
max_fragment_lengthif smaller), versus the ~2× a naive in-place send holds (the assembled body plus a copy). The reduction is largest exactly where it matters for constrained devices — a smallmax_fragment_length, where the second term is sub-KB and the peak is essentially the signature itself. It also stops the output buffer from staying inflated to the whole signature for the connection's lifetime.Buffer sizing
MAX_X509_SIZEis now parameter-aware for post-quantum certificates, sized from the largest enabled SLH-DSA/ML-DSA signature. The previous flat 8 KB could not hold an SLH-DSA-signed certificate (~8.5 KB), which silently broke the static session-certificate cache paths.CertificateVerifyand PreTBS buffers are sized from the actual signature length instead of the worst-caseWC_MAX_CERT_VERIFY_SZ(which balloons to ~50 KB with SLH-DSA).WC_MAX_CERT_VERIFY_SZis retained for API/backward compatibility and documented as no longer used internally.Tests and certificates
CertificateVerifytest configs covering the fragmented (128f) and single-record (128s) send paths for both hash families, wired intosuites.c(server-auth + mutual-auth scenarios).Enabling
TLS 1.3 is required (enabled by default). The SHA2 family additionally requires SHA-256, SHA-512, and HMAC.
Scope / carve-outs
Dtls13HandshakeSend, and its reliability model must retain the full message for retransmission until ACKed — so streaming yields no memory benefit there. A code comment inDtls13SendFragmentedrecords the follow-up (sign directly into the fragments buffer to avoid the transient second copy; the retransmit copy is a hard floor).WOLFSSL_ASYNC_CRYPTbuilds keep the existing in-place fragmented path. Async introduces a record-AEADWC_PENDING_Eresume point whose cursor-timing requirement conflicts with the non-blockingWANT_WRITEcursor; the in-place path already handles both correctly. Async targets (HW-offload servers) are not memory-constrained, so there is no beneficiary for the added complexity.Testing
./tests/unit.testsuite: pass (normal handshakes + all SLH-DSA configs + DTLS + crypto API tests).-Werror: clean; verified the streaming feature-macrooff path (classical/async builds) also compiles clean.
Known limitations / follow-ups
WOLFSSL_SLHDSA_PARAM_*macros) would strengthen the streaming/fragmentation coverage.WOLFSSL_DUAL_ALG_CERTS, BOTH) path with a fragment-sized signature is not covered by a test config (no dual-alg SLH-DSA test certificates exist); it is correct by construction but unexercised.