OpenSSL compat with ML-DSA - #10962
Conversation
|
Frauschi
left a comment
There was a problem hiding this comment.
🐺 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 types —
src/x509.c:12953-12962, 16922-16923 - [Medium] wolfSSL_X509_REQ_sign changes lack ML-DSA test coverage —
src/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
Add ML-DSA test coverage for X509_REQ_sign
PRIVATE_KEY_UNLOCK to X509 sign/set_pubkey
Dynamic buff size based on expected cert size |
|
@kojo1 The CAVP self test failure is legit (undefined references during linking). |
Frauschi
left a comment
There was a problem hiding this comment.
🐺 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 object —
wolfcrypt/src/evp_pk.c:1086-1096 - [Medium] DER buffer sizing ignores the subject public key, only the signing key —
src/x509.c:12961-12974 - [Medium] ML-DSA signing still requires a valid digest; comment claims md is ignored —
src/x509.c:12274-12277 - [Medium] Preprocessor gating mismatch: sigTypeFromPKEY accepts ML-DSA in builds where resign_cert cannot sign —
src/x509.c:12274 - [Medium] MlDsaKey reused across a failed PrivateKeyDecode without Free/Init in X509_set_pubkey —
src/x509.c:16519-16542 - [Medium] d2i_evp_pkey PKCS#8 algorithm cross-check not extended for ML-DSA —
wolfcrypt/src/evp_pk.c:1548-1553 - [Medium] Test coverage limited to ML-DSA-44; the riskiest new paths are untested —
tests/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 unreachable —
src/x509.c:16546-16590 - [Low] New ML-DSA code uses MlDsaKey and mixed heap hints, unlike the rest of x509.c —
src/x509.c:12786 - [Low] WC_MAX_X509_GEN_MLDSA is an unrelated magic number to WC_MAX_X509_GEN —
src/x509.c:12962-12974 - [Low] X509_GEN_BUF_SZ sizes the DER buffer from the signing key, ignoring an ML-DSA subject public key —
src/x509.c:12961-12974 - [Low] New ML-DSA code uses legacy-only LEVEL2/3/5 spellings, breaking WOLFSSL_NO_DILITHIUM_LEGACY_NAMES builds —
src/x509.c:12279-12295 - [Info] ML-DSA key object reused across a failed private-key decode and a public-key decode without re-init —
src/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
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 9 total — 8 posted, 1 skipped
Posted findings
- [Medium] X509_set_pubkey() fails for ML-DSA private keys that carry no seed and no embedded public key —
src/x509.c:16598 - [Medium] Uninitialized MlDsaKey / WC_RNG are freed unconditionally when an earlier Expect short-circuits —
tests/api/test_ossl_x509_pk.c:488 - [Medium] Double XFCLOSE on the same XFILE: the f = XBADFILE reset is missing after the second close —
tests/api/test_evp_pkey.c:2826 - [Medium] New tests still use the legacy-only MlDsaKey typedef, breaking WOLFSSL_NO_DILITHIUM_LEGACY_NAMES builds —
tests/api/test_evp_pkey.c:2846 - [Medium] EVP_PKEY reuse still leaks the previously attached algorithm object, and the new test hides it —
wolfcrypt/src/evp_pk.c:68 - [Low] sigOID is left set to WOLFSSL_FAILURE on the X509/REQ object when the signature-type lookup fails —
src/x509.c:13029 - [Low] Private-key DER is released without zeroization when an EVP_PKEY is reused —
wolfcrypt/src/evp_pk.c:71 - [Low] A non-NULL digest is silently ignored for ML-DSA signing —
src/x509.c:12206
Skipped findings
- [Low] The new reuse block indexes fnames[] unconditionally, which can be a zero-length array
Review generated by Skoll via Claude/Codex
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: COMMENT
Findings: 12 total — 3 posted, 9 skipped
Posted findings
- [Medium] d2i_free_prev_key_obj() does not mirror EVP_PKEY_free() for X25519/X448/HKDF/CMAC, so those objects leak on reuse —
wolfcrypt/src/evp_pk.c:44-107 - [Low] Ed25519/Ed448 d2i reuse leaves the stale type and DER in place - pre-existing, but the prePopulated pattern this PR adds is the fix —
wolfcrypt/src/evp_pk.c:1180 - [Medium] New tests add several multi-KB stack buffers —
tests/api/test_ossl_x509_pk.c:441,470,501,545; tests/api/test_evp_pkey.c:2735-2737,2790-2792
Skipped findings
- [Low] The new ForceZero assumes pkey_sz describes the pkey.ptr allocation - true today, but the invariant is already broken on master (pre-existing, not a blocker)
- [Medium] Unrelated deletion of WOLFSSL_MEM_FAIL_COUNT from .wolfssl_known_macro_extras
- [Info] FALSE POSITIVE - X509_GEN_BUF_SZ / WOLFSSL_MLDSA_X509_SIGN definition block cannot be skipped while a consumer is compiled
- [Medium] No test for the 'sigOID unchanged on rejected key/md' behavior the PR adds
- [Info] ALREADY RESOLVED - non-NULL md silently ignored for ML-DSA
- [Low] X509_GEN_BUF_SZ evaluates its pkey argument twice and drops it entirely in the non-ML-DSA variant
- [Low] Reused EVP_PKEY is left gutted when the d2i copy allocation fails
- [Low] Conditional free of pkey inside the key-file loop is hard to follow
- [Info] Continuation backslash misaligned in TEST_X509_DECLS
Review generated by Skoll via Claude/Codex
| * | ||
| * @param [in, out] pkey EVP PKEY being repurposed. | ||
| */ | ||
| static void d2i_free_prev_key_obj(WOLFSSL_EVP_PKEY* pkey) |
There was a problem hiding this comment.
🟡 [Medium] d2i_free_prev_key_obj() does not mirror EVP_PKEY_free() for X25519/X448/HKDF/CMAC, so those objects leak on reuse
💡 SUGGEST bug
The new helper's comment says it mirrors "the per-type cleanup in wolfSSL_EVP_PKEY_free()", but it only covers RSA, EC, DSA, DH, ED25519 and ED448. wolfSSL_EVP_PKEY_free() (wolfcrypt/src/evp.c:12332-12368) additionally handles WC_EVP_PKEY_X25519 (curve25519), WC_EVP_PKEY_X448 (curve448), WC_EVP_PKEY_HKDF (hkdfSalt/hkdfKey/hkdfInfo) and WC_EVP_PKEY_CMAC (cmacCtx).
Because d2i_make_pkey() overwrites pkey->type right after calling the helper, an object of one of those four types is not merely left attached - it becomes permanently unreachable, since wolfSSL_EVP_PKEY_free() switches on the new type and never visits the old case. That is the same defect this PR just fixed for RSA, only for the types the helper missed.
Confirmed with a PoC against the PR head (--enable-all --enable-dilithium --enable-experimental, macOS leaks):
pkey = wolfSSL_EVP_PKEY_new_raw_public_key(WC_EVP_PKEY_X25519, NULL, raw, 32);
p = client_keypub_der_2048;
wolfSSL_d2i_PUBKEY(&pkey, &p, sizeof_client_keypub_der_2048); /* reuse */
wolfSSL_EVP_PKEY_free(pkey);Process 42402: 1 leak for 160 total leaked bytes.
STACK OF 1 INSTANCE OF 'ROOT LEAK: <malloc in wolfSSL_EVP_PKEY_new_raw_public_key>'
The same run confirms the RSA case this PR fixed is now clean: an RSA-then-RSA d2i_PUBKEY reuse leaks nothing and the pkey->rsa pointer changes between decodes.
Suggestion:
| static void d2i_free_prev_key_obj(WOLFSSL_EVP_PKEY* pkey) | |
| #ifdef HAVE_CURVE25519 | |
| case WC_EVP_PKEY_X25519: | |
| if (pkey->curve25519 != NULL && pkey->ownCurve25519 == 1) { | |
| wc_curve25519_free(pkey->curve25519); | |
| XFREE(pkey->curve25519, pkey->heap, DYNAMIC_TYPE_CURVE25519); | |
| } | |
| pkey->curve25519 = NULL; | |
| pkey->ownCurve25519 = 0; | |
| break; | |
| #endif | |
| #ifdef HAVE_CURVE448 | |
| case WC_EVP_PKEY_X448: | |
| if (pkey->curve448 != NULL && pkey->ownCurve448 == 1) { | |
| wc_curve448_free(pkey->curve448); | |
| XFREE(pkey->curve448, pkey->heap, DYNAMIC_TYPE_CURVE448); | |
| } | |
| pkey->curve448 = NULL; | |
| pkey->ownCurve448 = 0; | |
| break; | |
| #endif | |
| #ifdef HAVE_HKDF | |
| case WC_EVP_PKEY_HKDF: | |
| XFREE(pkey->hkdfSalt, NULL, DYNAMIC_TYPE_SALT); | |
| pkey->hkdfSalt = NULL; | |
| if (pkey->hkdfKey != NULL && pkey->hkdfKeySz > 0) { | |
| ForceZero(pkey->hkdfKey, pkey->hkdfKeySz); | |
| } | |
| XFREE(pkey->hkdfKey, NULL, DYNAMIC_TYPE_KEY); | |
| pkey->hkdfKey = NULL; | |
| XFREE(pkey->hkdfInfo, NULL, DYNAMIC_TYPE_INFO); | |
| pkey->hkdfInfo = NULL; | |
| pkey->hkdfSaltSz = pkey->hkdfKeySz = pkey->hkdfInfoSz = 0; | |
| break; | |
| #endif |
| * pre-filled the EVP PKEY with the input bytes (d2i_evp_pkey()). | ||
| * A reused key must be re-populated here. */ | ||
| ret = 1; | ||
| if (!prePopulated) { |
There was a problem hiding this comment.
🔵 [Low] Ed25519/Ed448 d2i reuse leaves the stale type and DER in place - pre-existing, but the prePopulated pattern this PR adds is the fix
🔧 NIT bug
This PR fixes reuse for ML-DSA with an explicit prePopulated flag on d2iTryMlDsaKey(), so d2i_evp_pkey() can state "the bytes are already in place" instead of the callee guessing. d2iTryEd25519Key()/d2iTryEd448Key() still use the old heuristic if (*out == NULL || (*out)->pkey.ptr == NULL). On the auto-detect reuse path both conditions are false, so d2i_make_pkey() is skipped entirely and neither pkey->type nor pkey->pkey.ptr is updated.
Confirmed with a PoC - decode an RSA SPKI into a fresh EVP_PKEY, then reuse it for an Ed25519 PKCS#8 via wolfSSL_d2i_PrivateKey_EVP():
after RSA decode : type=16 (RSA=16) pkey_sz=294
after Ed25519 reuse: type=16 (ED25519=2140104148) pkey_sz=294 ed25519=0x101c917d0 rsa=0x101c909b0
The decode reports success, but the object still claims RSA, still serializes the previous RSA DER, and now holds an ed25519 object that wolfSSL_EVP_PKEY_free() will never reach because it switches on type.
This is pre-existing, not a defect of this PR - the PR does not touch either helper, and master (7a8aae3) reproduces the output byte for byte. Raising it here only because this PR establishes the pattern that fixes it.
Suggestion:
| if (!prePopulated) { | |
| static int d2iTryEd25519Key(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_ED25519); | |
| } | |
| /* d2i_evp_pkey_try() passes 0; d2i_evp_pkey()'s typed case passes 1. */ |
| * fails and the fallback public decode must work on a reset key. */ | ||
| { | ||
| WOLFSSL_EVP_PKEY* spki = NULL; | ||
| unsigned char der[2048]; |
There was a problem hiding this comment.
🟡 [Medium] New tests add several multi-KB stack buffers
💡 SUGGEST test
Sites: tests/api/test_ossl_x509_pk.c:441,470,501,545; tests/api/test_evp_pkey.c:2735-2737,2790-2792
The new test code declares unsigned char der[2048], unsigned char der[4096], byte pubDer[MLDSA_MAX_PUB_KEY_SIZE + 64] (~2656 bytes), byte draftDer[4096], unsigned char der44[2048], der65[2600], mldsaDer[4096], rsaDer[2048] and rawBlob[2560] as automatic arrays. Several sit in nested scopes inside one function, and compilers are not obliged to overlap nested-scope frames, so test_wolfSSL_X509_set_pubkey can reach ~13 KB of frame on its own. wolfSSL is regularly built and CI-tested with constrained stacks (--enable-smallstack, embedded/RTOS targets, and thread-stack-limited harnesses). The rest of tests/api generally XMALLOCs buffers of this size.
Suggestion:
| unsigned char der[2048]; | |
| unsigned char* der = NULL; | |
| ... | |
| ExpectNotNull(der = (unsigned char*)XMALLOC(4096, NULL, | |
| DYNAMIC_TYPE_TMP_BUFFER)); | |
| ... | |
| XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); |
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