Skip to content

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

Open
stenslae wants to merge 4 commits 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 4 commits 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 +76 B (+0.1%, 64,331 B / 262,144 B, total: 25% used)

gcc-arm-cortex-m3

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

gcc-arm-cortex-m4

  • FLASH: .text +64 B (+0.0%, 201,173 B / 262,144 B, total: 77% 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-dtls13

  • FLASH: .text +64 B (+0.0%, 181,988 B / 1,048,576 B, total: 17% 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,908 B / 1,048,576 B, total: 74% used)

gcc-arm-cortex-m4-pkcs7

  • FLASH: .text +64 B (+0.0%, 213,972 B / 262,144 B, total: 82% used)

gcc-arm-cortex-m4-pq

  • FLASH: .rodata -32 B, .text +832 B (+0.3%, 281,888 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-m4-tls12

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

gcc-arm-cortex-m4-tls13

  • FLASH: .text +64 B (+0.0%, 237,319 B / 262,144 B, total: 91% used)

gcc-arm-cortex-m7

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

gcc-arm-cortex-m7-pq

  • FLASH: .rodata -32 B, .text +832 B (+0.3%, 282,464 B / 1,048,576 B, total: 27% used)

gcc-arm-cortex-m7-tls13

  • FLASH: .text +64 B (+0.0%, 237,383 B / 262,144 B, total: 91% used)

linuxkm-pie

  • Data: __patchable_function_entries +32 B (+0.1%, 26,472 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
@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 2 times, most recently 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 Outdated
Comment thread wolfcrypt/src/asn.c Outdated
@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
Comment thread tests/api/test_mldsa.c
Comment thread wolfcrypt/src/wc_mldsa.c
Comment thread wolfcrypt/src/wc_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 tests/api/test_mldsa.c Outdated
Comment thread wolfcrypt/src/asn.c
Comment thread wolfcrypt/src/wc_mldsa.c
@Frauschi Frauschi assigned stenslae and unassigned wolfSSL-Bot Aug 4, 2026
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from afe7a9f to e5044af Compare August 5, 2026 22:37

@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. ✅

@Frauschi

Frauschi commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Jenkins retest this please

@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: 24 total — 15 posted, 9 skipped

Posted findings

  • [High] ecc_make_pub_ex no longer upgrades key->type on WC_PENDING_E, so async ECC verify with a private-only key never resumeswolfcrypt/src/ecc.c:5765-5769
  • [Medium] EccDerivePubBestEffort retries with WC_ASYNC_FLAG_CALL_AGAIN on an operation documented as not re-callablewolfcrypt/src/asn.c:33222-33234
  • [Medium] test_wc_EccPrivateKeyDecode_derive_pub guard omits !defined(WC_NO_RNG) and asserts behaviour the code deliberately skips theretests/api/test_asn.c:2785-2795,2856-2864
  • [Medium] tests/suites.c guards add ML-DSA sign but not ML-DSA verifytests/suites.c:1329-1333,1362-1366
  • [Medium] Public-key derivation now runs on every successful wc_EccPrivateKeyDecode, including probe/measure call sites that discard the keywolfcrypt/src/asn.c:33370-33374
  • [Medium] Best-effort ECC public-key derivation is abandoned when the temporary RNG cannot be created, and instantiates an unused DRBG on every decode in SP-math buildswolfcrypt/src/asn.c:33196-33220, wolfcrypt/src/asn_orig.c:7630-7654
  • [Low] EccDerivePubBestEffort duplicated verbatim (90 lines) in asn.c and asn_orig.c with no sync mechanismwolfcrypt/src/asn.c:33164-33253
  • [Low] New user-settable macro WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE not registered in .wolfssl_known_macro_extraswolfcrypt/src/asn.c:33167-33173
  • [Low] *Public verify entry points return PUBLIC_KEY_E without zeroing res, unlike the internal guard they duplicatewolfcrypt/src/wc_mldsa.c:12175,12231,12293,12335
  • [Low] ECC OOM loop never asserts the injected failure actually firedtests/api/test_asn.c:2978-3010
  • [Low] ECC OOM guard omits WOLFSSL_MEM_FAIL_COUNT / WOLFSSL_FORCE_MALLOC_FAIL_TEST exclusions the ML-DSA harness hastests/api/test_asn.c:2785-2795,2943-2944
  • [Low] ForceZero runs over uninitialized stack scratch on every early-error return of wc_MlDsaKey_MakePublicKeywolfcrypt/src/wc_mldsa.c:11757-11765
  • [Low] Doxygen for the new/changed ML-DSA APIs is inconsistent with the implemented return codesdoc/dox_comments/header_files/wc_mldsa.h:260-282,853-855
  • [Low] wc_MlDsaKey_PublicKeyToDer() performs a full public-key derivation for the documented output==NULL size querywolfcrypt/src/wc_mldsa.c:14343-14360
  • [Info] WOLFSSL_ECC_BLIND_K scrub of key->ku added in the ASN layer instead of ecc_make_pub_ex(), leaving every other make-pub caller unscrubbedwolfcrypt/src/asn.c:33247-33249, wolfcrypt/src/asn_orig.c:7681-7683
Skipped findings
  • [Medium] WC_MLDSA_HAVE_CHECK_KEY's second arm is logically dead, and its condition is now hand-duplicated in three places that can drift
  • [Low] devId skip in EccDerivePubBestEffort is ineffective under WOLF_CRYPTO_CB_FIND
  • [Low] WOLFSSL_ECC_BLIND_K has no CI coverage, so the kua fix and the new key->ku scrub are never compiled
  • [Low] test_mldsa_make_public_key_cache_invalidation's core assertions never run in any CI job that executes unit.test
  • [Low] Roughly 30 added lines exceed the 80-column convention, including a 98-char comment duplicated in two files
  • [Low] wc_MlDsaKey_MakePublicKey has no wc_dilithium_* legacy alias
  • [Low] wc_mldsa.c gains an unconditional dependency on ConstantCompare(), which is compiled out under WOLFSSL_NO_CONST_CMP
  • [Info] New ML-DSA public-key derivation scratch not registered with WOLFSSL_CHECK_MEM_ZERO
  • [Info] WC_MLDSA_HAVE_CHECK_KEY and the fault-harden call site are gated on different macros; the second clause of WC_MLDSA_HAVE_CHECK_KEY is dead

Review generated by Skoll via Claude/Codex

Comment thread wolfcrypt/src/ecc.c

/* change key state if public part is cached */
if (key->type == ECC_PRIVATEKEY_ONLY && pubOut == NULL) {
/* change key state if public part is cached -- success only. */

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] ecc_make_pub_ex no longer upgrades key->type on WC_PENDING_E, so async ECC verify with a private-only key never resumes
🚫 BLOCK bug

The old unconditional key->type = ECC_PRIVATEKEY upgrade was load-bearing for the async resume path, and gating it on err == MP_OKAY breaks it. wc_ecc_verify_hash captures isPrivateKeyOnly from key->type at ECC_STATE_VERIFY_DO (ecc.c:9227-9231), then deliberately does not advance key->state when it is set (ecc.c:9262-9266, comment: "do not advance state if doing make pub key"). wc_ecc_verify_hash_ex re-enters the make-pub branch on every entry where key->type == ECC_PRIVATEKEY_ONLY (ecc.c:10034-10053). Before this change: the first call returned WC_PENDING_E with the type already flipped to ECC_PRIVATEKEY, so the resume skipped make-pub and ran the actual ECDSA verify. After this change: the type stays ECC_PRIVATEKEY_ONLY, the resume re-enters make-pub and re-submits the point multiply — the verify never progresses. This is broader than the QAT-only path: ecc_make_pub_ex can return WC_PENDING_E from wc_CryptoCb_EccMakePub (ecc.c:5727-5732, which sets doneInCb=1 and lets WC_PENDING_E through), so any WOLF_CRYPTO_CB device that offloads ECC make-pub asynchronously hits it. The comment directly above the function (ecc.c:5659-5661) states the async contract explicitly: "For async the results are placed directly into pubOut, so this function does not need to be called again." The tightening is correct for the plain failure case — pub->x/y/z have already been mp_cleared at ecc.c:5746-5757, so the old code labelled a cleared point ECC_PRIVATEKEY — it just must not catch WC_PENDING_E.

Suggestion:

Suggested change
/* change key state if public part is cached -- success only. */
/* change key state if public part is cached -- success (or still
* pending) only. */
if (((err == MP_OKAY)
#ifdef WOLFSSL_ASYNC_CRYPT
|| (err == WC_NO_ERR_TRACE(WC_PENDING_E))
#endif
) && (key->type == ECC_PRIVATEKEY_ONLY) && (pubOut == NULL)) {
key->type = ECC_PRIVATEKEY;
}

Recommendation: Let WC_PENDING_E through the new condition, mirroring the existing pattern in _ecc_make_key_ex (ecc.c:6228-6234), which already special-cases WC_PENDING_E when setting the type.

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.

🟡 [Medium] EccDerivePubBestEffort retries with WC_ASYNC_FLAG_CALL_AGAIN on an operation documented as not re-callable
💡 SUGGEST bug

The new helper uses the generic do { wc_AsyncWait(..., WC_ASYNC_FLAG_CALL_AGAIN); op(); } while (ret == WC_PENDING_E) idiom, but ecc_make_pub_ex documents the opposite contract at ecc.c:5659-5661 ("For async the results are placed directly into pubOut, so this function does not need to be called again"). Trace: iteration 1's wc_AsyncWait(0, ...) short-circuits (returns immediately when ret != WC_PENDING_E), then wc_ecc_make_pub_ex submits and returns WC_PENDING_E; iteration 2's wc_AsyncWait(WC_PENDING_E, ..., CALL_AGAIN) waits for completion, returns 0, and wc_ecc_make_pub_ex is then called again, submitting a fresh point multiply — the loop does not terminate. The in-tree precedent for this exact operation is a blocking WC_ASYNC_FLAG_NONE wait with no re-call (ecc.c:7382-7398). Reachability is narrow: the helper returns early for devId != INVALID_DEVID under WOLF_CRYPTO_CB, so this needs either a QAT keygen build or WOLF_CRYPTO_CB_FIND with an async find-callback (see the related finding on the devId skip). Duplicated verbatim at wolfcrypt/src/asn_orig.c:7656-7668.

Suggestion:

Suggested change
do {
#ifdef ECC_TIMING_RESISTANT
pubRet = wc_ecc_make_pub_ex(key, NULL, rng);
#else
pubRet = wc_ecc_make_pub_ex(key, NULL, NULL);
#endif
#ifdef WOLFSSL_ASYNC_CRYPT
pubRet = wc_AsyncWait(pubRet, &key->asyncDev, WC_ASYNC_FLAG_NONE);
#endif

Recommendation: Replace the retry loop with a single call plus a blocking WC_ASYNC_FLAG_NONE wait, matching ecc.c:7395-7398. Apply to both copies (asn.c and asn_orig.c).

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

#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_NO_MALLOC) && \

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] test_wc_EccPrivateKeyDecode_derive_pub guard omits !defined(WC_NO_RNG) and asserts behaviour the code deliberately skips there
💡 SUGGEST test

Two independent failures in a WC_NO_RNG build. (a) wc_InitRng is a macro expanding to NOT_COMPILED_IN under WC_NO_RNG (wolfssl/wolfcrypt/random.h:463), so ExpectIntEQ(wc_InitRng(&rng), 0) fails immediately and short-circuits the whole test to TEST_FAIL rather than skipping it. (b) EccDerivePubBestEffort() deliberately returns without deriving under ECC_TIMING_RESISTANT && WC_NO_RNG (asn.c:33214-33218, the #else arm that emits "no RNG for blinding" and returns), so ExpectIntEQ(privOnlyKey.type, ECC_PRIVATEKEY) asserts the opposite of the implemented behaviour. Every other RNG-using test in tests/api/test_ecc.c carries !defined(WC_NO_RNG), including test_wc_EccPrivateKeyToDer (test_ecc.c:2529), which exercises the same API. Not currently reachable in CI — the only --disable-rng job is rust-wrapper.yml:68, which is --enable-cryptonly and does not build tests/unit.test — but it is a supported downstream config.

Suggestion:

Suggested change
#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_NO_MALLOC) && \
#if !defined(NO_ASN) && defined(HAVE_ECC) && !defined(NO_ECC_MAKE_PUB) && \
!defined(WC_NO_RNG) && \
!defined(WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE) && \
defined(HAVE_ECC_KEY_EXPORT) && \

Recommendation: Add !defined(WC_NO_RNG) to both the function guard (test_asn.c:2856-2864) and the OOM-helper guard (test_asn.c:2785-2795).

Comment thread tests/suites.c
@@ -1329,8 +1329,10 @@ int SuiteTest(int argc, char** argv)
#if defined(WOLFSSL_HAVE_SLHDSA) && \

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] tests/suites.c guards add ML-DSA sign but not ML-DSA verify
💡 SUGGEST test

Both conf files use ML-DSA-44 entity certs on both sides — tests/test-tls13-slhdsa-shake.conf references server-mldsa44-shake.pem/server-mldsa44-priv.pem (lines 8-9, 23-24) and client-mldsa44-shake.pem/client-mldsa44-priv.pem (lines 32-33) for the mutual-auth scenario. The peer therefore has to verify an ML-DSA-44 CertificateVerify, which is gated on !defined(WOLFSSL_MLDSA_NO_VERIFY) at src/tls13.c:12362 and :12513. WOLFSSL_MLDSA_NO_VERIFY is an independent configure output, so a verify-disabled ML-DSA build still compiles these suite entries in and the handshakes fail. The added !defined(WOLFSSL_MLDSA_NO_SIGN) term is correct and does close a real gap (NO_SIGN alone does not imply WOLFSSL_MLDSA_VERIFY_ONLY — see dilithium.h:303-308 — so the pre-existing !defined(WOLFSSL_MLDSA_VERIFY_ONLY) term did not cover it); the symmetric verify term is just missing. SLH-DSA sign is not needed, since the certs are pre-generated.

Suggestion:

Suggested change
#if defined(WOLFSSL_HAVE_SLHDSA) && \
defined(WOLFSSL_TLS13) && !defined(WOLFSSL_NO_ML_DSA_44) && \
!defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_MLDSA_NO_VERIFY)

Recommendation: Add && !defined(WOLFSSL_MLDSA_NO_VERIFY) to both blocks. The now-redundant !defined(WOLFSSL_MLDSA_VERIFY_ONLY) term can be dropped since VERIFY_ONLY implies NO_SIGN (dilithium.h:309-316).

Comment thread wolfcrypt/src/asn.c
key, curve_id);
}

#ifdef WOLFSSL_ECC_DERIVE_PUB_BEST_EFFORT

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] Public-key derivation now runs on every successful wc_EccPrivateKeyDecode, including probe/measure call sites that discard the key
💡 SUGGEST bug

EccDerivePubBestEffort() runs on every successful SEC1/PKCS#8 ECC private-key decode. ECC_TIMING_RESISTANT is on in default autotools builds (--enable-harden defaults to yes, configure.ac:3044-3052) and key->rng is NULL at decode time, so each decode now costs a full DRBG instantiation (wc_InitRng_ex -> entropy poll) plus a base-point scalar multiply. Several in-tree callers decode only to probe or measure and then throw the key away, making the derived point pure waste: wc_CheckPrivateKey (asn.c:9617 — exports the raw scalar at 9621, wc_ecc_free at 9626, re-imports at 9629; runs on every use_certificate/check_private_key/OCSP-responder pairing check), wolfcrypt/src/evp_pk.c:199 (pure type probe, key freed at 205), and src/ssl_load.c:573 (ProcessBufferTryDecodeEcc, which only needs keyFormat/keySize). TLS server startup and every d2i_PrivateKey-style type sniff now pay an extra point multiply plus DRBG init per candidate key; on constrained targets the RNG init can block on entropy inside what the caller thinks is a parse. The opt-out macro exists, but the default is the expensive path.

Recommendation: Prefer lazy derivation — the consumers that actually need the point (wc_EccKeyToDer, ECIES, verify, EVP export) already call wc_ecc_make_pub_ex() themselves on an ECC_PRIVATEKEY_ONLY key. If eager derivation is required for the wolfSSH use case, at minimum skip it on the probe paths, or hoist the RNG so the DRBG is instantiated once per process rather than per decode. Same call site at wolfcrypt/src/asn_orig.c:7816-7820.

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

#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_NO_MALLOC) && \

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 OOM guard omits WOLFSSL_MEM_FAIL_COUNT / WOLFSSL_FORCE_MALLOC_FAIL_TEST exclusions the ML-DSA harness has
💡 SUGGEST test

The ML-DSA OOM harness added in the same diff excludes both macros (tests/api/test_mldsa.c:8154-8161 and :8295-8301) because each injects its own allocation failures inside wolfSSL_Mallocwc_MemFailCount_AllocMem() at wolfcrypt/src/memory.c:337-342 runs before the user hook, and the gMemFailCount countdown at memory.c:395-411 runs after it — making both the counting pass and the injection pass non-deterministic. The ECC harness omits them, so in a WOLFSSL_FORCE_MALLOC_FAIL_TEST build the counting decode at test_asn.c:2969 can fail spuriously and the whole test reports failure.

Also raised by the bugs scan (tests/api/test_asn.c:2785-2790, 2943-2944):

The new ECC allocation-failure loop installs its own allocators and calibrates a totalAllocCount by counting the allocations one decode makes, then fails each in turn. Its guard is only:

#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_NO_MALLOC) && \
    !defined(WOLFSSL_STATIC_MEMORY)

The parallel ML-DSA test added by the same PR (tests/api/test_mldsa.c) additionally excludes WOLFSSL_MEM_FAIL_COUNT and WOLFSSL_FORCE_MALLOC_FAIL_TEST; the ECC one does not. Both of those macros make wolfSSL_Malloc() fail independently of the test's injector:

  • WOLFSSL_MEM_FAIL_COUNT (wolfcrypt/src/memory.c:337-342) returns NULL from wc_MemFailCount_AllocMem() before the registered malloc callback runs, so those allocations are invisible to ecc_oom_count. totalAllocCount is then under-counted, and a decode can fail from an allocation the test never injected.
  • WOLFSSL_FORCE_MALLOC_FAIL_TEST (memory.c:395-405) frees and nulls a successful allocation once --gMemFailCount hits zero, again after the callback has already been counted.

Either one makes the calibration assertion ExpectIntEQ(wc_EccPrivateKeyDecode(privOnlyDer, &idx, &privOnlyKey, privOnlyDerSz), 0) able to fail, and makes totalAllocCount non-reproducible between the two passes. wolfSSL runs a WOLFSSL_MEM_FAIL_COUNT configuration in CI, so this is a reachable flake rather than a theoretical one.

Suggestion:

Suggested change
#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_NO_MALLOC) && \
#if defined(USE_WOLFSSL_MEMORY) && !defined(WOLFSSL_NO_MALLOC) && \
!defined(WOLFSSL_STATIC_MEMORY) && !defined(WOLFSSL_MEM_FAIL_COUNT) && \
!defined(WOLFSSL_FORCE_MALLOC_FAIL_TEST) && !defined(NO_ASN) && \

Recommendation: Add !defined(WOLFSSL_MEM_FAIL_COUNT) && !defined(WOLFSSL_FORCE_MALLOC_FAIL_TEST) to both the helper guard (2785-2795) and the inner allocator block guard (2943-2944), matching the ML-DSA harness.

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

/* t0 is private key material, so scrub both encode buffers. */

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] ForceZero runs over uninitialized stack scratch on every early-error return of wc_MlDsaKey_MakePublicKey
🔧 NIT style

In a non-WOLFSSL_SMALL_STACK build, WC_VAR_OK() expands to the literal 1 (wolfssl/wolfcrypt/types.h:962), so both ForceZero calls execute unconditionally — including on the key == NULL, !prvKeySet, key->params == NULL, and devId-rejection paths, where t0Poly/t1Poly are uninitialized stack arrays that were never written. That is 424 + 328 bytes of pointless volatile-qualified writes on what should be a cheap argument-validation failure. Harmless, but it defeats the purpose of the early BAD_FUNC_ARG returns.

Suggestion: Track whether the encode scratch was actually used, e.g. set a local int scratchUsed = 0; just before the first mldsa_encode_t_stream() call and gate both ForceZero calls on scratchUsed && WC_VAR_OK(...).

Recommendation: Gate the scrub on the scratch having been written, or move the cleanup inside the if ((ret == 0) && (!key->pubKeySet)) block where the buffers are allocated and used.

*/
int wc_MlDsaKey_MakeKeyFromSeed(wc_MlDsaKey* key, const byte* seed);

/*!

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] Doxygen for the new/changed ML-DSA APIs is inconsistent with the implemented return codes
🔧 NIT style

Two small doc mismatches. (1) The new wc_MlDsaKey_MakePublicKey block lists 0 / BAD_FUNC_ARG / MEMORY_E / Other negative but omits PUBLIC_KEY_E, which is the function's most interesting documented failure (t0 or tr mismatch, wc_mldsa.c:11634 and :11650) — and every export function's block in the same file does list it. (2) The wc_MlDsaKey_PrivateKeyDecode addition says the public key is "derived best-effort on demand by export functions", but the export functions now hard-fail with PUBLIC_KEY_E/MEMORY_E when derivation fails (as those same doc blocks state); "best-effort" describes the ECC path, not this one, and reads as a contradiction.

Suggestion:

Suggested change
/*!
\return 0 on success or already set.
\return BAD_FUNC_ARG if invalid args, or the public key is not yet set
and key has a devId set.
\return MEMORY_E on allocation failure.
\return PUBLIC_KEY_E if the derived public key does not match the t0
and tr values stored in the private key.
\return Other negative on error.

Recommendation: Add PUBLIC_KEY_E to the MakePublicKey return list, and reword the PrivateKeyDecode note to "public key left unset; derived on demand by the export functions, which fail if derivation fails."

Comment thread wolfcrypt/src/wc_mldsa.c
*
* @return Size of encoded data in bytes on success.
* @return BAD_FUNC_ARG when key is NULL.
* @return MEMORY_E when dynamic memory allocation failed.

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] wc_MlDsaKey_PublicKeyToDer() performs a full public-key derivation for the documented output==NULL size query
🔧 NIT

The derive hook is placed immediately after the key == NULL check and before any handling of output == NULL. The doxygen for this function states "Pass NULL as output to query the required buffer size", and the encoded size depends only on key->params, never on the key bytes. A caller following the documented two-call pattern (query size, allocate, encode) on a private-only ML-DSA key therefore pays a full keygen-cost derivation - matrix A expansion, NTT, matrix multiply, plus the t0 and tr verification passes - on the first call, purely to answer a question that needs none of it.

The derivation also mutates the key (key->p, key->pubKeySet, key->aSet, and key->privVecsSet under WC_MLDSA_CACHE_PRIV_VECTORS) inside what the signature still advertises as \param [in] key. wc_MlDsaKey_ExportPubRaw() at line 13151 has the same hook but at least places it after the *outLen computation, so a pure size query there is cheap - the two functions are inconsistent with each other.

Trigger: wc_MlDsaKey_PublicKeyToDer(key, NULL, 0, withAlg) on a key decoded from a private-only ML-DSA DER.

Recommendation: Gate the derive on output != NULL, matching the placement already used in wc_MlDsaKey_ExportPubRaw():

#if !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && \
    !defined(WOLFSSL_MLDSA_ASSIGN_KEY) && \
    !defined(WOLFSSL_MLDSA_NO_MAKE_KEY)
    /* Size query needs only the parameter set - don't derive for it. */
    if ((ret == 0) && (output != NULL) && (!key->pubKeySet)) {
        ret = wc_MlDsaKey_MakePublicKey(key);
    }
#endif

and move the existing !key->pubKeySet -> BAD_FUNC_ARG check under the same output != NULL condition so the size query still answers from key->params.

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

#ifdef WOLFSSL_ECC_BLIND_K

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] WOLFSSL_ECC_BLIND_K scrub of key->ku added in the ASN layer instead of ecc_make_pub_ex(), leaving every other make-pub caller unscrubbed
🔧 NIT

The PR correctly identifies that wc_ecc_make_pub_ex() leaves the unblinded private scalar in key->ku (via ecc_get_k() at ecc.c:377-378, which XORs k with kb into ku and returns it), and adds mp_forcezero(key->ku) after the call. But the scrub is added in the two ASN decode helpers only. Every other caller of the same primitive still leaves the plaintext scalar sitting in the key object for its whole lifetime, defeating the point of WOLFSSL_ECC_BLIND_K until wc_ecc_free() runs:

  • ecc.c:6222 - _ecc_make_key_ex() keygen
  • ecc.c:7382 - ecc_sign_hash_sw() nonce point
  • ecc.c:10045 - verify-time public-key recovery on an ECC_PRIVATEKEY_ONLY key
  • ecc.c:15521-15523 - wc_ecc_encrypt_ex() ECIES
  • the public wc_ecc_make_pub() / wc_ecc_make_pub_ex() entry points

This is not a defect the PR introduces - it is the pre-existing behaviour for those paths - but the mitigation was placed one layer too high, so the fix does not generalize and will not be picked up by any future caller.

Recommendation: Move the scrub into ecc_make_pub_ex() (wolfcrypt/src/ecc.c:5763), next to the existing key->type update, so every caller benefits and the two duplicated ASN copies can be dropped:

/* change key state if public part is cached -- success only. */
if ((err == MP_OKAY) && (key->type == ECC_PRIVATEKEY_ONLY) &&
        (pubOut == NULL)) {
    key->type = ECC_PRIVATEKEY;
}

#ifdef WOLFSSL_ECC_BLIND_K
/* ecc_get_k() left the unblinded scalar in key->ku; don't keep it. */
mp_forcezero(key->ku);
#endif

return err;

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