Skip to content

OpenSSL compat with ML-DSA - #10962

Open
kojo1 wants to merge 4 commits into
wolfSSL:masterfrom
kojo1:oss_mldsa
Open

OpenSSL compat with ML-DSA#10962
kojo1 wants to merge 4 commits into
wolfSSL:masterfrom
kojo1:oss_mldsa

Conversation

@kojo1

@kojo1 kojo1 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Description

wolfSSL_PEM_read_bio_PrivateKey
wolfSSL_PEM_read_PrivateKey
wolfSSL_d2i_PrivateKey, wolfSSL_d2i_PublicKey
wolfSSL_X509_sign
wolfSSL_X509_REQ_sign
wolfSSL_X509_set_pubkey

Fixes zd#22151

Testing

Added test_wolfSSL_PEM_PrivateKey_mldsa
Added ML-DSA cases in test_wolfSSL_X509_set_pubkey

Checklist

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

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

@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: APPROVE
Findings: 4 total — 3 posted, 2 skipped

Posted findings

  • [High] ML-DSA private key decode in X509 sign/set_pubkey missing PRIVATE_KEY_UNLOCK bracket (fails under FIPS)src/x509.c:12827, src/x509.c:16508
  • [Medium] WC_MAX_X509_GEN increase puts 20 KB on the stack in wolfSSL_X509_REQ_sign for all key typessrc/x509.c:12953-12962, 16922-16923
  • [Medium] wolfSSL_X509_REQ_sign changes lack ML-DSA test coveragesrc/x509.c:16918-16950
Skipped findings
  • [Low] fnames[] can become a zero-length array if all ML-DSA levels are disabled
  • [Medium] WC_MAX_X509_GEN increase puts 20 KB on the stack in wolfSSL_X509_REQ_sign for all key types

Review generated by Skoll via Claude/Codex

Comment thread src/x509.c
@kojo1

kojo1 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

lack ML-DSA test coverage

Add ML-DSA test coverage for X509_REQ_sign

missing PRIVATE_KEY_UNLOCK bracket

PRIVATE_KEY_UNLOCK to X509 sign/set_pubkey

WC_MAX_X509_GEN increase puts 20 KB on the stack

Dynamic buff size based on expected cert size

@Frauschi

Copy link
Copy Markdown
Contributor

@kojo1 The CAVP self test failure is legit (undefined references during linking).

@kojo1
kojo1 requested a review from Frauschi July 29, 2026 00:28
@kojiws kojiws assigned wolfSSL-Bot and kojo1 and unassigned kojo1 and wolfSSL-Bot Jul 29, 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: APPROVE
Findings: 19 total — 13 posted, 6 skipped

Posted findings

  • [High] d2iTryMlDsaKey silently returns a stale EVP_PKEY when the caller reuses an existing key objectwolfcrypt/src/evp_pk.c:1086-1096
  • [Medium] DER buffer sizing ignores the subject public key, only the signing keysrc/x509.c:12961-12974
  • [Medium] ML-DSA signing still requires a valid digest; comment claims md is ignoredsrc/x509.c:12274-12277
  • [Medium] Preprocessor gating mismatch: sigTypeFromPKEY accepts ML-DSA in builds where resign_cert cannot signsrc/x509.c:12274
  • [Medium] MlDsaKey reused across a failed PrivateKeyDecode without Free/Init in X509_set_pubkeysrc/x509.c:16519-16542
  • [Medium] d2i_evp_pkey PKCS#8 algorithm cross-check not extended for ML-DSAwolfcrypt/src/evp_pk.c:1548-1553
  • [Medium] Test coverage limited to ML-DSA-44; the riskiest new paths are untestedtests/api/test_x509.c:1071-1128
  • [Medium] wolfSSL_X509_set_pubkey stores the standard ML-DSA OID for FIPS204-draft keys; the WC_ML_DSA_*_DRAFT cases are unreachablesrc/x509.c:16546-16590
  • [Low] New ML-DSA code uses MlDsaKey and mixed heap hints, unlike the rest of x509.csrc/x509.c:12786
  • [Low] WC_MAX_X509_GEN_MLDSA is an unrelated magic number to WC_MAX_X509_GENsrc/x509.c:12962-12974
  • [Low] X509_GEN_BUF_SZ sizes the DER buffer from the signing key, ignoring an ML-DSA subject public keysrc/x509.c:12961-12974
  • [Low] New ML-DSA code uses legacy-only LEVEL2/3/5 spellings, breaking WOLFSSL_NO_DILITHIUM_LEGACY_NAMES buildssrc/x509.c:12279-12295
  • [Info] ML-DSA key object reused across a failed private-key decode and a public-key decode without re-initsrc/x509.c:16527-16545
Skipped findings
  • [Medium] WC_DECLARE_VAR/WC_ALLOC_VAR_EX abstraction dropped in X509_REQ_sign
  • [Low] ML-DSA level-to-OID mapping duplicated three times; four-macro guard repeated four times
  • [Low] Test guards: zero-length array risk and missing NO_SHA256 condition
  • [Info] New test entry breaks the backslash alignment of TEST_X509_DECLS
  • [Info] ML-DSA private key DER newly persisted in EVP_PKEY.pkey.ptr, which is freed without ForceZero
  • [Info] X509_GEN_BUF_SZ evaluates its argument twice and is never #undef'd

Review generated by Skoll via Claude/Codex

Comment thread wolfcrypt/src/evp_pk.c

/* Copy the consumed DER into pkey->pkey.ptr when the input was DER */
ret = d2i_make_pkey(out, mem, keyIdx, priv, WC_EVP_PKEY_DILITHIUM);
/* Copy the consumed DER into pkey->pkey.ptr when the input was DER.

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] d2iTryMlDsaKey silently returns a stale EVP_PKEY when the caller reuses an existing key object
💡 SUGGEST

The PR changes d2iTryMlDsaKey() to skip d2i_make_pkey() whenever *out already has pkey.ptr set. That guard is correct for the new d2i_evp_pkey() call site (where local was just allocated and pre-filled with the input bytes), but d2iTryMlDsaKey() is also reached from the auto-detect path d2i_evp_pkey_try(), which at line 1124-1126 deliberately adopts a caller-supplied key: if ((out != NULL) && (*out != NULL)) pkey = *out;. In that path *out is a previously populated EVP_PKEY holding a completely different key. With the new condition, d2i_make_pkey() is never called, so pkey.ptr, pkey_sz and type are left pointing at the OLD key, yet the function returns 1 (success) and stamps the NEW mldsaOID onto the stale object. d2i_evp_pkey_try() then returns that object as the decoded ML-DSA key.

Unlike the Ed25519/Ed448 helpers this pattern was copied from, ML-DSA has no key object on the EVP_PKEY ((*out)->ed25519 = edKey has no ML-DSA analogue) - the ML-DSA key lives entirely in pkey.ptr. So for ML-DSA nothing at all is updated, not even pkey->type.

Before this PR d2i_make_pkey() was called unconditionally and correctly re-populated a reused EVP_PKEY, so this is a regression.

Trigger: EVP_PKEY *k = NULL; d2i_PUBKEY(&k, &rsa_spki, rsa_len); d2i_PUBKEY(&k, &mldsa_spki, mldsa_len); - the second call returns non-NULL "success" but EVP_PKEY_id(k) is still EVP_PKEY_RSA and k->pkey.ptr still holds the RSA SPKI. The same happens through wolfSSL_PEM_read_bio_PUBKEY() / wolfSSL_PEM_read_PUBKEY() (src/pk.c:6477 and 6645 both adopt *key) and through wolfSSL_d2i_PrivateKey_EVP(), all of which are documented OpenSSL reuse semantics.

Recommendation: Do not infer "caller already populated the key" from pkey.ptr != NULL. Pass the intent explicitly instead, e.g. add a int prePopulated parameter set to 1 only from d2i_evp_pkey() and 0 from d2i_evp_pkey_try():

static int d2iTryMlDsaKey(WOLFSSL_EVP_PKEY** out, const unsigned char* mem,
    long memSz, int priv, int prePopulated)
{
    ...
    ret = 1;
    if (!prePopulated) {
        ret = d2i_make_pkey(out, mem, keyIdx, priv, WC_EVP_PKEY_DILITHIUM);
    }

Alternatively, in d2i_evp_pkey() do not pre-fill local->pkey.ptr for the ML-DSA case and let d2i_make_pkey() own the copy, as it did before this PR. Consider adding a regression test that calls PEM_read_bio_PUBKEY() twice with the same non-NULL EVP_PKEY** (RSA first, then ML-DSA) and asserts EVP_PKEY_id() changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Explicit prePopulated flag for d2iTryMlDsaKey.
Added test_wolfSSL_d2i_PUBKEY_mldsa_reuse.

Comment thread src/x509.c
/* able to override max size until dynamic buffer created */
#define WC_MAX_X509_GEN 4096
#endif
#ifdef WOLFSSL_HAVE_MLDSA

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] DER buffer sizing ignores the subject public key, only the signing key
💡 SUGGEST bug

The new X509_GEN_BUF_SZ(pkey) macro selects 20480 vs 4096 purely from pkey->type — the signing key. But the buffer passed to wolfssl_x509_make_der() must hold the whole TBS, which includes the subject public key, and this same PR newly teaches wolfSSL_X509_set_pubkey() to accept ML-DSA keys. So the combination the PR just enabled — a certificate carrying an ML-DSA-87 subject public key (2592-byte SPKI, ~2616 encoded) signed by a classic RSA/ECC key — still gets only WC_MAX_X509_GEN (4096). With an RSA-4096 signature (512 + algid), a full issuer/subject DN and a handful of extensions, the encoding can exceed 4096 and wolfssl_x509_make_der fails with BUFFER_E. The commit message says "Dynamic buff size based on expected cert size", but the heuristic only looks at half of what determines that size.

Suggestion:

Suggested change
#ifdef WOLFSSL_HAVE_MLDSA
/* Consider the subject public key as well as the signing key. */
#define X509_GEN_BUF_SZ(x509, pkey) \
((((pkey) != NULL && (pkey)->type == WC_EVP_PKEY_DILITHIUM) || \
((x509) != NULL && wolfssl_x509_is_mldsa_pubkey(x509))) ? \
WC_MAX_X509_GEN_MLDSA : WC_MAX_X509_GEN)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Size X509 gen buffer from subject public key too.

Comment thread src/x509.c
return WOLFSSL_FAILURE;
}
}
#ifdef WOLFSSL_HAVE_MLDSA

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] ML-DSA signing still requires a valid digest; comment claims md is ignored
💡 SUGGEST api

The new ML-DSA branch is annotated /* ML-DSA does not use a separate hash; ignore md. */, but it is only reached after the unconditional wolfSSL_EVP_get_hashinfo(md, &hashType, NULL) guard at the top of the function, which returns WOLFSSL_FAILURE for a NULL or unrecognized md. wolfSSL_X509_sign() also rejects md == NULL outright. OpenSSL's idiomatic call for a hash-free signature algorithm is X509_sign(x, pkey, NULL), and X509_sign_ctx() on an EVP_DigestSignInit(ctx, &pctx, NULL, NULL, pkey) context passes a NULL md through EVP_MD_CTX_md(). Both fail here. For a PR whose stated goal is OpenSSL compatibility with ML-DSA, callers written against OpenSSL semantics will not work, and the comment misleads the next reader into thinking they will.

Suggestion:

Suggested change
#ifdef WOLFSSL_HAVE_MLDSA
/* In wolfSSL_sigTypeFromPKEY, hoist the ML-DSA dispatch above the hash lookup: */
#ifdef WOLFSSL_HAVE_MLDSA
if (pkey->type == WC_EVP_PKEY_DILITHIUM) {
/* ML-DSA signs the message directly - md is unused and may be NULL. */
switch (WOLFSSL_ATOMIC_LOAD(pkey->mldsaOID)) {
...
}
return sigType;
}
#endif
if (wolfSSL_EVP_get_hashinfo(md, &hashType, NULL) == ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Allow NULL md for ML-DSA X509 signing.

Comment thread src/x509.c
return WOLFSSL_FAILURE;
}
}
#ifdef WOLFSSL_HAVE_MLDSA

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] Preprocessor gating mismatch: sigTypeFromPKEY accepts ML-DSA in builds where resign_cert cannot sign
💡 SUGGEST bug

wolfSSL_sigTypeFromPKEY() returns a valid ML-DSA sigType under #ifdef WOLFSSL_HAVE_MLDSA alone, while the key-decoding branch in wolfSSL_X509_resign_cert() is gated on WOLFSSL_HAVE_MLDSA && WOLFSSL_MLDSA_PRIVATE_KEY && !WOLFSSL_MLDSA_NO_ASN1 && !WOLFSSL_MLDSA_NO_SIGN. In a verify-only ML-DSA build (WOLFSSL_MLDSA_VERIFY_ONLY / WOLFSSL_MLDSA_NO_SIGN), wolfSSL_X509_sign(x509, mldsa_pkey, md) now passes the sigType lookup, produces a TBS, then reaches wc_SignCert_ex(..., type = -1, key = NULL, ...). That does not crash (GetSignatureBufferSz() returns ALGO_ID_E when every key pointer is NULL), but the failure surfaces as an obscure wolfCrypt error late in the pipeline instead of a clean early rejection, and the two gates silently disagree about what the library supports. Before this PR the unsupported key type was rejected up front by sigTypeFromPKEY.

Suggestion:

Suggested change
#ifdef WOLFSSL_HAVE_MLDSA
/* Define one derived macro near the top of the ML-DSA section and use it everywhere: */
#if defined(WOLFSSL_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \
!defined(WOLFSSL_MLDSA_NO_ASN1) && !defined(WOLFSSL_MLDSA_NO_SIGN)
#define WOLFSSL_X509_MLDSA_SIGN
#endif

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Single gate for ML-DSA X509 signing capability

Comment thread src/x509.c
byte level = 0;
int decodeOk = 0;

mldsa = (MlDsaKey*)XMALLOC(sizeof(MlDsaKey), cert->heap,

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] MlDsaKey reused across a failed PrivateKeyDecode without Free/Init in X509_set_pubkey
💡 SUGGEST bug

The new ML-DSA case tries wc_MlDsaKey_PrivateKeyDecode() first and, on failure, falls through to wc_MlDsaKey_PublicKeyDecode() on the same, un-reset MlDsaKey. wc_MlDsaKey_PrivateKeyDecode() is not atomic: at wolfcrypt/src/wc_mldsa.c:12221-12227 it calls wc_MlDsaKey_SetParams(key, level) after the ASN.1 layer succeeds and can still fail afterwards during the key-material import. In that window key->level is left set, so the subsequent PublicKeyDecode() runs with a pinned level instead of auto-detecting from the SPKI OID, and mismatching input is rejected with ASN_PARSE_E rather than being decoded. d2iTryMlDsaKey() in wolfcrypt/src/evp_pk.c handles exactly this by doing wc_MlDsaKey_Free() + wc_MlDsaKey_Init() before switching decode strategies; this new code does not.

Suggestion:

Suggested change
mldsa = (MlDsaKey*)XMALLOC(sizeof(MlDsaKey), cert->heap,
if (!decodeOk) {
/* Private decode may have partially set params before failing;
* reset the key so the public decode auto-detects the level. */
wc_MlDsaKey_Free(mldsa);
if (wc_MlDsaKey_Init(mldsa, cert->heap, INVALID_DEVID) != 0) {
XFREE(mldsa, cert->heap, DYNAMIC_TYPE_MLDSA);
return WOLFSSL_FAILURE;
}
idx = 0;
if (wc_MlDsaKey_PublicKeyDecode(mldsa, ..., &idx) != 0) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reset MlDsaKey between decode attempts in X509_set_pubkey

Comment thread src/x509.c
WC_RNG rng;
#if defined(WOLFSSL_HAVE_MLDSA) && defined(WOLFSSL_MLDSA_PRIVATE_KEY) && \
!defined(WOLFSSL_MLDSA_NO_ASN1) && !defined(WOLFSSL_MLDSA_NO_SIGN)
MlDsaKey* mldsa = NULL;

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] New ML-DSA code uses MlDsaKey and mixed heap hints, unlike the rest of x509.c
🔧 NIT convention

Three small consistency drifts from the surrounding file. (1) The new code declares MlDsaKey* mldsa, while every pre-existing ML-DSA site in src/x509.c (lines 12342, 12522) uses the canonical wc_MlDsaKey; MlDsaKey is a compat alias in wc_mldsa.h:689 alongside the legacy dilithium_key. (2) In wolfSSL_X509_set_pubkey the struct is allocated with cert->heap but wc_MlDsaKey_Init(mldsa, NULL, INVALID_DEVID) is passed a NULL heap, so the key's internal allocations bypass the hint — this matters in WOLFSSL_STATIC_MEMORY builds. In wolfSSL_X509_resign_cert both use NULL even though x509->heap is available. (3) None of the new error returns emit a WOLFSSL_MSG, whereas neighbouring branches ("Failed to allocate memory for wc_MlDsaKey", "malloc error") consistently do.

Suggestion:

Suggested change
MlDsaKey* mldsa = NULL;
wc_MlDsaKey* mldsa;
...
mldsa = (wc_MlDsaKey*)XMALLOC(sizeof(wc_MlDsaKey), cert->heap,
DYNAMIC_TYPE_MLDSA);
if (mldsa == NULL) {
WOLFSSL_MSG("Failed to allocate memory for wc_MlDsaKey");
return WOLFSSL_FAILURE;
}
if (wc_MlDsaKey_Init(mldsa, cert->heap, INVALID_DEVID) != 0) {

Comment thread src/x509.c
#define WC_MAX_X509_GEN 4096
#endif
#ifdef WOLFSSL_HAVE_MLDSA
#ifndef WC_MAX_X509_GEN_MLDSA

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_MAX_X509_GEN_MLDSA is an unrelated magic number to WC_MAX_X509_GEN
🔧 NIT style

WC_MAX_X509_GEN_MLDSA is hardcoded to 20480 while WC_MAX_X509_GEN defaults to 4096, and the two are unrelated. The comment justifies the value with ML-DSA-87's 2592-byte key and 4627-byte signature — roughly 7.5 KB with overhead — so 20480 is ~2.7x the stated need with no explanation of the margin. Both macros are user-overridable and independent, so a user who raises WC_MAX_X509_GEN above 20480 would get a smaller buffer for ML-DSA than for classic keys. The new macro is also undocumented (no README or manual entry, and the PR checklist leaves those boxes unticked).

Suggestion:

Suggested change
#ifndef WC_MAX_X509_GEN_MLDSA
#ifndef WC_MAX_X509_GEN_MLDSA
/* ML-DSA-87: 2592 byte public key + 4627 byte signature, plus DN,
* extensions and ASN.1 overhead; rounded up with headroom. Never
* smaller than the classic buffer. */
#define WC_MAX_X509_GEN_MLDSA (WC_MAX_X509_GEN + 16384)
#endif

Comment thread src/x509.c
/* able to override max size until dynamic buffer created */
#define WC_MAX_X509_GEN 4096
#endif
#ifdef WOLFSSL_HAVE_MLDSA

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] X509_GEN_BUF_SZ sizes the DER buffer from the signing key, ignoring an ML-DSA subject public key
💡 SUGGEST

The new macro picks WC_MAX_X509_GEN_MLDSA (20480) only when the signing key is ML-DSA: ((pkey)->type == WC_EVP_PKEY_DILITHIUM) ? WC_MAX_X509_GEN_MLDSA : WC_MAX_X509_GEN. But the buffer must hold the whole TBS body, which contains the subject public key set by X509_set_pubkey() - and that key can be ML-DSA while the issuer signs with RSA or ECDSA (a very common hybrid/migration setup, and exactly what the PR newly enables via the ML-DSA X509_set_pubkey() case).

An ML-DSA-87 SPKI alone is 2614 bytes (WC_MLDSA_87_PUB_KEY_DER_SIZE). Add issuer/subject DNs, validity, serial, SKID/AKID/SAN extensions and an RSA-4096 signature (512 bytes + AlgorithmIdentifier) and the 4096-byte budget is easily exhausted.

The failure is clean, not memory-unsafe - wc_MakeCert_ex(cert, der, *derSz, ...) and wc_SignCert_ex(..., der, derSz, ...) both honour the passed size - so the symptom is wolfSSL_X509_sign() returning WOLFSSL_FAILURE for a certificate that should encode fine. The condition also existed before the PR (the buffer was unconditionally 4096), so this is an incomplete fix rather than a regression; flagging it because the PR's stated goal is exactly to size the buffer for ML-DSA and the comment ("chosen from the signing key type") documents the wrong criterion.

Recommendation: Take the subject public key into account as well, e.g. make the macro a small static helper that inspects both the signing key and x509->pubKeyOID:

static int x509_gen_buf_sz(const WOLFSSL_X509* x509,
        const WOLFSSL_EVP_PKEY* pkey)
{
#ifdef WOLFSSL_HAVE_MLDSA
    if (((pkey != NULL) && (pkey->type == WC_EVP_PKEY_DILITHIUM)) ||
            ((x509 != NULL) && wolfssl_is_mldsa_oid(x509->pubKeyOID))) {
        return WC_MAX_X509_GEN_MLDSA;
    }
#endif
    return WC_MAX_X509_GEN;
}

Better still, honour the long-standing @TODO dynamic set based on expected cert size the PR removed: size the buffer from the actual encoded length rather than from a per-algorithm constant.

Comment thread src/x509.c
/* ML-DSA does not use a separate hash; ignore md. */
switch (WOLFSSL_ATOMIC_LOAD(pkey->mldsaOID)) {
case ML_DSA_44k:
sigType = CTC_ML_DSA_LEVEL2;

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] New ML-DSA code uses legacy-only LEVEL2/3/5 spellings, breaking WOLFSSL_NO_DILITHIUM_LEGACY_NAMES builds
💡 SUGGEST

Both new switch statements use identifiers that only exist when the pre-standardization aliases are enabled:

  • CTC_ML_DSA_LEVEL2/3/5 - defined in wolfssl/wolfcrypt/oid_sum.h:1791-1796 under #ifndef WOLFSSL_NO_DILITHIUM_LEGACY_NAMES
  • ML_DSA_LEVEL2_TYPE/3/5 - defined in wolfssl/wolfcrypt/asn_public.h:190-196 under the same guard

Both new blocks are gated on WOLFSSL_HAVE_MLDSA, which is not disabled by WOLFSSL_NO_DILITHIUM_LEGACY_NAMES, so a build with WOLFSSL_NO_DILITHIUM_LEGACY_NAMES + ML-DSA + OPENSSL_EXTRA + WOLFSSL_CERT_GEN fails to compile with undeclared identifiers. WOLFSSL_NO_DILITHIUM_LEGACY_NAMES is a documented, user-settable option (see the comment at wolfssl/wolfcrypt/dilithium.h:58-60).

Adjacent pre-existing ML-DSA code in the same file uses the canonical spellings (ML_DSA_44_TYPE, wc_MlDsaKey_SetParams(mldsa, WC_ML_DSA_44) at src/x509.c:12537-12548), so the new code is also internally inconsistent.

Note for triage: other legacy uses already exist in the tree (ML_DSA_LEVEL2k at asn.c:9231 and evp_pk.c:1922) under #ifdef HAVE_DILITHIUM, which is gated by the different macro WOLFSSL_NO_DILITHIUM_LEGACY_GATES (settings.h:436-439) - so that configuration is already broken today. Severity is therefore Low rather than Medium, but this PR adds six new instances in a fresh code path and should not deepen the problem.

Recommendation: Use the canonical names, matching the rest of src/x509.c:

case ML_DSA_44k: sigType = CTC_ML_DSA_44; break;
case ML_DSA_65k: sigType = CTC_ML_DSA_65; break;
case ML_DSA_87k: sigType = CTC_ML_DSA_87; break;

case WC_ML_DSA_44: type = ML_DSA_44_TYPE; break;
case WC_ML_DSA_65: type = ML_DSA_65_TYPE; break;
case WC_ML_DSA_87: type = ML_DSA_87_TYPE; break;

Comment thread src/x509.c
XFREE(mldsa, cert->heap, DYNAMIC_TYPE_MLDSA);
return WOLFSSL_FAILURE;
}
#ifdef 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.

⚪ [Info] ML-DSA key object reused across a failed private-key decode and a public-key decode without re-init
💡 SUGGEST

The new branch calls wc_MlDsaKey_PrivateKeyDecode() first and, if it fails, retries wc_MlDsaKey_PublicKeyDecode() on the same MlDsaKey object without freeing and re-initialising it. A failed private-key decode can leave key->level and key->params set from a partially parsed AlgorithmIdentifier, which then changes how wc_MlDsaKey_PublicKeyDecode() behaves: with a non-zero key->level it derives keyType from that level (wc_mldsa.c:12553-12560) instead of auto-detecting from the input OID, and wc_MlDsaKey_ImportPubRaw() is length-checked against the already-selected parameter set.

With the inputs that reach this code today (a single DER buffer that is either PKCS#8 or SPKI) the private decode fails before extracting an OID, so key->level stays 0 and the retry auto-detects correctly - the code is currently correct. It is fragile against changes in wc_MlDsaKey_PrivateKeyDecode()'s failure points. Compare d2iTryMlDsaKey() in wolfcrypt/src/evp_pk.c:1043-1048, which explicitly does wc_MlDsaKey_Free() + wc_MlDsaKey_Init() between decode attempts.

Recommendation: Reset the key between attempts, mirroring d2iTryMlDsaKey():

        if (!decodeOk) {
            wc_MlDsaKey_Free(mldsa);
            if (wc_MlDsaKey_Init(mldsa, NULL, INVALID_DEVID) != 0) {
                XFREE(mldsa, cert->heap, DYNAMIC_TYPE_MLDSA);
                return WOLFSSL_FAILURE;
            }
            idx = 0;
            if (wc_MlDsaKey_PublicKeyDecode(mldsa, ..., &idx) != 0) {
                ...
            }
        }

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.

3 participants