Repository navigation
ISO 19790 Zeroization for v7 - #11434
jackctj117 wants to merge 74 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved zeroization gaps remain, along with a critical SHA-3 build-compatibility issue.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This pull request expands secure zeroization across wolfCrypt cryptographic implementations to support ISO/IEC 19790 compliance.
Changes:
- Clears sensitive keys, hashes, seeds, signatures, and temporary buffers.
- Adds cleanup across classical, elliptic-curve, post-quantum, hashing, RNG, and KDF code.
- Extends zeroizing cleanup to architecture-specific arithmetic implementations.
File summaries
| File | Changes |
|---|---|
wolfcrypt/src/wolfentropy.c |
Zeroizes entropy samples and conditioning state. |
wolfcrypt/src/wc_xmss.c |
Clears XMSS state, seeds, and private material. |
wolfcrypt/src/wc_xmss_impl.c |
Clears XMSS hash intermediates. |
wolfcrypt/src/wc_slhdsa.c |
Clears SLH-DSA signing and key-generation state. |
wolfcrypt/src/wc_mlkem.c |
Clears ML-KEM key-generation failure state. |
wolfcrypt/src/wc_mldsa.c |
Clears ML-DSA signing and private state. |
wolfcrypt/src/wc_lms_impl.c |
Clears LMS hash and derived-seed buffers. |
wolfcrypt/src/sp_x86_64.c |
Zeroizes x86-64 arithmetic temporaries. |
wolfcrypt/src/sp_riscv64.c |
Zeroizes RISC-V arithmetic temporaries. |
wolfcrypt/src/sp_cortexm.c |
Zeroizes Cortex-M arithmetic temporaries. |
wolfcrypt/src/sp_c64.c |
Zeroizes generic 64-bit arithmetic temporaries. |
wolfcrypt/src/sp_c32.c |
Zeroizes generic 32-bit arithmetic temporaries. |
wolfcrypt/src/sp_armthumb.c |
Zeroizes ARM Thumb arithmetic temporaries. |
wolfcrypt/src/sp_arm64.c |
Zeroizes ARM64 arithmetic temporaries. |
wolfcrypt/src/sp_arm32.c |
Zeroizes ARM arithmetic temporaries. |
wolfcrypt/src/sha512.c |
Clears SHA-512 temporary state. |
wolfcrypt/src/sha3.c |
Clears SHA-3 state and buffers. |
wolfcrypt/src/sha256.c |
Clears SHA-256 temporary state. |
wolfcrypt/src/sha.c |
Clears hash temporary state. |
wolfcrypt/src/rsa.c |
Clears RSA temporary values. |
wolfcrypt/src/random.c |
Clears DRBG and temporary state. |
wolfcrypt/src/pwdbased.c |
Clears password-based derivation temporaries. |
wolfcrypt/src/kdf.c |
Clears derived-key temporary buffers. |
wolfcrypt/src/hmac.c |
Clears HMAC temporary state. |
wolfcrypt/src/ed448.c |
Clears Ed448 signing intermediates. |
wolfcrypt/src/ed25519.c |
Clears Ed25519 signing intermediates. |
wolfcrypt/src/ecc.c |
Clears ECC signing and scalar temporaries. |
wolfcrypt/src/dh.c |
Clears DH private and temporary state. |
wolfcrypt/src/curve448.c |
Clears Curve448 private material. |
wolfcrypt/src/curve25519.c |
Clears Curve25519 private material. |
wolfcrypt/src/cmac.c |
Clears CMAC state before release. |
wolfcrypt/src/aes.c |
Clears AES temporary state and intermediates. |
Review details
Suppressed comments (16)
wolfcrypt/src/aes.c:18629
- This wrapper now clears its tweak buffer, but
AesXtsEncryptUpdate_swstill leaves its localbuf/ciphertext-stealing buffers uncleared when AES fails (and after normal return). Those buffers contain plaintext or key-derived intermediate data, so the inner helper needs success and error cleanup as well.
ForceZero(tweak_block, sizeof(tweak_block));
wolfcrypt/src/aes.c:19177
- Clearing
tweak_blockhere does not clear the localbufandtmp2intermediates inAesXtsDecryptUpdate_sw. AES failures and the bounds-error return leave ciphertext/plaintext-derived values on the stack; add cleanup in that helper for each exit path.
ForceZero(tweak_block, sizeof(tweak_block));
wolfcrypt/src/cmac.c:681
wc_AesCmacVerify_exuses localafor the computed tag, but wiping theCmacobject here does not touch that buffer. On successful comparison, mismatch, or generation error, the derived tag remains on the stack; add a cleanup ofaon every exit after it is declared.
ForceZero(cmac, sizeof(Cmac));
XFREE(cmac, NULL, DYNAMIC_TYPE_CMAC);
wolfcrypt/src/curve25519.c:712
- This handles a public-generation failure, but
wc_curve25519_make_priv()can partially fillkey->kand return an RNG error before this block is entered. Because the function then returns withprivSetclear, the partial scalar is left in the key object. Wipekey->kon every nonzero private-generation result, not only after public derivation fails.
ForceZero(key->k, sizeof(key->k));
key->privSet = 0;
}
wolfcrypt/src/curve25519.c:805
- The normal key-generation path has the same gap as the nonblocking path:
wc_curve25519_make_priv()may leave partial RNG output inkey->k, then the function returns before this public-generation cleanup. Clear the scalar when private generation fails as well.
/* Public half failed: drop the scalar too
* (ISO/IEC 19790:2012 7.9.7). */
ForceZero(key->k, sizeof(key->k));
key->privSet = 0;
wolfcrypt/src/curve448.c:296
- This clears the scalar only when public derivation fails.
wc_RNG_GenerateBlock()can partially fillkey->kbefore returning an error, and that path bypasses this block, leaving failed key material in the object. Wipekey->kfor RNG failures too before returning.
ForceZero(key->k, sizeof(key->k));
XMEMSET(key->p, 0, sizeof(key->p));
/* A zeroised SSP shall not be reusable
* (ISO/IEC 19790:2012 7.9.7 [09.29]). */
key->privSet = 0;
wolfcrypt/src/dh.c:1515
GeneratePrivateDh186()can fill part ofprivand then return an error frommp_to_unsigned_bin()after setting*privSz, but this flag is set only when the call returns success. The new cleanup therefore skips a partially written private key on that path; track possible output (or wipe on every nonzero return using the output capacity) before returning.
privWritten = (ret == 0);
wolfcrypt/src/dh.c:1528
- The new private-key wipe is limited to
wc_DhGenerateKeyPair_Sync. The Intel async path generatesprivbefore bignum conversion and hardware submission, then returns directly on failures without clearing it or resetting*privSz; a failed asynchronous key pair can therefore expose the generated private value. Apply the same failure cleanup to that path before its return.
if (privWritten && (ret != 0)) {
/* A failed pair is not handed back (ISO/IEC 19790:2012 7.9.7). */
ForceZero(priv, *privSz);
*privSz = 0;
wolfcrypt/src/ecc.c:7904
- The cleanup at this point only runs on the normal software path. Deterministic signing has already populated
key->sign_k, but the allocation/initialization failures in the earlier bignum setup and the Intel/Cavium async branch return before reaching this line, leaving the nonce allocated in the key. Route those exits through a common cleanup label (while preserving the pending-operation case) so every completed error consumessign_k.
ecc_sign_k_forcezero(key);
wolfcrypt/src/ed25519.c:485
- The PCT cleanup clears the generated key, but
ed25519_pairwise_consistency_test()also leaves its RNG-generateddigestmessage on the stack on success and every failure path. Since this temporary random material is created solely for the consistency test, clear it before returning from the PCT as well.
ForceZero(key->k, ED25519_PRV_KEY_SIZE);
ForceZero(key->p, ED25519_PUB_KEY_SIZE);
wolfcrypt/src/ed448.c:417
- The PCT cleanup clears the generated key, but
ed448_pairwise_consistency_test()also leaves its RNG-generateddigestmessage on the stack on success and every failure path. Clear that temporary random material before the PCT returns.
ForceZero(key->k, ED448_PRV_KEY_SIZE);
ForceZero(key->p, ED448_PUB_KEY_SIZE);
wolfcrypt/src/wc_lms_impl.c:485
wc_Sha256HashBlock()stages the input block insha256->bufferon the byte-reversing path and does not clear it. The LMS caller passes blocks containingSEED/WOTS material, so clearing only the local digest output still leaves secret input in the hash object after this helper returns.
/* Prefix is x_q[i] or a child SEED (ISO/IEC 19790:2012 7.9.7). */
ForceZero(output, sizeof(output));
wolfcrypt/src/wc_lms_impl.c:550
- In the 64-to-120-byte path above, the secret suffix and padding are written into
sha256->bufferandwc_Sha256HashBlock()leaves that buffer populated. The newoutputwipe does not remove this residual seed material; clear the SHA-256 buffer here as well.
/* Prefix is x_q[i] or a child SEED (ISO/IEC 19790:2012 7.9.7). */
ForceZero(output, sizeof(output));
wolfcrypt/src/wc_lms_impl.c:596
- This manual finalizer constructs the padded block directly in
sha256->buffer, andwc_Sha256HashBlock()does not reset or wipe that field. Consequently, secret LMS prefixes can remain in the hash object after the finalizer returns; wipe the buffer on both success and error paths.
/* Prefix is x_q[i] or a child SEED (ISO/IEC 19790:2012 7.9.7). */
ForceZero(output, sizeof(output));
wolfcrypt/src/wc_slhdsa.c:7368
- On a hypertree-signing failure, the code clears only the FORS prefix.
slhdsakey_ht_signcan have already written part of the suffix, and the caller has also written the initial randomness prefix, so those regions can retain partial signature/WOTS intermediates after an error. Clear the entire produced signature buffer on failure, with the outer caller handling its prefix length correctly.
if (ret != 0) {
/* Unreleased FORS secrets may be in sig
* (ISO/IEC 19790:2012 7.9.7). */
ForceZero(sigFors, key->params->k * (1 + key->params->a) * n);
wolfcrypt/src/wc_slhdsa.c:7240
- After callback unavailability, the software fallback stages all three seeds in
key->skandslhdsakey_compute_root()can return an error. The callback-only wipe above is not reached on that path, so failed software generation leaves private seeds marked in the key object; clear them and invalidate the key before returning the fallback error.
/* fall-through when unavailable */
ret = 0;
- Files reviewed: 32/32 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
9df8962 to
86ed158
Compare
a21ae27 to
0ad5d42
Compare
0ad5d42 to
78c1884
Compare
013c1fa to
201f2e8
Compare
JacobBarthelmeh
left a comment
There was a problem hiding this comment.
Changes look good. Performance is under review.
1c3cc39 to
a895adf
Compare
Callback and async paths, dynamic fallback, non-blocking contexts, SAKKE, DH, X25519/X448 and the fe/ge arithmetic, PBKDF1, PKCS#12, scrypt, TLS 1.0 PRF, X9.63 KDF, ECIES and the LMS/XMSS private paths. They return in a non-FIPS follow-up.
ML-KEM wipes its hash objects at every exit (FIPS 203 3.3). GCM wipes the raw GHASH, the stale H table, the stream state and partial plaintext on a primitive failure. OAEP unpad wipes its whole scratch. The ECC ladder tmp is wiped on every exit and a rejected private import clears the key type. EdDSA frees the persistent hash on failure. The stir purge keeps the old EMPTY store when its claim fails. ecc.c reads the shared struct layouts before its local small-stack override.
The ECC import type check is skipped on the device set-key path: with WOLF_CRYPTO_CB_SETKEY and WOLF_CRYPTO_CB_FIND the import is staged in a temporary key, so a rejected scalar leaves the caller's key as it was. The RSA OAEP and AES-GCM failure-wipe tests are skipped under HAVE_SELFTEST, where the CAVP build replaces rsa.c and aes.c with the tagged selftest sources.
Importing over a live key dropped the previous scalar and ordinates without a wipe or a free. Caught by the async valgrind run.
wolfBoot's keytools imports into a key that was never initialised, so the cleanup from 63d8a61 read garbage and crashed. The test now rejects the scalar into a fresh key instead of re-importing over a live one.
Keeping the C round scratch in the context cost 1 to 3 percent on the C lane (SHA-3, SHAKE, KMAC, ML-KEM). The block function and its callers go back to master's local scratch; the state wipes at Free stay.
… per block Move W back to the stack as on master. Holding it in the context made the C transform alias W with the input block, which cost SHA-256 and HMAC-SHA256 about 3% on default builds. W is now wiped before the transform returns, the same way sha512.c already handles its schedule, so a keyed block never outlives the frame. The Transform_Sha256_Len copy buffer is hoisted out of the loop and wiped once per call. WOLFSSL_CHECK_MEM_ZERO covers all of them.
…olatile stores Without GNU inline asm, or with WOLFSSL_NO_ASM, WC_BARRIER_DATA() used to fall back to WC_BARRIER(), which does not cover a local whose address never escaped, so the wipe could be dropped as a dead store. The volatile typedefs in ForceZero worked around that at the cost of a byte-by-byte wipe and two fences per call. The fallback now passes the pointer to wc_BarrierDataSink() in wc_port.c, reached through a volatile load of a function pointer so link time optimization cannot see the empty body, and ForceZero goes back to the plain stores on master.
gcc with -Wsign-conversion rejects the int product of the byte parameters being passed as a size_t.
…plete zeroization coverage in wc_RNG_TestSeed() and _InitRng(). * NextStirPurge(): never claim or repaint a constructor's _TORNDOWN -- the reopen is the constructor's to make. The claim loop skips _TORNDOWN alongside _CONSUMING (re-checked on every retry), and the reopen is now a CAS retry loop rather than a plain store, so a depositor's fill length stored over the claim is discarded (length only -- its bytes lie in place until the next wipe or full-buffer fold, benign for the uncredited stir) while a _TORNDOWN painted at any point is left standing. Both abort arms (a port's retry clause) get explicit dispositions: an aborted claim now stores nothing, since any store could repaint a live claim or a _TORNDOWN, and the stale fill's resurrection is benign by the three-no-ops doctrine; an aborted reopen stores EMPTY, since stranding _CONSUMING would silence the stir aperture on the instance, and the repaint risk on that already-exotic path is accepted. * wc_RNG_TestSeed(): zeroize the APT reference byte and the all-values test's majority candidate at the end of their window scopes, matching the RCT arm's prevByte -- all three hold a byte of raw seed. * _InitRng(): set drbg_scratch_instantiated before the scratch instantiate calls (both boundaries), as for drbg_instantiated: a failed instantiate still leaves V in the state, and the failure arm must uninstantiate it. The scratch is NULL-seeded, so the stranded state would be a public constant -- wiped anyway, for uniformity of the invariant.
Follows the WC_INLINE pattern: C99 restrict where available, __restrict on GNU-compatible compilers and MSVC (including C++ translation units), and nothing on compilers with no support. WC_RESTRICT applies the C99 restrict contract: it tells the compiler that in each call, objects modified through the qualified pointer are accessed only through it (or through pointers derived from it), and that objects it accesses are modified only through the qualified pointer. A caller-supplied buffer therefore compiles like a local array. The practical benefit is that, after a store through the qualified pointer, the compiler can keep using a value previously loaded through an unrelated pointer without reloading it.
wc_mldsa.c mldsa_import_priv_raw: the pre-copy ForceZero of key->k runs before XMEMCPY(key->k, priv, privSz), so a caller re-importing from key->k itself (an exact alias) has the source zeroed and imports an all-zero key -- and because mldsa_check_eta_range() validates priv BEFORE the wipe, the zeroed import passes validation, rebuilds the caches from zeros, and returns success with prvKeySet set. Skip both the wipe and the copy for an exact alias: the new key is already in place, and exact-alias XMEMCPY is undefined besides. The sibling import sites (SLH-DSA seed and encoded imports, ML-DSA keygen's k move) preserve aliased input in a stack bounce buffer per ISO 19790 7.9.7; at ML-DSA private-key sizes (2560-4896 bytes) a stack bounce is not an option, and in the non-aliased case the same-size overwrite leaves no residue of the old key, so the explicit wipe is kept only on that path.
Refactor to eliminate the per-block ForceZero of the C transforms' message
schedule (and, for SHA-256, the working variables): Move the scratch to the
caller so the wipe runs once per public call instead of once per block, without
changing any context layout, public signature, dispatch arm, or function-pointer
table:
- Sha256TScratch{W,S} / Sha512TScratch{W}, provisioned by per-config
DECL/BEGIN/WIPE macros built on the WC_DECLARE_VAR / WC_ALLOC_VAR_EX /
WC_FREE_VAR_EX family: a local in big-stack builds, a per-call heap block
under WOLFSSL_SMALL_STACK (previously allocated and freed PER BLOCK), and the
per-context cache under WOLFSSL_SMALL_STACK_CACHE, which keeps its zero
per-call cost and wipe-at-Free semantics (cache allocations are resized to
the scratch struct; the wc_Sha512Transform() backup block now sits after it).
- External providers of wc_Sha256.W / wc_Sha512.W (linuxkm's w_buf, the
Nuvoton copy callback, mpfs_athena's SHA-384 copy) size their buffers by new
WC_SHA256_TSCRATCH_SIZE / WC_SHA512_TSCRATCH_SIZE constants in the public
headers, and each translation unit statically asserts its scratch struct
matches its constant, so providers and the transform cannot drift apart.
- Transform_Sha256_scr() / _Transform_Sha512_scr() take the scratch as a
WC_RESTRICT parameter, so values loaded through other pointers stay in
registers across schedule stores, avoiding aliasing cost otherwise associated
with placing W on the stack. The old-signature transforms remain as
compatibility shims with per-call scratch, keeping every dispatch arm and
function-pointer type untouched.
- The bulk C paths hoist one scratch across their block loop:
Transform_Sha256_Len(), both *_Len_C_from_raw(), and both of Sha512Update()'s
block loops -- including the intel arm's runtime pre-AVX fallback loop -- so
every multi-block C path wipes once per call. In the loop shared with the
ESP32 port, the hoist is compiled only for the software-transform
configuration; hardware-port arms are untouched.
- The *_C_from_raw() adapters become thin wrappers over new *_C_from_raw_scr()
variants: the block is staged (when not already there) in the aligned context
buffer and byte-reversed in place, so the bulk loops share one scratch while
raw input of any alignment stays safe on strict-alignment targets.
- SHA-512's working variables T stay a local with their existing
per-block wipe: behind a pointer they cannot be register-allocated
across the 80 rounds, measured at ~9% on the C transform, while their
wipe is one cache line.
- WOLFSSL_CHECK_MEM_ZERO coverage moves with the scratch: registered at
BEGIN, checked at WIPE, with the scratch zeroed at registration (a const
void* registration argument pointing at an uninitialized object trips
-Wmaybe-uninitialized under -Werror); wc_Sha512Transform()'s existing
backup-buffer registration gets the same initialization.
Zeroization lifetime: transient schedule and working variables are wiped
when the public call returns; the pending input block in ctx->buffer
already persists longer, so no new exposure class is introduced. The
chaining state the working variables derive from persists in the context
until Free, as before.
Performance scope (noasm C transforms; ForceZero never auto-vectorizes, so the
wipe is scalar word stores in userspace and kernel alike): recovers the ~2.3%
SHA-256 regression from the per-block W+S wipes, and improves SHA-512 8% over
master itself by amortizing the per-block W wipe out of Sha512Update()'s block
loop. Asm-accelerated paths (intelasm, armasm) carry their own schedules and
are untouched in both directions.
…IC_STRUCT_INITIALIZER, not ad hoc { 0 }, for warning-free build in C++ mode.
…ro stack buffers before WOLFSSL_CHECK_MEM_ZERO registration. wc_MemZero_Add() takes const void*, so gcc assumes a read: registering a not-yet-written stack buffer trips -Wmaybe-uninitialized under -Werror. Zero each registered buffer first -- cost confined to WOLFSSL_CHECK_MEM_ZERO builds.
…6TScratch), as allocation operand in smallstack wc_Sha{224,256}Copy().
wolfssl/wolfcrypt/sha{256,512}.h: add note that WC_SHA{256,512}_TSCRATCH_SIZE are precious names that wolfBoot depends on.
…frodokem_mat.c: refactor ad-hoc __restrict macros using WC_RESTRICT (no functional changes).
Zeroization of SSPs inside the FIPS v7 module for ISO/IEC 19790:2012 7.9.7: the Level 1 zeroization methods ([09.28], [09.29]) and the Level 2 rules for temporary SSPs ([09.30] to [09.32]). Every wipe is
ForceZero; none is built out by a define. Scope is the v7 service list; see the end for what is left out on purpose.By area
WOLFSSL_HASH_KEEPmessages andSMALL_STACK_CACHEschedules are wiped before they are freed. HMAC wipes its key padding temporaries and the object.GcmScratch, keystream blocks, XTS tweaks and key-wrap intermediates. GCM and CCM decrypt output is wiped on authentication or primitive failure, the raw GHASH is wiped if E(J0) fails, a failed re-key clears the GCM state, and a stream whose primitive failed is invalidated. One-shot CMAC objects are wiped before free.SMALL_STACK_CACHE.ForceZeroon compilers without the inline-asm barrier.Out of scope on purpose, per the v7 service list: crypto callback and async paths,
WC_C_DYNAMIC_FALLBACK, non-blocking contexts, SAKKE, DH, X25519/X448 and the fe/ge math files, PBKDF1, PKCS#12 PBKDF, scrypt, TLS 1.0 PRF, X9.63 KDF, ECIES, and the LMS/XMSS private-key paths. The SHA-3 block function signature change is carried through the fallback and FrodoKEM call sites so non-FIPS builds keep compiling. The SP backends and the assembly backends are a separate change and follow in a sister PR.Performance: RSA-2048, ECDHE and ECDSA P-256/384/521 and ML-KEM-768 are within 1 percent of master on arm64.
ABI note
The per-block scratch of the software hashes now lives in the context so it can be wiped once at Free instead of once per block or per Final. That changes public struct sizes:
wc_ShaW[16]wc_Sha256Wbuf[64](WC_SHA256_W_IN_CTX), except kernel builds (WC_SHA2_NO_SMALL_STACK) and small-stack builds with malloc, which keep it on the stack or heapwc_Sha3scratch[WC_SHA3_SCRATCH_W](5, 30 or 80 lanes by configuration)SlhDsaKeysha2Digest[32]at the end underWOLFSSL_SLHDSA_SHA2Everything that embeds these grows with them:
Hmac,wc_HashAlgand the OpenSSL compatibility holders. On the reviewer's 64-bit default build that waswc_Sha104 to 168 bytes,wc_Sha256120 to 376,wc_Sha3416 to 656,Hmac784 to 1024,wc_HashAlg432 to 672; exact sizes depend on target and options.WC_SHA3_BLOCK_FNalso takes a scratch argument whenWC_SHA3_SCRATCH_Wis defined.Applications that embed these structs or link the shared library must be rebuilt against the new headers. Whether the release bumps the soname is a release decision. The layout conditions follow
WOLFSSL_SMALL_STACKandWOLFSSL_SMALL_STACK_CACHE, soecc.cnow includes the hash, RNG and AES headers before its local override of those macros.