wc_AesGetKeySize() reports aes->keylen for a key a crypto callback
device owns, so the MAX3266X ECB and CBC entry points, which take a
zero return from it as proof of a key, ran the TPU on the wiped
all-zero aes->key after a claim and returned success. They now check
WC_AES_KEY_IS_SET() first and return MISSING_KEY, as the generic modes
do. The check is only in effect with MAX3266X_CB, which is also the
only MAX3266X build where a device can claim a key.
The wipe before a claim left aes->tmp and aes->left, so the keystream
of the old key buffered by CTR, CFB or OFB stayed in the context. The
generic, ARM32 and PPC set-key now clear both as well.
The docs told a callback to build a software schedule by calling
wc_AesSetKey() with aes->devId set to INVALID_DEVID. Under
WOLF_CRYPTO_CB_FIND the find callback can map INVALID_DEVID back to
the same device, so that call recurses into the callback without end.
The docs now ask for a devId no device is registered under, as the
test devices in tests/api.c already use.
The wc_AesGcmSetKey() doxygen did not say what this series changed for
its callers: a new key drops the streaming state, so the update calls
return MISSING_IV until the next wc_AesGcmInit(); a nonce cached with
wc_AesGcmSetIV() or wc_AesGcmSetExtIV() is kept; and a key a crypto
callback device claims without a software schedule leaves streaming GCM
at MISSING_KEY.
The callback contract told a callback that sets aes->rounds to write a
key schedule into aes->key. That is not enough in every build: the
bitsliced and C-fallback builds read other fields, which the pre-claim
wipe has just zeroed, and the AES-NI and ARMv8 flags and the buffered
keystream would still describe the previous key. README.md, the
doxygen page and the cryptocb.h docs now say to build the schedule by
calling wc_AesSetKey() on the same object with aes->devId set to
INVALID_DEVID, as the existing test callback does, which sets all of
that. A WOLF_CRYPTO_CB_AES_SETKEY request carries no key direction, so
only a WOLF_CRYPTO_CB_SETKEY callback can do this.
Two comments in aes.c are reworded.
When a WOLF_CRYPTO_CB_SETKEY or WOLF_CRYPTO_CB_AES_SETKEY callback
claims an AES key, wc_AesSetKey() builds no software key schedule. The
wrapper still marked the context keyInstalled because the set-key
returned 0, so a mode the device then declined passed the
WC_AES_KEY_IS_SET guard and ran in software:
- On a fresh context rounds is 0. With the C implementation the mode
fails with KEYUSAGE_E; with ARM assembly on aarch64 it crashes
(SIGSEGV in AES_CBC_encrypt).
- On a context that held a software key before, rounds and the
schedule were still the old key's. The declined mode then succeeded,
encrypting with the previous key instead of the one just set.
The set-key now drops the old schedule before offering the key to the
device: rounds goes to 0, and the schedule, including the C-fallback and
bitsliced copies, and devKey are wiped so no material from the previous
key outlives the claim. After a claim the context is marked installed
only if the callback built a schedule itself, as the in-tree test
callbacks do by re-running the software set-key. Otherwise a declined
mode returns MISSING_KEY, matching wc_AesInit_Id().
WOLF_CRYPTO_CB_ONLY_AES builds keep the key marked installed, since
there every block goes to the device's ECB callback.
The same wipe also clears the streaming GCM key and the recorded key
length. Otherwise a context that held a software GCM key and was then
re-keyed through wc_AesSetKey() or wc_AesCcmSetKey() kept gcmKeySet, and
the ARM assembly build streamed GCM on the wiped schedule; and a set-key
the device failed left the old key length behind, which
wc_AesGetKeySize() reported as a valid key.
The ARM32 and PPC assembly builds have their own wc_AesSetKey(), which
set keyInstalled on every claim and kept the old schedule. They get the
same reset. On ARM32 a declined CBC on a freshly claimed key returned 0,
with output from the empty schedule. The PPC copy only offers the key
through WOLF_CRYPTO_CB_AES_SETKEY, so it resets on that path alone.
Streaming GCM had the same problem through gcmKeySet, which
wc_AesGcmSetKey() never cleared. After a software GCM key, a key the
device then claimed left the flag set, and with ARM assembly the
streaming API encrypted under the previous key and returned 0.
wc_AesGcmSetKey() now clears gcmKeySet before setting the key and sets
it again only on success. A set-key the device fails returns before
deriving H or copying the key to devKey, so a caller that ignores the
error cannot stream on the wiped schedule. Dropping the key also clears
nonceSet, so a context already set up for a message under the old key
needs a new wc_AesGcmInit() instead of streaming on that key's counter
state.
The flag is set before H is derived, so a failure there, a refused
vector register save in an AES-NI build or a failing wc_AesEncrypt(),
returned an error with gcmKeySet still set and H from the previous key.
That block now clears the flag on failure, as the aarch64 path already
did. Neither failure can be provoked from the API tests.
wc_AesGcmSetKey() also stops skipping H and the devKey copy when devCtx
is set. A key a device owns without a schedule now leaves through the
early return, so devCtx is only still set at that point when the context
has a software schedule: the device built one, or a
WOLF_CRYPTO_CB_AES_SETKEY device declined a re-key and kept its handle
from an earlier claim. In the second case the skip left H all zero, and
the C build produced streaming GCM tags that did not depend on the
ciphertext. The ARM assembly H derivation never checked devCtx.
The wc_CryptoCb_AesSetKey() docs promised a devKey copy and GCM tables
for software fallback in TLS builds, which no build provides, while the
README and the AES CryptoCB key import page promised the opposite with
no conditions. All three now describe what a claimed key leaves behind,
and that wolfCrypt takes a non-zero aes->rounds to mean the callback
built a software schedule. wc_AesGetKeySize() reports aes->keylen for a
key with a length but no rounds, so a callback has no reason to set
aes->rounds just to make key-size queries work. The keyInstalled field
comment in aes.h now says a claimed key counts only with a schedule.
The docs also now say that a callback declining a new key must first
release any key it holds for the object and set aes->devCtx to NULL.
wolfCrypt leaves devCtx alone on a decline, so a callback that kept its
handle went on running the operations it accepts, such as one-shot GCM,
under the old key while the software modes used the new one.
A WOLF_CRYPTO_CB_SETKEY claim now also stores the IV it was given in
aes->reg, as the AES_SETKEY claim and the software set-key already do,
so a device that reads the IV from aes->reg does not get the previous
key's.
test_wc_CryptoCb_Aes_SetKey_Declined claims a key, declines CBC and
expects MISSING_KEY, both on a fresh context and after a software key,
and checks the old key is gone from devKey. It needs
WOLFSSL_AES_REQUIRE_KEY_SET, since without the key-set check the
declined mode fails with KEYUSAGE_E instead.
test_wc_CryptoCb_AesGcm_SetKey covers the same software-then-claimed
sequence for streaming GCM.
test_wc_CryptoCb_AesSetKey checks the same for a key claimed through
WOLF_CRYPTO_CB_AES_SETKEY: CBC, which its device declines, returns
MISSING_KEY.
Both tests also check a set-key the device fails with WC_HW_E: CBC and
streaming GCM then return MISSING_KEY.
test_wc_CryptoCb_AesSetKey has its device release its handle and
decline a re-key to a different key, checks that one-shot GCM then fails
in the callback instead of using the old key, and compares the streaming
GCM tag with one computed in software under the new key.
test_wc_CryptoCb_Aes_SetKey_Declined also checks that a claimed key
takes the IV passed with it and reports its size, and that a failed
set-key reports none. test_wc_CryptoCb_AesGcm_SetKey re-keys a software
GCM key through wc_AesSetKey() and checks that streaming GCM refuses the
claimed key. test_wc_AesGcmStream checks that wc_AesGcmSetKey() on a
context initialized for a message leaves updates waiting for a new
wc_AesGcmInit().
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>