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
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from 8b41aee to 10a84a1 Compare July 29, 2026 17:10
Comment thread wolfcrypt/src/asn.c Outdated
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from 10a84a1 to 546e1ce Compare July 29, 2026 17:19

@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 tests/api/test_mldsa.c Outdated
Comment thread tests/api/test_asn.c
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from 546e1ce to fec67b5 Compare July 29, 2026 17:49
Comment thread tests/api/test_mldsa.c Outdated
Comment thread tests/api/test_asn.c
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from fec67b5 to 1ee6dc8 Compare July 29, 2026 17:54

@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/wc_mldsa.c Outdated
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from 1ee6dc8 to 3bb246b Compare July 29, 2026 20:49
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
int test_mldsa_make_public_key(void)
{
EXPECT_DECLS;
#if defined(WOLFSSL_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_PRIVATE_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] Three new ML-DSA tests use certs_test.h buffers without the NO_SIGN/NO_VERIFY guards that declare them
🚫 BLOCK bug

Three new tests in tests/api/test_mldsa.c reference wolfssl/certs_test.h buffers whose declarations sit behind narrower guards than the tests themselves, so a sign-disabled ML-DSA build hits undeclared-identifier errors.

The arrays and their real guards (verified in certs_test.h at the PR head):

  • bench_mldsa_{44,65,87}_key / sizeof_* — inside #ifndef WOLFSSL_MLDSA_NO_SIGN (44 at 5017-5280, 65 at 5422-5832, 87 at 6038-6534).
  • bench_mldsa_{44,65,87}_pubkey / sizeof_* — inside #ifndef WOLFSSL_MLDSA_NO_VERIFY (44 at 5282-5420, 65 at 5834-6036, 87 at 6536-6802).
  • mldsa44_priv_only / sizeof_mldsa44_priv_only — inside #ifndef WOLFSSL_MLDSA_NO_SIGN (6952-7496).

The three affected sites:

  1. test_mldsa_make_public_key (7769-7770) — guard is WOLFSSL_HAVE_MLDSA && WOLFSSL_MLDSA_PRIVATE_KEY && !WOLFSSL_MLDSA_ASSIGN_KEY && !WOLFSSL_MLDSA_NO_MAKE_KEY, but the body uses both the _key and _pubkey arrays, so it needs both new conditions.
  2. test_mldsa_verify_pubkeyset_guard (7961-7962) — guard is ... && !WOLFSSL_MLDSA_NO_VERIFY && !WOLFSSL_NO_ML_DSA_44, but the body imports bench_mldsa_44_key; needs !WOLFSSL_MLDSA_NO_SIGN.
  3. test_mldsa_private_key_export_derive_fail (8067-8070) and its static OOM-allocator callback block (8004-8007) — neither guards on WOLFSSL_MLDSA_NO_SIGN, but the body decodes mldsa44_priv_only.

--enable-mldsa=44,make,verify (configure.ac -> -DWOLFSSL_MLDSA_NO_SIGN) reaches all three: WOLFSSL_MLDSA_PRIVATE_KEY stays defined because WOLFSSL_MLDSA_NO_MAKE_KEY is not derived from WOLFSSL_MLDSA_NO_SIGN (dilithium.h:295-330), so every guard above is satisfied while the arrays are absent.

The omission is inconsistent within the same file: the sibling test test_mldsa_private_key_export_derives_public_key (7852-7856) already guards on both !WOLFSSL_MLDSA_NO_SIGN and !WOLFSSL_MLDSA_NO_VERIFY. This PR also adds !defined(WOLFSSL_MLDSA_NO_SIGN) gating to tests/suites.c, which shows sign-disabled builds are exercised.

Recommendation: Widen each guard to match the buffers its test consumes:

/* test_mldsa.c:7769 — test_mldsa_make_public_key */
#if defined(WOLFSSL_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \
    !defined(WOLFSSL_MLDSA_ASSIGN_KEY) && !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && \
    !defined(WOLFSSL_MLDSA_NO_SIGN) && !defined(WOLFSSL_MLDSA_NO_VERIFY)

/* test_mldsa.c:7961 — test_mldsa_verify_pubkeyset_guard */
#if defined(WOLFSSL_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \
    !defined(WOLFSSL_MLDSA_NO_VERIFY) && !defined(WOLFSSL_MLDSA_NO_SIGN) && \
    !defined(WOLFSSL_NO_ML_DSA_44)

For test_mldsa_private_key_export_derive_fail, add !defined(WOLFSSL_MLDSA_NO_SIGN) to both the function guard at 8067-8070 and the static-callback block guard at 8004-8007.

Worth adding a WOLFSSL_MLDSA_NO_SIGN-only CI build to catch this class of breakage.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed guards

Comment thread wolfcrypt/src/wc_mldsa.c
* @param [in] aValid Non-zero if `a` already holds expanded `rho` matrix.
* @return 0 on success, negative on error.
*/
static int mldsa_calc_t_std(wc_MlDsaKey* key, const byte* rho, sword32* s1,

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] Build break: new mldsa_calc_t_std() calls mldsa_expand_a(), which is not compiled in MAKE_KEY_SMALL_MEM builds
💡 SUGGEST

The PR factors the standard t = NTT^-1(A o NTT(s1)) + s2 computation into a new static helper mldsa_calc_t_std(), guarded only by #if !defined(WOLFSSL_MLDSA_NO_MAKE_KEY) || defined(WOLFSSL_MLDSA_CHECK_KEY) (line 7767). It calls mldsa_expand_a() unconditionally at line 7774. But mldsa_expand_a()'s guard (lines 2786-2793, untouched by this PR) explicitly excludes the small-memory keygen build: its first arm is (!defined(WOLFSSL_MLDSA_NO_MAKE_KEY) && !defined(WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM)). mldsa_calc_t_std() has no MAKE_KEY_SMALL_MEM exclusion term.

The helper is genuinely reachable in that config: the PR also changed wc_MlDsaKey_CheckKey's guard (line 11808) to defined(WOLFSSL_MLDSA_CHECK_KEY) || (!defined(WOLFSSL_MLDSA_ASSIGN_KEY) && !defined(WOLFSSL_MLDSA_NO_MAKE_KEY)), and CheckKey calls mldsa_calc_t_std() at lines 11899/11901.

Triggering config (WOLFSSL_MLDSA_NO_CHECK_KEY is required because dilithium.h:325-330 auto-defines WOLFSSL_MLDSA_CHECK_KEY otherwise):
-DWOLFSSL_HAVE_MLDSA -DWOLFSSL_MLDSA_NO_CHECK_KEY -DWOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM -DWOLFSSL_MLDSA_SIGN_SMALL_MEM -DWOLFSSL_MLDSA_VERIFY_SMALL_MEM (no WC_MLDSA_CACHE_MATRIX_A).

Guard evaluation: mldsa_expand_a (2786) => (T && F)=F || F || (T && F)=F || (T && (F||F))=F => NOT compiled. mldsa_calc_t_std (7767) => !NO_MAKE_KEY = T => compiled. Result: implicit declaration of the static mldsa_expand_a — a hard error on clang >=16 / gcc >=14 and an undefined static symbol elsewhere. Every other callee of mldsa_calc_t_std (mldsa_vec_ntt_small_full, mldsa_matrix_mul, mldsa_vec_invntt_full, mldsa_vec_add, mldsa_vec_red) IS available in that config; mldsa_expand_a is the only missing one.

This is new: pre-PR, wc_MlDsaKey_CheckKey was #ifdef WOLFSSL_MLDSA_CHECK_KEY-only and mldsa_calc_t_std did not exist, so nothing referenced mldsa_expand_a in this configuration.

Secondary effect in the same family: whenever MAKE_KEY_SMALL_MEM is set and CHECK_KEY is not, mldsa_calc_t_std() is also completely unused (both keygen and MakePublicKey take the mldsa_calc_t_small_mem() path), tripping -Wunused-function under -Werror.

Recommendation: Either widen mldsa_expand_a's guard at line 2786 to track mldsa_calc_t_std's availability, e.g. add || (!defined(WOLFSSL_MLDSA_ASSIGN_KEY) && !defined(WOLFSSL_MLDSA_NO_MAKE_KEY)); or tighten mldsa_calc_t_std's guard to the exact set of configs that call it:

#if (!defined(WOLFSSL_MLDSA_NO_MAKE_KEY) &&
!defined(WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM)) ||
defined(WOLFSSL_MLDSA_CHECK_KEY)

and make wc_MlDsaKey_CheckKey use the streaming path in small-mem builds. Add a MAKE_KEY_SMALL_MEM + NO_CHECK_KEY CI job.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed guards

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

#if defined(WOLFSSL_MLDSA_CHECK_KEY) || \
(!defined(WOLFSSL_MLDSA_ASSIGN_KEY) && !defined(WOLFSSL_MLDSA_NO_MAKE_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] Build break: outer guard for mldsa_vec_decode_eta_bits not widened, so the new CheckKey/MakePublicKey arm is dead
💡 SUGGEST

The PR widened the inner guard around mldsa_vec_decode_eta_bits (line 1279) with the new arm (!defined(WOLFSSL_MLDSA_ASSIGN_KEY) && !defined(WOLFSSL_MLDSA_NO_MAKE_KEY)), but left the enclosing guard at line 1144 untouched:

#if !defined(WOLFSSL_MLDSA_NO_SIGN) || defined(WOLFSSL_MLDSA_CHECK_KEY)

Because the widened inner #if is nested inside the un-widened outer #if, the new arm is dead: when WOLFSSL_MLDSA_NO_SIGN is defined and WOLFSSL_MLDSA_CHECK_KEY is not, mldsa_decode_eta_2_bits, mldsa_decode_eta_4_bits and mldsa_vec_decode_eta_bits are all compiled out.

This is a missed spot in the same edit: for the sibling helper mldsa_vec_decode_t0 the PR did widen the outer guard (line 1488 now carries the (!ASSIGN_KEY && !NO_MAKE_KEY) arm). Both outer guards should have been changed identically.

Meanwhile wc_MlDsaKey_CheckKey (guard at 11808) calls mldsa_vec_decode_eta_bits at 11889-11890, and the new wc_MlDsaKey_MakePublicKey calls it at 10587-10588 and 10652-10654.

Triggering config: -DWOLFSSL_HAVE_MLDSA -DWOLFSSL_MLDSA_NO_SIGN -DWOLFSSL_MLDSA_NO_CHECK_KEY (equivalently --enable-mldsa=44,make,verify, which emits -DWOLFSSL_MLDSA_NO_SIGN at configure.ac:7800, plus WOLFSSL_MLDSA_NO_CHECK_KEY in user_settings.h). Evaluation with NO_MAKE_KEY/ASSIGN_KEY/NO_VERIFY undefined and CHECK_KEY suppressed:

  • line 1144: (!NO_SIGN)=F || (CHECK_KEY)=F => FALSE => not compiled
  • wc_MlDsaKey_CheckKey (11808): F || (T && T) => TRUE => compiled
  • wc_MlDsaKey_MakePublicKey (10412): !NO_MAKE_KEY && !ASSIGN_KEY => TRUE => compiled

Result: implicit declaration / undefined static mldsa_vec_decode_eta_bits. Every other helper CheckKey needs is present in that config; this is the only missing one. Pre-PR the guards were consistent, because wc_MlDsaKey_CheckKey was #ifdef WOLFSSL_MLDSA_CHECK_KEY — exactly the second arm of line 1144.

Recommendation: Widen the outer guard at line 1144 exactly as the outer guard at line 1488 was widened:

#if !defined(WOLFSSL_MLDSA_NO_SIGN) || defined(WOLFSSL_MLDSA_CHECK_KEY) || \
    (!defined(WOLFSSL_MLDSA_ASSIGN_KEY) && !defined(WOLFSSL_MLDSA_NO_MAKE_KEY))

and add a CI config exercising --enable-mldsa=make,verify with WOLFSSL_MLDSA_NO_CHECK_KEY.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed guards

Comment thread tests/api/test_mldsa.c Outdated
int allocatorsSet = 0;
int targetMatch = 0;
/* Match t0Scratch allocation size. */
size_t t0ScratchSz = (size_t)PARAMS_ML_DSA_44_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.

🟠 [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 job
💡 SUGGEST

mldsa_oom_malloc_cb only counts an allocation when size == mldsa_oom_match_size, and the test computes the expected sizes itself:

  • t0Scratch: PARAMS_ML_DSA_44_K * (MLDSA_D * MLDSA_N / 8U) = 4 * 416 = 1664 (matches wc_mldsa.c:10494-10495 / 10573).
  • s1/s2/t/a: S1_SIZE + 2*S2_SIZE + A_SIZE = 4096 + 8192 + 16384 = 28672 (matches wc_mldsa.c:10500-10504 / 10531).

Those values are right for the default build, but the size the allocator callback receives is not the size XMALLOC was called with. Verified in wolfcrypt/src/memory.c:344-354:

#ifdef WOLFSSL_CHECK_MEM_ZERO
    /* Space for requested size. */
    size += MEM_ALIGN;
#endif
    if (malloc_function) { res = malloc_function(size); }

MEM_ALIGN is 16 (memory.c:192), so with -DWOLFSSL_CHECK_MEM_ZERO the callback sees 1680 and 28688 — never 1664/28672. mldsa_oom_match_count stays 0, targetMatch becomes 0, and ExpectIntGE(targetMatch, 1) at line 8110 fails. Worse, mldsa_oom_fail_at_match = 0 then disables injection entirely, so ExpectIntNE(wc_MlDsaKey_MakePublicKey(key), 0) also fails because the call succeeds.

This is not hypothetical: .github/workflows/os-check.yml:158 runs the job with ["--enable-all", "CPPFLAGS=-DWOLFSSL_CHECK_MEM_ZERO"], and --enable-all turns on ML-DSA 44/65/87 with make/sign/verify — exactly the configuration in which this test body is compiled.

The same class of breakage applies to -DWOLFSSL_MEM_FAIL_COUNT (memory.c:337-341 returns NULL before the callback runs, so mldsa_oom_failed never becomes 1 while the allocation still fails) and -DWOLFSSL_FORCE_MALLOC_FAIL_TEST.

Secondary robustness note: matching purely on byte size uses a process-global hook, so any unrelated allocation of the same size in the window is counted. In the default build nothing else allocates 1664 or 28672 bytes there (wc_MlDsaKey_CheckKey allocates s1Sz + 4*s2Sz + aSz = 36864, wc_mldsa.c:11852-11856), but the coupling is silent: any future change to MakePublicKey's allocation layout turns this into a failure rather than a stale-but-passing test.

Recommendation: Stop matching on exact byte size. Either (a) match with size >= mldsa_oom_match_size plus a tolerance, or (b) drop the size heuristic and fail the Nth allocation made while a test-controlled flag is set around the wc_MlDsaKey_MakePublicKey() call only. At minimum add !defined(WOLFSSL_CHECK_MEM_ZERO) && !defined(WOLFSSL_MEM_FAIL_COUNT) && !defined(WOLFSSL_FORCE_MALLOC_FAIL_TEST) to the guard at test_mldsa.c:8067-8070 so the all-check-mem-zero CI job does not regress.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed oom checking

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

#ifdef ECC_TIMING_RESISTANT

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] ECC decode-time public key derivation is inert in default hardened builds
💡 SUGGEST question

The helper bails out early when ECC_TIMING_RESISTANT is defined and key->rng == NULL. ECC_TIMING_RESISTANT is enabled by --enable-harden, which is on by default, and key->rng is only populated by an explicit wc_ecc_set_rng() call. Grepping the tree shows no wolfSSL internal caller sets an RNG on an ecc_key before wc_EccPrivateKeyDecode() - the only wc_ecc_set_rng() sites (src/internal.c:5913, src/sniffer.c:2821, wolfcrypt/src/hpke.c, wolfcrypt/src/pkcs7.c) all operate on keys that are already decoded or freshly generated. So in the default build and in every internal key-load path, the new derivation never runs and the key stays ECC_PRIVATEKEY_ONLY. The new test only exercises the derive branch because it explicitly calls wc_ecc_set_rng() first. Meanwhile ecc_make_pub_sw() documents rng as "optional RNG for a timing-resistant point multiply, may be NULL", so the skip is a conservative choice rather than a hard requirement.

Suggestion: Either fall back to wc_ecc_make_pub_ex(key, NULL, NULL) when no RNG is available (matching what ecc_verify_hash already does at ecc.c:9643), or set a global/default RNG on the key inside the decode path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added the wc_ecc_make_pub_ex(key, NULL, NULL) fallback, following the verify hash convention. A default RNG could cause entropy blocking or memory management issues, which would get rid of the point of having ECC_TIMING_RESISTANT as a guard in the first place.

Note that not passing in an RNG value into this will allow for a minor timing/power side channel on the scalar multiply, but we now have the WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE opt-out.

Comment thread wolfcrypt/src/wc_mldsa.c Outdated

key->pubKeySet = 1;

/* Recompute/compare t1 (like CheckKey) to catch glitches.

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] MakePublicKey always doubles the matrix work with an unconditional CheckKey
💡 SUGGEST convention

After deriving t1, the function unconditionally re-runs the whole expand_a + matrix_mul + invntt pipeline (via wc_MlDsaKey_CheckKey(), or a streaming re-derive under WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM) purely as fault detection, and in the WC_MLDSA_CACHE_MATRIX_A case it forces key->aSet = 0 so the cached matrix cannot be reused. That is roughly a 2x cost on every private-key-only decode-then-export, plus an extra t0ScratchSz + t1Sz allocation in the small-mem path where memory is the scarce resource. The codebase already has a dedicated opt-in macro for exactly this trade-off, WC_MLDSA_FAULT_HARDEN (used at wc_mldsa.c:8787), which this new code ignores.

Suggestion: Wrap the verification block in #ifdef WC_MLDSA_FAULT_HARDEN so builds that have not opted into fault hardening pay the derivation cost once.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added fault hardening wrappers

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

#ifdef WOLFSSL_MLDSA_CHECK_KEY
#if defined(WOLFSSL_MLDSA_CHECK_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.

🟡 [Medium] WOLFSSL_MLDSA_NO_CHECK_KEY no longer removes wc_MlDsaKey_CheckKey
💡 SUGGEST api

dilithium.h:325-330 auto-defines WOLFSSL_MLDSA_CHECK_KEY whenever public+private key support is present and WOLFSSL_MLDSA_NO_CHECK_KEY is not set. Therefore the only situation in which the new guard WOLFSSL_MLDSA_CHECK_KEY || (!WOLFSSL_MLDSA_ASSIGN_KEY && !WOLFSSL_MLDSA_NO_MAKE_KEY) differs from the old one is a build that explicitly set WOLFSSL_MLDSA_NO_CHECK_KEY to save code space. Those builds now get wc_MlDsaKey_CheckKey() compiled and exported anyway, plus its transitive helpers (mldsa_vec_decode_t0, mldsa_vec_decode_t1, mldsa_vec_sub, ...) whose guards were widened in the same way. The user-facing meaning of WOLFSSL_MLDSA_NO_CHECK_KEY silently changed with no documentation update.

Suggestion: Introduce an internal-only helper (for example mldsa_check_key_internal()) compiled under the wider guard, and keep the public wc_MlDsaKey_CheckKey() symbol under WOLFSSL_MLDSA_CHECK_KEY alone; or gate the derivation-time verification on WC_MLDSA_FAULT_HARDEN so WOLFSSL_MLDSA_NO_CHECK_KEY keeps its meaning.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added fault hardening wrapper so CheckKey only happens in those cases

Comment thread wolfcrypt/src/wc_mldsa.c
}
}

#if !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && !defined(WOLFSSL_MLDSA_ASSIGN_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.

🟡 [Medium] Export/DER functions now mutate key state, breaking their read-only contract
💡 SUGGEST api

wc_MlDsaKey_ExportPubRaw(), wc_MlDsaKey_PublicKeyToDer() and wc_MlDsaKey_KeyToDer() were effectively read-only accessors. They now perform a full key derivation that writes key->p, sets key->pubKeySet, and (under WC_MLDSA_CACHE_PRIV_VECTORS) overwrites key->s1/key->s2/key->t0 and clears key->privVecsSet; under WC_MLDSA_CACHE_MATRIX_A it also rewrites key->a and toggles key->aSet. A long-lived server key shared read-only across threads for DER export would now race on those cached buffers, and a concurrent wc_MlDsaKey_SignCtx() on the same key could observe half-rewritten s1/s2. The doxygen for these three functions was not updated to note the side effect (only wc_MlDsaKey_PrivateKeyDecode's was).

Suggestion: Document the mutation in doxygen for all three functions, e.g. "When only the private key is set, the public key is derived and cached in key as a side effect; the key must not be shared across threads during this call."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Documented the policy change.

Comment thread wolfcrypt/src/asn.c
#endif /* WOLFSSL_ASN_TEMPLATE */
#ifdef HAVE_ECC

/* Like HAVE_ECC_MAKE_PUB, but excludes CAAM and FIPS/selftest. */

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] No opt-out for the new decode-time ECC scalar multiply
💡 SUGGEST api

WOLFSSL_ECC_DERIVE_PUB_BEST_EFFORT is derived purely from feature/hardware macros, so any build where derivation is possible gets an unconditional point multiply on every SEC1 private key decode with no compile-time way to turn it off. wc_EccPrivateKeyDecode() is on the key-loading path for wolfSSL_CTX_use_PrivateKey_*, PKCS#8/PKCS#12 import and wc_CryptoCb key setup, and constrained targets loading several keys will pay a measurable startup cost for a public point they may never need. Every other similarly costly opt-in behaviour in this area (WOLFSSL_VALIDATE_ECC_IMPORT, WOLFSSL_VALIDATE_ECC_KEYGEN) is guarded by its own macro.

Suggestion:

Suggested change
/* Like HAVE_ECC_MAKE_PUB, but excludes CAAM and FIPS/selftest. */
#if !defined(NO_ECC_MAKE_PUB) && !defined(WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE) && \
!defined(WOLFSSL_ATECC508A) && ...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added the opt out

Comment thread wolfcrypt/src/wc_mldsa.c Outdated
#endif
mldsa_vec_encode_t0_t1(t, params->k, t0Scratch, t1);

/* Note: MakePublicKey doesn't recompute 'tr'; it must match

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] Derived ML-DSA public key is never checked against the stored tr hash
💡 SUGGEST bug

The comment acknowledges that tr is not recomputed and "must match the authentic hash already in the private key blob", but nothing verifies that assumption. The subsequent wc_MlDsaKey_CheckKey() compares the derived t1 against the private key's t0 and the public seed against rho - it never touches tr. So a private key blob whose tr field is corrupt or was written by a non-conforming encoder yields pubKeySet = 1 with a public key that looks fine, while every signature produced afterwards computes mu = H(tr || M) with the wrong tr and fails verification against that very public key. Since the derived key->p is complete at this point, checking SHAKE256(key->p, pkSz) == tr is a single cheap hash and would catch it immediately.

Suggestion:

Suggested change
/* Note: MakePublicKey doesn't recompute 'tr'; it must match
byte trCalc[MLDSA_TR_SZ];
const byte* tr = key->k + MLDSA_PUB_SEED_SZ + MLDSA_K_SZ;
ret = mldsa_shake256(&key->shake, key->p, params->pkSz, trCalc,
MLDSA_TR_SZ);
if ((ret == 0) && (XMEMCMP(trCalc, tr, MLDSA_TR_SZ) != 0)) {
ret = PUBLIC_KEY_E;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added unconditional tr validation

@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 Outdated
@stenslae
stenslae force-pushed the fix-mldsa-privkey-derive-pubkey branch from 81bbb62 to 9e69c56 Compare August 3, 2026 17:35
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