Skip to content

wolfCrypt security hardening, portability fixes, and negative test coverage - #10958

Merged
dgarske merged 11 commits into
wolfSSL:masterfrom
Frauschi:fenrir
Aug 3, 2026
Merged

wolfCrypt security hardening, portability fixes, and negative test coverage#10958
dgarske merged 11 commits into
wolfSSL:masterfrom
Frauschi:fenrir

Conversation

@Frauschi

Copy link
Copy Markdown
Contributor

Summary

This branch is a batch of eleven independent fixes across wolfCrypt and several hardware ports, from a security review. They fall into four groups: fail-open/validation hardening, undefined-behavior/portability fixes, hardware-port correctness, and negative regression test coverage. Each change is a self-contained commit; there is no shared refactoring.

Validation and fail-open hardening

  • ChaCha20 rejects an unset key. wc_Chacha_Process had no key-state check, so a zero-initialized context that received only a nonce would encrypt with an all-zero, attacker-predictable key and return success. A keySet flag is now set in wc_Chacha_SetKey and checked in wc_Chacha_Process, which returns MISSING_KEY, mirroring wc_Arc4Process.
  • DH validates untrusted moduli with random-witness Miller-Rabin. wc_DhSetKey_ex loads parameters as untrusted but ran a fixed-base primality test (bases 2..19) when no RNG was supplied, which a crafted strong pseudoprime can pass. It now creates a temporary RNG so mp_prime_is_prime_ex runs with random witnesses, and falls back to the deterministic test only when no RNG can be obtained, so there is no new failure mode. Named FFDHE primes still short-circuit.
  • ECDH rejects a point-at-infinity shared secret. The non-SP path of wc_ecc_shared_secret_gen_sync copied the affine x-coordinate to the output without checking for the identity point, so a shared secret that computed to infinity was returned as an all-zero secret with a success code. It now returns ECC_INF_E, as required by SP 800-56Ar3 section 5.7.1.2.

Undefined behavior and portability

  • RSA (SE050): alignment-safe key-id read. wc_InitRsaKey_Id cast the byte id[] array to word32* and dereferenced it, an unaligned 32-bit read that faults or misreads on strict-alignment targets and violates strict aliasing. It now uses readUnalignedWord32, matching the existing wc_ecc_init_id.
  • ML-KEM: alignment-safe reads and writes. The little-endian fast paths of mlkem_cbd_eta3 (input) and mlkem_vec_compress_10_c (output ciphertext) reinterpreted caller byte buffers through word32* pointers. Both now use readUnalignedWord32 / writeUnalignedWord32, matching the neighboring mlkem_cbd_eta2 and mldsa_encode_w1_88_c.

Hardware-port correctness

  • PSoC6: bound ECDSA r/s before serialization. psoc6_ecc_verify_hash_ex wrote attacker-supplied r/s of unbounded size into a fixed 132-byte stack buffer, a pre-authentication stack overflow reachable during TLS signature verification. It now rejects any component larger than the key size, matching the generic path's wc_ecc_check_r_s_range.
  • Intel QAT: surface hardware failures. The synchronous IntelQaSymCipher never translated the cpaCySymPerformOp completion status into its return code, so a hardware failure returned success with the plaintext copy written to the ciphertext output. A non-success status now yields ASYNC_OP_E, matching the asynchronous port.
  • ESP32-C3: fix transposed register and mask. The RSA accelerator deactivation passed the register address and bit mask in the wrong argument order (with a spurious base offset), so the accelerator was never powered down and stray writes hit a bogus address. The arguments now match the activation path and the other targets.

Negative test coverage

  • wc_ecc_check_key. Added a test that imports secp224r1 public keys that are off the curve and out of coordinate range, asserting IS_POINT_E and ECC_OUT_OF_RANGE_E. This closed a mutation-survival gap: the software on-curve and range checks previously had no negative coverage in a software build.
  • Ed448 signature S-range. Added a test that signs a message and then verifies crafted signatures whose scalar S equals or exceeds the group order, including the malleability case S + L, asserting rejection. Without the range check, (R, S + L) verifies as a second valid signature for the same message.

@Frauschi Frauschi self-assigned this Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

gcc-arm-cortex-m0plus

  • FLASH: .text +36 B (+0.1%, 64,243 B / 262,144 B, total: 25% used)

gcc-arm-cortex-m3

  • FLASH: .text +44 B (+0.0%, 122,655 B / 262,144 B, total: 47% used)

gcc-arm-cortex-m4

  • FLASH: .text +64 B (+0.0%, 200,981 B / 262,144 B, total: 77% used)

gcc-arm-cortex-m4-baremetal

  • FLASH: .text +64 B (+0.1%, 66,891 B / 262,144 B, total: 26% used)

gcc-arm-cortex-m4-crypto-only

  • FLASH: .text +64 B (+0.0%, 175,056 B / 262,144 B, total: 67% used)

gcc-arm-cortex-m4-min-ecc

  • FLASH: .text +64 B (+0.1%, 61,805 B / 262,144 B, total: 24% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .text +128 B (+0.0%, 771,540 B / 1,048,576 B, total: 74% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .text +128 B (+0.0%, 326,232 B / 1,048,576 B, total: 31% used)

gcc-arm-cortex-m4-sp-math

  • FLASH: .text +64 B (+0.1%, 61,805 B / 262,144 B, total: 24% used)

gcc-arm-cortex-m4-tls12

  • FLASH: .text +64 B (+0.1%, 123,419 B / 262,144 B, total: 47% used)

gcc-arm-cortex-m4-tls13

  • FLASH: .text +64 B (+0.0%, 236,679 B / 262,144 B, total: 90% used)

gcc-arm-cortex-m7

  • FLASH: .text +64 B (+0.0%, 200,917 B / 262,144 B, total: 77% used)

gcc-arm-cortex-m7-pq

  • FLASH: .text +64 B (+0.0%, 280,576 B / 1,048,576 B, total: 27% used)

gcc-arm-cortex-m7-tls13

@Frauschi
Frauschi force-pushed the fenrir branch 2 times, most recently from 1521775 to 7d339d2 Compare July 22, 2026 08:40
@Frauschi Frauschi assigned wolfSSL-Bot and unassigned Frauschi Jul 22, 2026

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #10958

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.

Comment thread wolfcrypt/src/dh.c Outdated
@Frauschi
Frauschi force-pushed the fenrir branch 2 times, most recently from 55947e5 to d7d80ad Compare July 22, 2026 11:07
@philljj
philljj self-requested a review July 31, 2026 15:10

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skoll Code Review

Scan type: reviewOverall recommendation: COMMENT
Findings: 10 total — 10 posted, 0 skipped
10 finding(s) posted as inline comments (see file-level comments below)

Posted findings

  • [Medium] ECDH infinity check is skipped for a zero scalar, which is the only way infinity occurs on prime-order curveswolfcrypt/src/ecc.c:5046-5055
  • [Medium] Infinity rejection is absent from the SP shared-secret paths, so the hardening does not apply to P-256/384/521 in SP buildswolfcrypt/src/ecc.c:5046-5055
  • [Medium] DH temporary RNG hand-rolls the allocate/init/free sequence instead of using wc_rng_new_ex()/wc_rng_free()wolfcrypt/src/dh.c:2673-2706
  • [Medium] wc_DhSetKey_ex now instantiates a DRBG per call and silently downgrades to the fixed-base test when it cannotwolfcrypt/src/dh.c:2673-2695
  • [Medium] New DH pseudoprime test asserts an outcome the implementation only provides on a best-effort basistests/api/test_dh.c:330-350
  • [Low] *PSoC6: new attacker-reachable early return leaves verif_res unmodifiedwolfcrypt/src/port/cypress/psoc6_crypto.c:2110-2114
  • [Low] New PSoC6 rejection returns a positive error value (-BAD_FUNC_ARG)wolfcrypt/src/port/cypress/psoc6_crypto.c:2113
  • [Low] Two new lines exceed the 80-column source-text limitwolfcrypt/src/dh.c:2684
  • [Low] Japanese doxygen for ecc.h not updated alongside the English copydoc/dox_comments/header_files/ecc.h:319-320
  • [Low] ESP32-C3 register fix and other hardware-port changes have no automated coveragewolfcrypt/src/port/Espressif/esp32_mp.c:567-576

Review generated by Skoll

Comment thread wolfcrypt/src/ecc.c
Comment thread wolfcrypt/src/ecc.c
Comment thread wolfcrypt/src/dh.c
Comment thread wolfcrypt/src/dh.c
Comment thread tests/api/test_dh.c
Comment thread wolfcrypt/src/port/cypress/psoc6_crypto.c
Comment thread wolfcrypt/src/port/cypress/psoc6_crypto.c Outdated
Comment thread wolfcrypt/src/dh.c Outdated
Comment thread doc/dox_comments/header_files/ecc.h
Comment thread wolfcrypt/src/port/Espressif/esp32_mp.c

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frauschi added 11 commits August 1, 2026 13:11
psoc6_ecc_verify_hash_ex serialized the signature r and s components
into a fixed 132-byte stack buffer using mp_to_unsigned_bin without
checking their sizes. The values come from attacker-supplied ASN.1 in
DecodeECC_DSA_Sig with no magnitude cap beyond sp_int capacity, so an
oversized r or s wrote past signature_buf, a pre-authentication stack
overflow reachable during TLS signature verification. The generic path
guards this with wc_ecc_check_r_s_range, but that check is compiled out
on WOLFSSL_PSOC6_CRYPTO builds and the port function performed no r/s
validation of its own. Reject any r or s whose serialized size exceeds
the key size before writing into the buffer.

Fixes F-6778.
wc_Chacha_Process validated only its pointer arguments and then produced
keystream directly from the context state. A zero-initialized ChaCha
context, common for static or global storage, that received a nonce via
wc_Chacha_SetIV but never had wc_Chacha_SetKey called would encrypt with
an all-zero, attacker-predictable key and still return success. This is
the same fail-open class already guarded against in wc_Arc4Process.

Add a keySet flag to the ChaCha struct, set it in wc_Chacha_SetKey, and
return MISSING_KEY from wc_Chacha_Process when the key was never set.

Fixes F-6893.
wc_ecc_shared_secret_gen_sync ran the scalar multiplication and then
copied the x-coordinate to the output without checking whether the
result was the point at infinity. Both math backends report success for
the identity: ecc_map_ex sets x, y to zero and z to one and returns
success, and the single precision generators serialize the identity as
an all-zero x-coordinate. Either way a shared secret that computed to
infinity was handed back as an all-zero secret with a success code,
where SP 800-56Ar3 5.7.1.2 requires an error and stop.

Check the mapped point on the software path, and detect the all-zero
output after the single precision generators, returning ECC_INF_E in
both cases. The scan accumulates over the whole buffer so it does not
branch on the secret.

A key whose private value is resident in an SE050 carries no software
scalar, so the software multiply legitimately yields the identity for
it. Skip the check for those keys specifically, rather than for a zero
scalar: on a prime-order curve a zero scalar is the one way the identity
can arise, so exempting it would disable the check for the case it
exists to catch.

Fixes F-6770.
wc_DhSetKey_ex loads DH parameters as untrusted and validates that the
modulus is prime, but it passed no RNG, so the check fell back to a
Miller-Rabin test using the fixed small-prime bases 2 through 19. That
test is defeatable: a composite crafted as a strong pseudoprime to those
known bases passes as prime, letting an attacker supply a composite
modulus with a smooth factorization for small-subgroup recovery of the
private exponent and shared secret.

When no RNG is supplied on the untrusted path, create a temporary RNG so
mp_prime_is_prime_ex runs with random witnesses, which such crafted
composites cannot reliably pass. Named FFDHE primes still short-circuit
the check, and builds without an RNG keep the deterministic test.

Fixes F-6776.
wc_ecc_check_key validates a public key's coordinate range, that the
point is on the curve, and its order, but the software path had no
negative coverage: the existing test only exercised a valid key and
NULL, and the off-curve case lived in the crypto-callback test, which
validates the device path rather than the software on-curve check. A
deletion of either the on-curve check or the coordinate-range checks
therefore passed the suite.

Add a test that imports secp256r1 public keys that are off the curve
and out of coordinate range, asserting IS_POINT_E and ECC_OUT_OF_RANGE_E
respectively, exercising the software validation path.

Fixes F-6620.
Ed448 verification rejects a non-canonical signature scalar S (S >= L)
per RFC 8032, and that range check is the only guard against a malleated
signature: because L times the base point is the identity, (R, S + L)
recomputes the same R and would otherwise verify. The check had no
negative coverage, so a deletion or boundary mutation passed the suite
while all canonical KAT signatures kept working.

Add a test that signs a message, then verifies crafted signatures whose
S half equals the order, exceeds it in a high or low byte, and equals
S + L, asserting BAD_FUNC_ARG, plus an in-range wrong S asserting
SIG_VERIFY_E.

Fixes F-6777.
IntelQaSymCipher ran cpaCySymPerformOp synchronously but never
translated its completion status into the return code. Only the AES-GCM
decrypt auth check, driven by verifyResult, could set an error. For
AES-CBC, AES-GCM encrypt, and 3DES-CBC a non-success status left ret at
zero, so the exit path copied the working buffer to the output and
returned success. On encrypt that buffer still holds the plaintext copy,
so a hardware failure returned success with plaintext written to the
ciphertext output.

Translate a non-success perform-op status into ASYNC_OP_E, matching the
asynchronous port, so hardware failures are surfaced instead of
returning zero with unprocessed output.

Fixes F-6623.
The ESP32-C3 deactivation path in esp_mp_hw_unlock passed its arguments
to DPORT_REG_CLR_BIT and DPORT_REG_SET_BIT in the wrong order and added
a stray DR_REG_RSA_BASE offset. The macros take (register address, bit
mask), but the code used the bit mask as part of the register address
and the register as the bit mask. As a result the RSA clock-enable and
memory power-down bits were never updated at deactivation, and reads and
writes landed at a bogus peripheral address. The accelerator was left
powered up after every bignum and RSA operation.

Pass the register address first and the bit mask second and drop the
offset, mirroring the activation path and the other targets.

Fixes F-6779.
wc_InitRsaKey_Id cast the byte array key->id to word32* and dereferenced
it to recover the SE050 key id. key->id is a byte array with no alignment
guarantee inside RsaKey, so the dereference is an unaligned 32-bit read
that faults or mis-reads on strict-alignment targets, and it violates
strict aliasing. Read the value with readUnalignedWord32 instead, which
does an alignment-safe byte copy, matching wc_ecc_init_id.

Fixes F-6624.
The little-endian large-code path of mlkem_cbd_eta3 cast the
caller-supplied byte buffer to word32* and read through it. The buffer
has no alignment guarantee, so on strict-alignment targets such as
Cortex-M3 and M4 with unaligned-access trapping enabled the read faults
or returns wrong values, and the cast violates strict aliasing. Read
each word with readUnalignedWord32, which does an alignment-safe byte
copy, matching the neighboring mlkem_cbd_eta2.

Fixes F-6781.
The little-endian large-code path of mlkem_vec_compress_10_c cast the
output byte buffer to word32* and issued five 32-bit stores through it.
That buffer is the caller-supplied ML-KEM ciphertext, which has no
alignment guarantee, so on strict-alignment targets the store bus-faults
and the cast violates strict aliasing. Write each word with
writeUnalignedWord32, which does an alignment-safe byte copy, matching
mldsa_encode_w1_88_c and the neighboring ML-KEM sampling code.

Fixes F-6782.
@Frauschi Frauschi removed their assignment Aug 1, 2026
@Frauschi
Frauschi requested a review from dgarske August 1, 2026 12:31
@dgarske
dgarske merged commit 8063178 into wolfSSL:master Aug 3, 2026
352 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants