Skip to content

Fix ML-DSA MakePublicKey derivation, verify guards, and ASN derivation testing - #10985

Open
stenslae wants to merge 1 commit into
wolfSSL:masterfrom
stenslae:fix-mldsa-privkey-derive-pubkey
Open

Fix ML-DSA MakePublicKey derivation, verify guards, and ASN derivation testing#10985
stenslae wants to merge 1 commit into
wolfSSL:masterfrom
stenslae:fix-mldsa-privkey-derive-pubkey

Conversation

@stenslae

@stenslae stenslae commented Jul 23, 2026

Copy link
Copy Markdown
Member

Description

Refactored ML-DSA public key derivation to fix memory allocation issues in SMALL_MEM builds, and adds guards to ensure verification isn't attempted without a valid public key. Introduced ECC best-effort public key derivation when decoding private-only SEC1 keys. Added additional CI testing.

Notes:

  • ML-DSA verify now returns PUBLIC_KEY_E instead of BAD_FUNC_ARG when no
    public key is set.
  • ECC private key decode now derives the public point by default. Define WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE to opt out.
  • ecc_make_pub_ex() no longer marks a key ECC_PRIVATEKEY when derivation failed.

wolfSSL/wolfssh#1120

Testing

Added test coverage.

Checklist

  • added tests
  • updated/added doxygen
  • updated appropriate READMEs
  • Updated manual and documentation

@stenslae stenslae self-assigned this Jul 23, 2026
@stenslae
stenslae marked this pull request as ready for review July 23, 2026 18:06
@github-actions

Copy link
Copy Markdown

retest this please

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

gcc-arm-cortex-m0plus

  • FLASH: .text +32 B (+0.0%, 64,275 B / 262,144 B, total: 25% used)

gcc-arm-cortex-m3

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

gcc-arm-cortex-m4-baremetal

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

gcc-arm-cortex-m4-min-ecc

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

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .text +64 B (+0.0%, 772,140 B / 1,048,576 B, total: 74% used)

gcc-arm-cortex-m4-pq

  • FLASH: .rodata -32 B, .text +576 B (+0.2%, 280,672 B / 1,048,576 B, total: 27% used)

gcc-arm-cortex-m4-sp-math

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

gcc-arm-cortex-m7

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

gcc-arm-cortex-m7-pq

  • FLASH: .rodata -32 B, .text +576 B (+0.2%, 281,248 B / 1,048,576 B, total: 27% used)

linuxkm-pie

  • Data: __patchable_function_entries +24 B (+0.1%, 26,376 B)

linuxkm-standard

@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 #10985

Scan targets checked: wolfcrypt-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/wc_mldsa.c Outdated

@Frauschi Frauschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🐺 Skoll Code Review

Overall recommendation: REQUEST_CHANGES
Findings: 6 total — 4 posted, 2 skipped

Posted findings

  • [High] wc_MlDsaKey_MakePublicKey mishandles buffers in the small-mem path (aSet, h, s1)wolfcrypt/src/wc_mldsa.c:10413-10665
  • [Medium] wc_EccPrivateKeyDecode now unconditionally derives the public pointwolfcrypt/src/asn.c:32722-32745
  • [Low] Dead/empty guard branch before t0Scratch allocationwolfcrypt/src/wc_mldsa.c:10458-10467
  • [Medium] Duplicated t = A o NTT(s1) + s2 kernel - extract shared helper with keygenwolfcrypt/src/wc_mldsa.c:10479-10635
Skipped findings
  • [Medium] No test coverage for small-mem / cache-macro configurations of MakePublicKey
  • [Medium] ML-DSA secret vectors left unzeroed in wc_MlDsaKey_MakePublicKey under SMALL_MEM + CACHE_PRIV_VECTORS

Review generated by Skoll via Claude/Codex

Comment thread wolfcrypt/src/wc_mldsa.c
Comment thread wolfcrypt/src/asn.c Outdated
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
Comment thread wolfcrypt/src/wc_mldsa.c
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from b6324ad to b72a8ed Compare July 27, 2026 17:05
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch 2 times, most recently from 106f551 to fa80057 Compare July 27, 2026 17:32

@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 #10985

Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src

Findings: 3
3 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/wc_mldsa.c
Comment thread wolfcrypt/src/asn.c Outdated
Comment thread wolfcrypt/src/asn_orig.c Outdated
Comment thread wolfcrypt/src/asn_orig.c Outdated
Comment thread wolfcrypt/src/asn.c Outdated
Comment thread wolfcrypt/src/wc_mldsa.c
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from fa80057 to e292a3c Compare July 27, 2026 17:50

@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 #10985

Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src

Findings: 3
3 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/wc_mldsa.c Outdated
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
Comment thread tests/api/test_mldsa.c
Comment thread tests/api/test_mldsa.c
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from e292a3c to ceb0a7d Compare July 27, 2026 20:48

@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 #10985

Scan targets checked: wolfcrypt-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/wc_mldsa.c
Comment thread wolfcrypt/src/wc_mldsa.c
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from ceb0a7d to 3d9668e Compare July 27, 2026 21:54
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from 3d9668e to 2527a37 Compare July 27, 2026 22:11

@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 #10985

Scan targets checked: wolfcrypt-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/wc_mldsa.c Outdated
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from 3bb246b to 38cd398 Compare July 30, 2026 16:14

@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 #10985

Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src

No new issues found in the changed files. ✅

@stenslae stenslae assigned wolfSSL-Bot and unassigned stenslae Jul 30, 2026

@Frauschi Frauschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🐺 Skoll Code Review

Overall recommendation: REQUEST_CHANGES
Findings: 28 total — 11 posted, 17 skipped

Posted findings

  • [High] Three new ML-DSA tests use certs_test.h buffers without the NO_SIGN/NO_VERIFY guards that declare themtests/api/test_mldsa.c:7769-7770
  • [High] Build break: new mldsa_calc_t_std() calls mldsa_expand_a(), which is not compiled in MAKE_KEY_SMALL_MEM buildswolfcrypt/src/wc_mldsa.c:7767-7788
  • [High] Build break: outer guard for mldsa_vec_decode_eta_bits not widened, so the new CheckKey/MakePublicKey arm is deadwolfcrypt/src/wc_mldsa.c:1279
  • [High] New OOM test matches allocations by exact byte size; WOLFSSL_CHECK_MEM_ZERO inflates every size by 16, failing the all-check-mem-zero CI jobtests/api/test_mldsa.c:8079-8110
  • [Medium] ECC decode-time public key derivation is inert in default hardened buildswolfcrypt/src/asn.c:32632-32637
  • [Medium] Best-effort ML-DSA derivation converts MEMORY_E into BAD_FUNC_ARGwolfcrypt/src/wc_mldsa.c:11951-11959
  • [Medium] MakePublicKey always doubles the matrix work with an unconditional CheckKeywolfcrypt/src/wc_mldsa.c:10627-10687
  • [Medium] WOLFSSL_MLDSA_NO_CHECK_KEY no longer removes wc_MlDsaKey_CheckKeywolfcrypt/src/wc_mldsa.c:11808-11809
  • [Medium] Export/DER functions now mutate key state, breaking their read-only contractwolfcrypt/src/wc_mldsa.c:12044-12049
  • [Medium] No opt-out for the new decode-time ECC scalar multiplywolfcrypt/src/asn.c:32607-32614
  • [Medium] Derived ML-DSA public key is never checked against the stored tr hashwolfcrypt/src/wc_mldsa.c:10622-10623
Skipped findings
  • [Medium] OOM tests leak custom allocators when an earlier assertion fails
  • [Medium] ECC OOM test infers the derivation allocation from an allocation-count delta between two different DER inputs
  • [Medium] Unrelated WOLFSSL_MLDSA_SMALL overflow fixes shipped without dedicated tests
  • [Medium] key->aSet restored to 1 over a partially-overwritten matrix A after a CheckKey failure
  • [Medium] MAKE_KEY_SMALL_MEM derivation self-check never validates the derived key against the private key's stored t0
  • [Medium] ecc_make_pub_ex no longer promotes key->type on WC_PENDING_E, stalling async ECDSA verify with a private-only key
  • [Low] File-static helper uses the reserved wc_ public-API prefix
  • [Low] Duplicate pubKeySet check in wrappers and mldsa_verify_with_mu
  • [Low] Early bare returns in mldsa_verify_with_mu deviate from the file's single-exit style
  • [Low] ECC best-effort public-key derivation is inert in the default hardened build, so no in-tree caller benefits
  • [Low] WOLFSSL_MLDSA_NO_CHECK_KEY is no longer honoured: wc_MlDsaKey_CheckKey is force-compiled and force-executed
  • [Low] wc_MlDsaKey_MakePublicKey holds its full scratch allocation across the CheckKey call, roughly doubling peak heap
  • [Low] NULL dereference in test_mldsa_make_public_key when the key allocation fails
  • [Low] tests/suites.c ML-DSA/SLH-DSA TLS 1.3 blocks gained !WOLFSSL_MLDSA_NO_SIGN but still lack !WOLFSSL_MLDSA_NO_VERIFY
  • [Low] ECC OOM test derives its failure index from a different DER input, so it can pass without exercising the derivation path
  • [Info] Derivation helper retries wc_ecc_make_pub_ex on WC_PENDING_E, contradicting the documented async contract, and its async guard does not match the WC_PENDING_E producer
  • [Info] wc_MlDsaKey_MakePublicKey allocates key->s1 with params->aSz and skips the XMEMSET the import path performs

Review generated by Skoll via Claude/Codex

Comment thread tests/api/test_mldsa.c
Comment thread wolfcrypt/src/wc_mldsa.c
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
Comment thread tests/api/test_mldsa.c Outdated
Comment thread wolfcrypt/src/asn.c
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
Comment thread wolfcrypt/src/wc_mldsa.c
Comment thread wolfcrypt/src/asn.c
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
@Frauschi Frauschi assigned stenslae and unassigned wolfSSL-Bot Jul 30, 2026
@Frauschi Frauschi mentioned this pull request Jul 31, 2026
4 tasks
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch 2 times, most recently from 732b1ea to c189863 Compare August 3, 2026 16:20
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from c189863 to 81bbb62 Compare August 3, 2026 16:48

@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 #10985

Scan targets checked: wolfcrypt-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.

Comment thread wolfcrypt/src/wc_mldsa.c Outdated
Comment thread wolfcrypt/src/asn_orig.c
Comment thread wolfcrypt/src/wc_mldsa.c Outdated
Comment thread wolfcrypt/src/asn_orig.c
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from 81bbb62 to 9e69c56 Compare August 3, 2026 17:35
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from 9e69c56 to afe7a9f Compare August 3, 2026 20:56

@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 #10985

Scan targets checked: wolfcrypt-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/asn.c
@stenslae stenslae removed their assignment Aug 3, 2026
@dgarske
dgarske requested a review from Frauschi August 3, 2026 23:17

@Frauschi Frauschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🐺 Skoll Code Review

Overall recommendation: REQUEST_CHANGES
Findings: 23 total — 20 posted, 3 skipped

Posted findings

  • [High] New ECC derive test's #if is missing four terms, so it fails in the opt-out, no-key-export and crypto-cb-only buildstests/api/test_asn.c:2854-2860
  • [High] test_wc_MldsaDecisionCoverage2 PUBLIC_KEY_E expectation is not gated on the conditions that enable the KeyToDer auto-derive; fails with --enable-mldsa=sign,verifytests/api/test_mldsa.c:31734-31749
  • [High] t0Scratch allocated without the 8-byte AVX2 tail slack, giving a 3-byte heap overflow on every private-only public-key derivationwolfcrypt/src/wc_mldsa.c:10694-10749
  • [High] Fault-harden verify buffers packed back-to-back: 6-byte heap overflow and an always-failing self-check under AVX2wolfcrypt/src/wc_mldsa.c:10777-10813
  • [Medium] wc_MlDsaKey_KeyToDer lost its BAD_FUNC_ARG default: an unrecognised key->level now returns 0wolfcrypt/src/wc_mldsa.c:13436-13488
  • [Medium] Best-effort ECC public-key derivation is a no-op in every default (hardened) buildwolfcrypt/src/asn.c:33143-33150
  • [Medium] Unconditional extra base-point scalar multiply on every ECC private-key decode, including paths that discard the key immediatelywolfcrypt/src/asn.c:33290-33294
  • [Medium] EccDerivePubBestEffort is duplicated verbatim into asn_orig.c although asn_orig.c is #included into asn.c (same translation unit)wolfcrypt/src/asn_orig.c:7598-7654
  • [Medium] Async ECC derive: retry loop keys off the wrong enable macro, and ecc_make_pub_ex no longer upgrades key->type on WC_PENDING_Ewolfcrypt/src/asn.c:33152-33166
  • [Medium] ECC OOM sub-test derives its baseline allocation count from a different DER blob, so the whole block can silently no-optests/api/test_asn.c:2957-3001
  • [Medium] WC_MLDSA_FAULT_HARDEN silently overrides WOLFSSL_MLDSA_NO_CHECK_KEY, and wc_mldsa.h re-derives the condition instead of consuming the new macrowolfcrypt/src/wc_mldsa.c:254-263
  • [Medium] Widened CheckKey guard newly exposes a NULL key->a dereference to configurations where CheckKey previously did not existwolfcrypt/src/wc_mldsa.c:12036-12066
  • [Medium] Coverage gaps: PublicKeyToDer auto-derive, ML-DSA 65/87 OOM, cached-vector invalidation, MakePublicKey small-mem and fault-harden pathstests/api/test_mldsa.c:7761-8290
  • [Medium] Under WOLF_CRYPTO_CB_FIND the devId gate is ineffective, so every SEC1 private-key decode fires an unsolicited EC_MAKE_PUB crypto callbackwolfcrypt/src/asn.c:33136-33141
  • [Low] Documentation gaps for the new decode-time derivation: undocumented macro, inaccurate guard comment, un-mirrored Japanese doxygenwolfcrypt/src/asn.c:33118
  • [Low] MakePublicKey invalidates the private-vector cache in MAKE_KEY_SMALL_MEM builds where it never touched itwolfcrypt/src/wc_mldsa.c:10715-10720
  • [Low] Unguarded struct pokes dereference key after a failed allocation or a skipped Inittests/api/test_mldsa.c:7787-7789
  • [Low] test_mldsa_make_public_key_tr_mismatch comment claims to exercise the WC_MLDSA_FAULT_HARDEN recheck, which that path can never reachtests/api/test_mldsa.c:8248-8253
  • [Low] ECC private-key decode now leaves an unblinded plaintext copy of the private scalar in key->ku (WOLFSSL_ECC_BLIND_K builds)wolfcrypt/src/asn.c:33153-33165
  • [Info] *New PUBLIC_KEY_E early return in mldsa_verify_with_mu leaves res untouched, unlike every other error path in the functionwolfcrypt/src/wc_mldsa.c:9837-9839
Skipped findings
  • [Low] Coding-standard violations: comment punctuation, over-80-column lines and new bare scope blocks
  • [Low] WC_RNG is not zeroed before use, so the unconditional wc_FreeRng at the end operates on an uninitialised stack struct when wc_InitRng fails
  • [Info] Fault-harden branch documents a false invariant about matrix A after a failed CheckKey, and its aSet assignment is dead code

Review generated by Skoll via Claude/Codex

Comment thread tests/api/test_asn.c
int test_wc_EccPrivateKeyDecode_derive_pub(void)
{
EXPECT_DECLS;
#if !defined(NO_ASN) && defined(HAVE_ECC) && !defined(NO_ECC_MAKE_PUB) && \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [High] New ECC derive test's #if is missing four terms, so it fails in the opt-out, no-key-export and crypto-cb-only builds
🚫 BLOCK test

Sites: tests/api/test_asn.c:2854-2860; tests/api/test_asn.c:2883-2887; tests/api/test_asn.c:2853-2860; tests/api/test_asn.c:2853-2917
The production helper is compiled only when WOLFSSL_ECC_DERIVE_PUB_BEST_EFFORT is defined, and its #if (wolfcrypt/src/asn.c:33116-33122 and wolfcrypt/src/asn_orig.c:7601-7607) starts with !defined(NO_ECC_MAKE_PUB) && !defined(WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE) && .... The new test copies 12 of the 13 terms (NO_ECC_MAKE_PUB, ATECC508A/608A, TA100, CRYPTOCELL, SILABS, KCAPI, QNX_CAAM, IMXRT1170_CAAM, HAVE_FIPS, HAVE_SELFTEST) but silently drops the new opt-out macro. With WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE defined — the only documented way to turn the new behaviour off — wc_EccPrivateKeyDecode() never calls EccDerivePubBestEffort(), the key stays ECC_PRIVATEKEY_ONLY, and the test still runs and asserts ExpectIntEQ(privOnlyKey.type, ECC_PRIVATEKEY) at lines 2898 and 2917. Guaranteed test failure in a supported configuration introduced by this same PR.

Also, at tests/api/test_asn.c:2883-2887 (High, review scan) — New ECC derive test calls wc_EccPrivateKeyToDer/wc_ecc_export_x963 without requiring HAVE_ECC_KEY_EXPORT:

wc_EccPrivateKeyToDer is defined inside #ifdef HAVE_ECC_KEY_EXPORT (wolfcrypt/src/asn.c:33400-33674) and wc_ecc_export_x963 is declared inside #ifdef HAVE_ECC_KEY_EXPORT (wolfssl/wolfcrypt/ecc.h:931-940). settings.h:3589-3592 only defines HAVE_ECC_KEY_EXPORT when NO_ECC_KEY_EXPORT is absent, and NO_ECC_KEY_EXPORT is a shipped option used by examples/configs/user_settings_min_ecc.h:90 and examples/configs/user_settings_baremetal.h:111. The new test's #if permits HAVE_ECC && USE_CERT_BUFFERS_256 with NO_ECC_KEY_EXPORT set, in which case tests/api/test_asn.c no longer compiles (implicit declaration) or fails to link. No other test in this file uses either symbol, so there is no existing guard being inherited.

Also, at tests/api/test_asn.c:2853-2860 (Medium, bugs scan) — New ECC derive test's #if omits WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE, so the PR's own opt-out macro fails its own test:

This PR introduces WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE as the documented opt-out for the new derive-on-decode behaviour: it appears in the WOLFSSL_ECC_DERIVE_PUB_BEST_EFFORT feature guard in both asn.c:33118 and asn_orig.c:7601, and defining it compiles the helper and its call site out entirely.

The new test's #if block mirrors HAVE_ECC_MAKE_PUB's exclusion list (NO_ECC_MAKE_PUB, ATECC508A/608A, TA100, CRYPTOCELL, SILABS_SE_ACCEL, KCAPI_ECC, QNX/IMXRT CAAM, FIPS, selftest) but does not exclude WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE.

So in an opt-out build the test still compiles and runs, wc_EccPrivateKeyDecode() correctly leaves the key as ECC_PRIVATEKEY_ONLY, and the unconditional assertion ExpectIntEQ(privOnlyKey.type, ECC_PRIVATEKEY); at test_asn.c:2917 fails, along with the export/compare block that follows it. The #ifndef ECC_TIMING_RESISTANT branch at test_asn.c:2898 fails the same way.

Also raised by the bugs scan (tests/api/test_asn.c:2853-2860):

The test calls wc_ecc_export_x963() at test_asn.c:2884, 2900 and 2919. That prototype is declared inside #ifdef HAVE_ECC_KEY_EXPORT (wolfssl/wolfcrypt/ecc.h:932-940). HAVE_ECC_KEY_EXPORT is auto-defined by settings.h:3589-3593 unless the user sets NO_ECC_KEY_EXPORT.

The test's guard does not include defined(HAVE_ECC_KEY_EXPORT), so a NO_ECC_KEY_EXPORT build compiles the test body against an undeclared function — an implicit-declaration error under C99+/-Werror. Narrow config, but it is a supported opt-out.

Also, at tests/api/test_asn.c:2853-2917 (Low, bugs scan) — New ECC derive test's #if omits WOLF_CRYPTO_CB_ONLY_ECC, where the derive can never succeed:

The test body mirrors HAVE_ECC_MAKE_PUB's own exclusion list (ecc.c:322-328) but does not exclude WOLF_CRYPTO_CB_ONLY_ECC.

In a WOLF_CRYPTO_CB_ONLY_ECC build HAVE_ECC_MAKE_PUB is still defined, so the helper compiles, but ecc_make_pub_ex()'s software branch is replaced by err = NO_VALID_DEVID (ecc.c:5736-5743) for any key with INVALID_DEVID and no device that claims the op. The derive therefore always fails, the key stays ECC_PRIVATEKEY_ONLY, and the assertion at test_asn.c:2917 (ExpectIntEQ(privOnlyKey.type, ECC_PRIVATEKEY);, reached after wc_ecc_set_rng() at 2911 so the RNG gate is satisfied) fails, together with the export/compare block that follows.

Suggestion:

Suggested change
#if !defined(NO_ASN) && defined(HAVE_ECC) && !defined(NO_ECC_MAKE_PUB) && \
#if !defined(NO_ASN) && defined(HAVE_ECC) && !defined(NO_ECC_MAKE_PUB) && \
!defined(WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE) && \
defined(HAVE_ECC_KEY_EXPORT) && \
defined(USE_CERT_BUFFERS_256) && !defined(HAVE_FIPS) && \

Recommendation: Add !defined(WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE) to the test guard. Better: publish WOLFSSL_ECC_DERIVE_PUB_BEST_EFFORT from a public header (e.g. wolfssl/wolfcrypt/asn_public.h) so the test keys off one symbol instead of duplicating a 13-term guard that has already drifted once.

[tests/api/test_asn.c:2883-2887] Add defined(HAVE_ECC_KEY_EXPORT) to the test's #if guard (and to the OOM-callback definition guard above it, which shares the same term list).

[tests/api/test_asn.c:2853-2860] Add !defined(WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE) to the test's #if. Cleaner still: promote WOLFSSL_ECC_DERIVE_PUB_BEST_EFFORT out of asn.c/asn_orig.c into a header so the test keys off a single symbol instead of re-deriving a 12-term guard that must be kept in sync in three places.

[tests/api/test_asn.c:2853-2917] Add !defined(WOLF_CRYPTO_CB_ONLY_ECC) to the test's #if and to the WOLFSSL_ECC_DERIVE_PUB_BEST_EFFORT guard in asn.c and asn_orig.c.

Comment thread tests/api/test_mldsa.c
@@ -31203,15 +31734,19 @@ int test_wc_MldsaDecisionCoverage2(void)
#if !defined(WOLFSSL_MLDSA_NO_ASN1) && defined(WOLFSSL_MLDSA_PUBLIC_KEY)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [High] test_wc_MldsaDecisionCoverage2 PUBLIC_KEY_E expectation is not gated on the conditions that enable the KeyToDer auto-derive; fails with --enable-mldsa=sign,verify
🚫 BLOCK test

wc_MlDsaKey_KeyToDer() only auto-derives under #if !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && !defined(WOLFSSL_MLDSA_ASSIGN_KEY) && !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) (wolfcrypt/src/wc_mldsa.c:13442-13447), but the changed assertion sits in a block guarded only by !defined(WOLFSSL_MLDSA_NO_ASN1) && defined(WOLFSSL_MLDSA_PUBLIC_KEY) nested in defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_NO_ML_DSA_44). Per dilithium.h:317-330, WOLFSSL_MLDSA_NO_MAKE_KEY alone leaves both WOLFSSL_MLDSA_PRIVATE_KEY and WOLFSSL_MLDSA_PUBLIC_KEY defined and does not derive WOLFSSL_MLDSA_VERIFY_ONLY, so the block still compiles but KeyToDer falls straight to the prvKeySet && pubKeySet check and returns BAD_FUNC_ARG, not PUBLIC_KEY_E. configure.ac:2153 initialises ENABLED_MLDSA_MAKE_KEY=no and configure.ac:8156-8158 emits -DWOLFSSL_MLDSA_NO_MAKE_KEY, so ./configure --enable-mldsa=sign,verify is exactly that configuration. A WOLFSSL_MLDSA_ASSIGN_KEY user_settings build fails the same way.

Also raised by the bugs scan (tests/api/test_mldsa.c:31733-31750):

The PR changes the expected result of wc_MlDsaKey_KeyToDer() on a private-only key from BAD_FUNC_ARG to PUBLIC_KEY_E, on the reasoning that KeyToDer now auto-derives the public key and the zero-filled test key fails the 'tr' hash compare.

That reasoning only holds when the auto-derive is compiled in. The derive block in wc_MlDsaKey_KeyToDer() (wc_mldsa.c:13442-13447) is guarded by !WOLFSSL_MLDSA_VERIFY_ONLY && !WOLFSSL_MLDSA_ASSIGN_KEY && !WOLFSSL_MLDSA_NO_MAKE_KEY. The test block is guarded only by !defined(WOLFSSL_MLDSA_NO_ASN1) && defined(WOLFSSL_MLDSA_PUBLIC_KEY).

Concrete failing config: define WOLFSSL_MLDSA_NO_MAKE_KEY with sign and verify left enabled (a sign+verify, no-keygen build). Then:

  • WOLFSSL_MLDSA_PUBLIC_KEY is still defined, via !NO_MAKE_KEY || !NO_VERIFY at dilithium.h:317-319, so the test block compiles and runs.
  • wc_MlDsaKey_KeyToDer() skips the derive, key->pubKeySet stays 0, and control reaches the else { ret = BAD_FUNC_ARG; } at wc_mldsa.c:13483.
  • The test asserts PUBLIC_KEY_E and fails.

The same applies to WOLFSSL_MLDSA_ASSIGN_KEY builds.

Suggestion:

Suggested change
#if !defined(WOLFSSL_MLDSA_NO_ASN1) && defined(WOLFSSL_MLDSA_PUBLIC_KEY)
#if !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && \
!defined(WOLFSSL_MLDSA_ASSIGN_KEY) && !defined(WOLFSSL_MLDSA_NO_MAKE_KEY)
ExpectIntEQ(wc_MlDsaKey_KeyToDer(&key, der, (word32)sizeof(der)),
WC_NO_ERR_TRACE(PUBLIC_KEY_E));
#else
ExpectIntEQ(wc_MlDsaKey_KeyToDer(&key, der, (word32)sizeof(der)),
WC_NO_ERR_TRACE(BAD_FUNC_ARG));
#endif

Recommendation: Select the expected value with a nested #if mirroring the auto-derive guard, keeping the BAD_FUNC_ARG assertion in the #else arm so both configurations stay covered.

Comment thread wolfcrypt/src/wc_mldsa.c
}
#endif /* WC_MLDSA_FIXED_ARRAY && WC_MLDSA_CACHE_PRIV_VECTORS && !SMALL_MEM */

if (ret == 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [High] t0Scratch allocated without the 8-byte AVX2 tail slack, giving a 3-byte heap overflow on every private-only public-key derivation
💡 SUGGEST

The new wc_MlDsaKey_MakePublicKey() allocates the t0 encoding scratch buffer at exactly the logical encoded size and hands it to mldsa_vec_encode_t0_t1():

t0ScratchSz = (unsigned int)params->k * (MLDSA_D * MLDSA_N / 8);   /* k * 416 */
t0Scratch   = (byte*)XMALLOC(t0ScratchSz, key->heap, DYNAMIC_TYPE_MLDSA);
...
mldsa_vec_encode_t0_t1(t, params->k, t0Scratch, t1);

t0ScratchSz is correct for the C encoder (mldsa_vec_encode_t0_t1_c, wc_mldsa.c:1382, writes 13 bytes per 8 coefficients = 416 bytes/poly). But under USE_INTEL_SPEEDUP the dispatcher at wc_mldsa.c:1490-1503 routes to wc_mldsa_vec_encode_t0_t1_avx2(), which emits a 16-byte store for every 13-byte group and advances only 13 (wolfcrypt/src/wc_mldsa_asm.S:21860-21863):

vmovdqu %xmm2, (%rdx)
addq    $13, %rdx

I walked the asm loop to its back-edge (addq $0x400,%rdi; jnz L_mldsa_encode_t0_t1_avx2_loop, wc_mldsa_asm.S:~22540) and there is no tail special-case: the final store of the final polynomial lands at t0Scratch + k*416 - 13 and writes 16 bytes, i.e. 3 bytes past the allocation. For ML-DSA-44 (k=4): size 1664, last store covers offsets 1651..1666. The spilled bytes are zero because lanes 13-15 of L_mldsa_encode_t0_t1_avx2_shuff_13_even/_odd are 0xff (wc_mldsa_asm.S:21752-21771), so it is a 3-byte zero-write past the heap chunk.

This is exactly the over-store that the rest of the file already compensates for: byte k[MLDSA_MAX_KEY_SIZE+8] / byte p[MLDSA_MAX_PUB_KEY_SIZE+8] under USE_INTEL_SPEEDUP (wc_mldsa.h:615-623), and secSz += 8; / pubSz += 8; in mldsa_alloc_priv_buf() / mldsa_alloc_pub_buf() (wc_mldsa.c:423-425, 457-459). t0Scratch is the first t0 destination introduced without that slack. The t1 destination here (key->p + 32) is safe because key->p already carries the +8.

Trigger: any --enable-intelasm x86-64 build with AVX2 at runtime that decodes a private-only ML-DSA key and then calls wc_MlDsaKey_ExportPubRaw(), wc_MlDsaKey_PublicKeyToDer() or wc_MlDsaKey_KeyToDer() — all three now auto-derive. ASAN or a hardened allocator will trap.

Recommendation: Allocate the same 8-byte tail slack the key buffers already carry:

        t0Scratch = (byte*)XMALLOC(t0ScratchSz + 8, key->heap,
            DYNAMIC_TYPE_MLDSA);

and widen the cleanup to ForceZero(t0Scratch, t0ScratchSz + 8);. Gating the +8 on USE_INTEL_SPEEDUP would also work, but unconditional padding matches how the static arrays are declared.

Comment thread wolfcrypt/src/wc_mldsa.c
#ifdef WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM
/* CheckKey() expands full matrix A, defeating small-mem.
* Re-derive streaming and compare t0/t1 bytes instead. */
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 [High] Fault-harden verify buffers packed back-to-back: 6-byte heap overflow and an always-failing self-check under AVX2
💡 SUGGEST

The WC_MLDSA_FAULT_HARDEN + WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM re-derivation path packs the t0 and t1 verification buffers into a single allocation with no slack between or after them:

t1Sz     = (unsigned int)params->pkSz - MLDSA_PUB_SEED_SZ;   /* k * 320 */
t0Verify = (byte*)XMALLOC(t0ScratchSz + t1Sz, key->heap, DYNAMIC_TYPE_MLDSA);
t1Verify = t0Verify + t0ScratchSz;
...
mldsa_vec_encode_t0_t1(t, params->k, t0Verify, t1Verify);

Both sizes are logically right, but the AVX2 encoder writes each 13-byte t0 group and each 10-byte t1 group with a 16-byte vmovdqu (wc_mldsa_asm.S:21860/21876). Two distinct defects follow, both provable by offset arithmetic. For ML-DSA-44 (k=4, t0ScratchSz=1664, t1Sz=1280, allocation 2944, valid 0..2943):

  1. Trailing overflow. The last t1 store is at t1Verify + 1280 - 10 = offset 2934 and writes 16 bytes, covering 2934..2949 — 6 bytes past the end of the allocation.

  2. Cross-buffer clobber, so the check can never pass. The last t0 store is at offset 1664 - 13 = 1651 and writes 16 bytes covering 1651..1666. t1Verify starts at 1664, so offsets 1664..1666 (t1Verify[0..2]) are overwritten with the zero-padded tail of the t0 lane. Because the asm iterates polynomial-by-polynomial and stores t0 before t1 within each group, this final t0 spill happens long after t1Verify[0..2] were legitimately written in the first polynomial. The subsequent XMEMCMP(t1Verify, t1, t1Sz) therefore compares three zeroed bytes against the real packed t1 bytes and fails unless t1[0..2] happen to be zero (~2^-24). Net effect: in an AVX2 build with WC_MLDSA_FAULT_HARDEN and WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM, wc_MlDsaKey_MakePublicKey() returns PUBLIC_KEY_E for a perfectly valid key — the opt-in fault-hardening check is effectively always-fail.

No pre-existing caller places a t0 and a t1 destination adjacently or without the +8 slack, so both issues are new in this diff.

Recommendation: Pad both sub-buffers so the 16-byte stores stay inside the allocation and t0 cannot bleed into t1:

                t0Verify = (byte*)XMALLOC(t0ScratchSz + 8 + t1Sz + 8,
                    key->heap, DYNAMIC_TYPE_MLDSA);
                ...
                t1Verify = t0Verify + t0ScratchSz + 8;
                ...
                ForceZero(t0Verify, t0ScratchSz + 8 + t1Sz + 8);

Two separate XMALLOCs work equally well provided each carries the 8-byte tail slack.

Comment thread wolfcrypt/src/wc_mldsa.c
int wc_MlDsaKey_KeyToDer(wc_MlDsaKey* key, byte* output, word32 len)
{
int ret = WC_NO_ERR_TRACE(BAD_FUNC_ARG);
int ret = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 [Medium] wc_MlDsaKey_KeyToDer lost its BAD_FUNC_ARG default: an unrecognised key->level now returns 0
💡 SUGGEST bug

The initializer changed from int ret = WC_NO_ERR_TRACE(BAD_FUNC_ARG); to int ret = 0; so the derive guard could run. But the if (key->level == WC_ML_DSA_44) ... else if 65 ... else if 87 chain has no terminating else, and the previous code relied on the BAD_FUNC_ARG initializer as the default. With the new initializer, a key that has prvKeySet && pubKeySet but a level outside {44, 65, 87} (and no matching FIPS204-draft level) falls through every branch and the function returns 0. Since SetAsymKeyDer() returns a positive byte count on success, a return of 0 is indistinguishable from 'encoded 0 bytes' for the standard if (ret < 0) fail; caller pattern — a size-query caller would then allocate nothing and a write caller would treat an empty DER as valid. Reachability is low (the import paths reject bad levels), but this removes a defensive default that was there before this diff. Note wc_MlDsaKey_ExportPubRaw at wc_mldsa.c:12193-12196 keeps exactly such an else { ret = BAD_FUNC_ARG; } arm, so the convention in this file is to have one.

Also, at wolfcrypt/src/wc_mldsa.c:13436-13488 (Low, bugs scan) — wc_MlDsaKey_KeyToDer lost its BAD_FUNC_ARG default: an unmatched key->level now returns 0 (success, zero bytes):

The restructure changed the initializer from int ret = WC_NO_ERR_TRACE(BAD_FUNC_ARG); to int ret = 0; so the new derive step can run first, and wrapped the body in if (ret == 0) { if (key && prvKeySet && pubKeySet) { <level chain> } else { ret = BAD_FUNC_ARG; } }.

The added else { ret = BAD_FUNC_ARG; } only covers the outer condition. The inner level dispatch chain at 13470-13481 (if (level == WC_ML_DSA_44) ... else if (65) ... else if (87)) has no terminal else, so when none matches, ret is still 0 and the function returns 0 with output untouched. The documented contract is 'Size of the encoded DER in bytes on success', and every in-tree caller uses ExpectIntGT(..., 0) or assigns the result to a length — a return of 0 reads as 'succeeded, wrote 0 bytes' rather than an error. Before this commit the BAD_FUNC_ARG initializer made that path a hard error.

All three sibling exporters kept their safety net: wc_MlDsaKey_ExportPubRaw (wc_mldsa.c:12193-12196), wc_MlDsaKey_PublicKeyToDer (13393-13396) and wc_MlDsaKey_PrivateKeyToDer (still int ret = WC_NO_ERR_TRACE(BAD_FUNC_ARG);) all terminate the same chain with else { /* Level not set. */ ret = BAD_FUNC_ARG; }. KeyToDer is now the lone outlier.

Reachability is narrow and I state it honestly: key->level is assigned in exactly one place, wc_MlDsaKey_SetParams (wc_mldsa.c:11662, key->level = level % WC_ML_DSA_DRAFT;), and only after mldsa_get_params() has accepted the level, so a key built through the public API always holds 2/3/5. This is a removed defensive default rather than a currently exploitable path — but it is a real behavioural change in this diff.

Suggestion:

Suggested change
int ret = 0;
else if (key->level == WC_ML_DSA_87) {
ret = SetAsymKeyDer(key->k, WC_MLDSA_87_KEY_SIZE, key->p,
WC_MLDSA_87_PUB_KEY_SIZE, output, len, ML_DSA_87k);
}
else {
/* Level not set. */
ret = BAD_FUNC_ARG;
}

Recommendation: Add a terminating else { ret = BAD_FUNC_ARG; } to the level chain, matching wc_MlDsaKey_ExportPubRaw.

[wolfcrypt/src/wc_mldsa.c:13436-13488] Restore the safety net, matching wc_MlDsaKey_PublicKeyToDer:

        else if (key->level == WC_ML_DSA_87) {
            ret = SetAsymKeyDer(key->k, WC_MLDSA_87_KEY_SIZE, key->p,
                WC_MLDSA_87_PUB_KEY_SIZE, output, len, ML_DSA_87k);
        }
        else {
            /* Level not set. */
            ret = BAD_FUNC_ARG;
        }

Comment thread wolfcrypt/src/wc_mldsa.c
mldsa_vec_decode_eta_bits(s1p, params->eta, s1, params->l);
mldsa_vec_decode_eta_bits(s2p, params->eta, s2, params->k);

#ifdef WC_MLDSA_CACHE_PRIV_VECTORS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 [Low] MakePublicKey invalidates the private-vector cache in MAKE_KEY_SMALL_MEM builds where it never touched it
🔧 NIT bug

Every other WC_MLDSA_CACHE_PRIV_VECTORS block in this function is written as #if defined(WC_MLDSA_CACHE_PRIV_VECTORS) && !defined(WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM) (lines 10592-10612 and the cleanup at 10853-10871), because in small-mem mode s1/s2/t come from the locally allocated allocPtr (10679-10688) and key->s1/key->s2/key->t0 are never written. The privVecsSet invalidation is gated on plain #ifdef WC_MLDSA_CACHE_PRIV_VECTORS, so with WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM + WC_MLDSA_CACHE_PRIV_VECTORS it clears a cache whose contents are still valid, and the comment above it ('s1/s2/t0 are overwritten here (wrong domain)') is false for that configuration. Not a memory-safety issue — privVecsSet exists whenever WC_MLDSA_CACHE_PRIV_VECTORS is set, independently of small-mem — the only effect is that the next sign recomputes the NTT-domain vectors unnecessarily.

Also raised by the bugs scan (wolfcrypt/src/wc_mldsa.c:10715-10720):

The scratch buffers are aliased onto the private-vector cache only under the pair WC_MLDSA_CACHE_PRIV_VECTORS && !WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM — see the reuse block at wc_mldsa.c:10592-10612, guarded exactly that way. Under WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM, s1/s2/t come from the locally malloc'd allocPtr (10678-10688) and key->s1 / key->s2 / key->t0 are never written.

But the invalidation is guarded by a bare #ifdef WC_MLDSA_CACHE_PRIV_VECTORS. So in a WC_MLDSA_CACHE_PRIV_VECTORS + WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM build, wc_MlDsaKey_MakePublicKey() throws away a still-valid cache that wc_MlDsaKey_ImportPrivRaw() just populated via mldsa_make_priv_vecs() (wc_mldsa.c:12508). The next wc_MlDsaKey_SignCtx() then re-runs the full decode + NTT of s1/s2/t0, because mldsa_sign_with_seed_mu() only recomputes them if (!key->privVecsSet) (wc_mldsa.c:8585).

No wrong results — just a silent per-key performance regression, and a guard that no longer matches the guard on the aliasing it exists to compensate for, which is the kind of drift that turns into a correctness bug on the next edit.

Suggestion:

Suggested change
#ifdef WC_MLDSA_CACHE_PRIV_VECTORS
#if defined(WC_MLDSA_CACHE_PRIV_VECTORS) && \
!defined(WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM)
key->privVecsSet = 0;
#endif

Recommendation: Match the other blocks in the same function by adding && !defined(WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM).

Comment thread tests/api/test_mldsa.c
ExpectIntEQ(wc_MlDsaKey_MakePublicKey(key), WC_NO_ERR_TRACE(BAD_FUNC_ARG));

/* Public key set but private key still not set - still BAD_FUNC_ARG. */
key->pubKeySet = 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 [Low] Unguarded struct pokes dereference key after a failed allocation or a skipped Init
🔧 NIT test

key->pubKeySet = 1; and key->pubKeySet = 0; are plain statements, not wrapped in Expect* and not inside the if (key != NULL) pattern used a few lines above in the same test. Because Expect* short-circuits after any prior failure (tests/unit.h:184-195), a failed XMALLOC makes ExpectNotNull(key) fail, all following ExpectIntEQ calls become no-ops, and these two assignments dereference NULL. Same class at tests/api/test_mldsa.c:8279-8281 in test_mldsa_make_public_key_tr_mismatch: if (key != NULL) { key->k[...] ^= 0xFF; } checks key but not key->k, and under WOLFSSL_MLDSA_DYNAMIC_KEYS key->k is a heap pointer that is still NULL if the preceding ExpectIntEQ(wc_MlDsaKey_ImportPrivRaw(...)) was skipped.

Also raised by the bugs scan (tests/api/test_mldsa.c:7787-7789):

key is heap-allocated at test_mldsa.c:7769 and only checked via ExpectNotNull(key) at 7770. Every Expect* macro short-circuits on prior failure (Expect() wraps its body in if (EXPECT_SUCCESS()), tests/unit.h:184-195), and the XMEMSET that follows is explicitly guarded by if (key != NULL).

But these two statements are bare assignments, not Expect macros, so they run unconditionally and dereference a possibly-NULL pointer:

key->pubKeySet = 1;
ExpectIntEQ(wc_MlDsaKey_MakePublicKey(key), WC_NO_ERR_TRACE(BAD_FUNC_ARG));
key->pubKeySet = 0;

Only reachable under allocation failure, hence Low. Note the sibling test test_mldsa_make_public_key_tr_mismatch does get this right — its key->k[...] ^= 0xFF; poke is wrapped in if (key != NULL).

Suggestion:

Suggested change
key->pubKeySet = 1;
if (EXPECT_SUCCESS() && (key != NULL)) {
key->pubKeySet = 1;
ExpectIntEQ(wc_MlDsaKey_MakePublicKey(key),
WC_NO_ERR_TRACE(BAD_FUNC_ARG));
key->pubKeySet = 0;
}

Recommendation: Wrap both assignments in if (key != NULL) (or if (EXPECT_SUCCESS() && key != NULL)), and in the tr_mismatch test guard on key->prvKeySet rather than just key != NULL.

Comment thread tests/api/test_mldsa.c
return EXPECT_RESULT();
}

/* Corrupt the 'tr' hash embedded in a private-only key blob so the derived

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 [Low] test_mldsa_make_public_key_tr_mismatch comment claims to exercise the WC_MLDSA_FAULT_HARDEN recheck, which that path can never reach
🔧 NIT test

The WC_MLDSA_FAULT_HARDEN block in wc_MlDsaKey_MakePublicKey is entered only under if (ret == 0) after the tr compare (wolfcrypt/src/wc_mldsa.c:10772-10773). Because this test deliberately forces the tr compare to fail, the recheck is skipped entirely, so the test only shows that ret is still PUBLIC_KEY_E — never that the recheck runs or that it 'must not override' anything. Nothing in the diff exercises the FAULT_HARDEN success path or its PUBLIC_KEY_E/MEMORY_E arms.

Recommendation: Reword the comment to say the recheck is deliberately not reached on this path, and add a separate case (authentic key in a WC_MLDSA_FAULT_HARDEN build) that actually drives the recheck.

Comment thread wolfcrypt/src/asn.c
}
#endif

do {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 [Low] ECC private-key decode now leaves an unblinded plaintext copy of the private scalar in key->ku (WOLFSSL_ECC_BLIND_K builds)
🔧 NIT

The PR adds a best-effort public-key derivation to wc_EccPrivateKeyDecode() via the new EccDerivePubBestEffort() helper, which calls wc_ecc_make_pub_ex(). In builds with WOLFSSL_ECC_BLIND_K, ecc_make_pub_sw() reaches the scalar exclusively through ecc_get_k(), whose body is mp_xor_ct(key->k, key->kb, key->dp->size, key->ku); return key->ku; (wolfcrypt/src/ecc.c:375-379). That unblinds the scalar into the persistent key->ku member of the ecc_key struct. Nothing on the make-pub path re-blinds or clears ku; the only wipe is mp_forcezero(key->ku) in wc_ecc_free() (wolfcrypt/src/ecc.c:8568-8570). Before this commit, wc_EccPrivateKeyDecode() ended at wc_ecc_import_private_key_ex(), which stores the scalar blinded via ecc_blind_k_rng() (ecc.c:12231-12233) and touches only key->k and key->kbku was left in its zero-initialized state (mp_init_multi/alt_fp_init, ecc.c:6470-6486). So the diff newly makes every SEC1 private-key decode leave the plaintext scalar resident in the key struct for the key's entire lifetime, defeating the purpose of k-blinding for keys that are loaded but not yet used. The residue is created even when derivation fails: the first statement of ecc_make_pub_sw() is the mp_iszero(ecc_get_k(key)) range check, so ku is populated before any error return. Reachability: WOLFSSL_ECC_BLIND_K with ECC_TIMING_RESISTANT off (derive always runs), or with it on when the caller set an RNG via wc_ecc_set_rng() before decoding — a sequence the PR's own new test test_wc_EccPrivateKeyDecode_derive_pub performs. The identical helper is duplicated at wolfcrypt/src/asn_orig.c:7611-7676 (call site asn_orig.c:7787), so the non-template build is affected the same way. Honest caveat: a later software sign or ECDH on the same key also calls ecc_get_k() and leaves the same residue (ecc_sign_hash_sw re-blinds k/kb but not ku), so for keys that are actually used this widens the exposure window rather than creating a wholly new one; for decoded-but-unused keys it is new. Consequence: a core dump, swap page, or heap-disclosure bug yields the plaintext ECDSA private scalar that a WOLFSSL_ECC_BLIND_K build was specifically configured to keep out of plaintext memory. Limitation: the sandbox denied compiler invocation, so this is source-level analysis only — no assembly or runtime confirmation.

Recommendation: Scrub the unblinded scratch scalar once the derive completes. Preferred fix in wolfcrypt/src/ecc.c: at every exit of ecc_make_pub_sw() (success and error) add #ifdef WOLFSSL_ECC_BLIND_K\n mp_forcezero(key->ku);\n#endif, which also clears the analogous residue left by sign/ECDH. If the fix must stay local to this PR, add the same guarded mp_forcezero(key->ku) at the end of EccDerivePubBestEffort() in both wolfcrypt/src/asn.c (after the if (pubRet != 0) log block, ~line 33170) and wolfcrypt/src/asn_orig.c (~line 7676), so a decoded-but-unused key does not carry the plaintext scalar for its lifetime.

Comment thread wolfcrypt/src/wc_mldsa.c
int valid = 0;
sword32 hi;

if (!key->pubKeySet) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

*⚪ [Info] New PUBLIC_KEY_E early return in mldsa_verify_with_mu leaves res untouched, unlike every other error path in the function
🔧 NIT

Both variants of mldsa_verify_with_mu() gained if (!key->pubKeySet) { return PUBLIC_KEY_E; } as the first statement (wc_mldsa.c:9837-9839 non-small-mem, 10008-10010 small-mem). The guard itself is a genuine fix — under WOLFSSL_MLDSA_DYNAMIC_KEYS, key->p is NULL when pubKeySet == 0 and the code immediately below dereferences it — and nothing is allocated at that point, so no cleanup is skipped.

The wrinkle is the out-parameter. Every other failure exit (BUFFER_E from the sigLen check, SIG_VERIFY_E from mldsa_check_hint(), MEMORY_E from the XMALLOC) falls through to the common tail *res = valid; with valid initialised to 0, so *res is always driven to 0. The new return PUBLIC_KEY_E; bypasses that tail, and the four wc_-level guards (wc_mldsa.c:11263, 11319, 11381, 11423) likewise set ret = PUBLIC_KEY_E without touching *res. A caller that seeds res = 1 and only inspects the out-parameter would read a stale 1.

I checked every in-tree consumer and none is affected: src/tls13.c:11763 (int res = 0; plus a ret check), pkcs7.c:5637, asn.c:18191/18200 (via ConfirmSignature), and the FIPS PCT in wc_mldsa.c. Reporting as Info: a latent API-contract inconsistency, not an exploitable defect today.

Recommendation: Drive the out-parameter before returning, in both variants:

if (!key->pubKeySet) {
    *res = 0;
    return PUBLIC_KEY_E;
}

and consider the same in the four wc_MlDsaKey_Verify*/VerifyMu guards so the API never leaves *res at a caller-supplied value on a hard error.

@Frauschi Frauschi assigned stenslae and unassigned wolfSSL-Bot Aug 4, 2026
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