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.
Add tooling to produce Software Bills of Materials and build provenance
for wolfSSL, supporting EU Cyber Resilience Act (CRA) obligations.
SBOM generation:
- New `make sbom` target producing SPDX 2.3 output with NTIA minimum
elements, urn:uuid document namespaces, and SPDX LicenseRef compliance.
- Reproducible library discovery across autotools and CMake builds, with
liboqs recorded as a linked artefact.
- Standalone `scripts/gen-sbom` for embedded / RTOS / custom-builder
flows that do not use the main build system, plus --srcs-file,
--no-artifact-hash, and hash-source options.
Build provenance (OmniBOR / bomsh):
- End-to-end bomsh tracing of the built binaries with ArtifactID
insertion, snapshotting the traced library before libtool relink and
hashing the bomsh-traced binary.
- `scripts/bomsh_verify.py` to validate provenance against the traced
gitoid.
Security advisories:
- `scripts/gen-advisory` generating CSAF 2.0 and CycloneDX VEX, with a
`make` target, VEX overlay schema/example, and CWE name data.
Docs, tests, and CI:
- doc/SBOM.md and doc/CRA.md, plus README/INSTALL updates.
- Unit and regression tests for gen-sbom and gen-advisory.
- New sbom.yml and advisory.yml workflows: SPDX validation via
pyspdxtools, CSAF validation, bomsh provenance verification, SBOM
artifact archiving, macOS coverage, and actions pinned to SHAs.
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
Add SRAM PUF (Physically Unclonable Function) support to wolfCrypt. Derives device-unique cryptographic keys from the power-on state of SRAM memory using a BCH(127,64,t=10) fuzzy extractor with HKDF key derivation.
- **wolfCrypt PUF API** (`wolfcrypt/src/puf.c`, `wolfssl/wolfcrypt/puf.h`)
- `wc_PufInit`, `wc_PufReadSram`, `wc_PufEnroll`, `wc_PufReconstruct`
- `wc_PufDeriveKey` (HKDF-SHA256), `wc_PufGetIdentity` (SHA-256 device fingerprint)
- `wc_PufZeroize` (secure context cleanup)
- `wc_PufSetTestData` (synthetic SRAM for testing without hardware)
- **BCH(127,64,t=10) error-correcting codec** - corrects up to 10 bit flips per 127-bit codeword across 16 codewords
- **`WC_PUF_SHA3` build option** - select SHA3-256 instead of SHA-256 for identity hash and HKDF (default: SHA-256)
- **Precomputed GF(2^7) tables** - `const` arrays in `.rodata` (no runtime init, thread-safe, flash-resident on embedded)
- `./configure --enable-puf` (auto-enables HKDF dependency)
- CMake: `WOLFSSL_PUF=yes`
- `WOLFSSL_USER_SETTINGS`: define `WOLFSSL_PUF` and `WOLFSSL_PUF_SRAM`
- See wolfssl-examples/puf for example implementation on STM32 NUCLEO-H563ZI (Cortex-M33, STM32H563ZI)
- Supports test mode (synthetic SRAM)
- Builds to ~13KB `.elf`
- Tested on NUCLEO-H563ZI: enrollment, noisy reconstruction, key derivation all pass
- `.github/workflows/puf.yml`: host build + test workflow for PUF feature
- Doxygen API docs for all 8 public functions
- PUF group added to `doxygen_groups.h`
Add CryptoCB-based AES key import support to enable Secure Element
offload without exposing raw AES key material to wolfCrypt.
When WOLF_CRYPTO_CB_AES_SETKEY is defined, wolfCrypt invokes a CryptoCB
callback during AES key setup. Behavior is determined by the callback
return value:
- If callback returns 0: Key is imported to the device (aes->devCtx).
Key is NOT copied to wolfCrypt RAM; GCM H/M tables are NOT generated.
Full hardware offload is assumed.
- If callback returns CRYPTOCB_UNAVAILABLE: Device does not support
SetKey. Normal software path is used; key is copied to devKey for
optional encrypt/decrypt acceleration.
- Any other error: Propagated to the caller.
Key points:
- Add wc_CryptoCb_AesSetKey() callback for AES key import
- Update AES SetKey paths to call CryptoCB and branch on return value
- Skip GCM H/M table generation when callback succeeded (devCtx set)
- Preserve existing behavior when devId is INVALID_DEVID or
WOLF_CRYPTO_CB_AES_SETKEY is not defined
Testing:
- Add unit test for CryptoCB AES SetKey (verifies key isolation when
callback succeeds)
- Add end-to-end AES-GCM offload test (SetKey, Encrypt, Decrypt, Free
via CryptoCB)
- Tests use a mock SE with software AES to validate routing
Enable with: CPPFLAGS="-DWOLF_CRYPTO_CB_AES_SETKEY -DWOLF_CRYPTO_CB_FREE"
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>