From 497de930fd6f6949daa79493348291217f84607d Mon Sep 17 00:00:00 2001 From: Chris Conlon Date: Thu, 14 May 2026 14:34:47 -0600 Subject: [PATCH] evp: support ML-DSA in wolfSSL_EVP_PKCS82PKEY() and wolfSSL_X509_check_private_key() --- tests/api/test_asn.c | 17 +++-- tests/api/test_ossl_x509_crypto.c | 108 ++++++++++++++++++++++++++++++ tests/api/test_ossl_x509_crypto.h | 3 + wolfcrypt/src/evp_pk.c | 36 +++++++++- 4 files changed, 155 insertions(+), 9 deletions(-) diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 7697d02fdb..4c20a4f624 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -1360,7 +1360,12 @@ int test_wc_DecodeObjectId(void) #if defined(HAVE_PKCS8) && !defined(NO_ASN) && \ (defined(WOLFSSL_TEST_CERT) || defined(OPENSSL_EXTRA) || \ defined(OPENSSL_EXTRA_X509_SMALL) || defined(WOLFSSL_PUBLIC_ASN)) && \ - (defined(HAVE_ED25519) || defined(HAVE_ED448) || defined(HAVE_DILITHIUM)) + (defined(HAVE_ED25519) || \ + (defined(HAVE_ED448) && defined(HAVE_ED448_KEY_EXPORT) && \ + defined(WOLFSSL_KEY_GEN)) || \ + (defined(HAVE_DILITHIUM) && \ + !defined(WOLFSSL_DILITHIUM_NO_MAKE_KEY) && \ + !defined(WOLFSSL_DILITHIUM_NO_ASN1))) /* Run ToTraditional_ex() on a copy of der and assert the algId, returned * length, and the inner OCTET STRING tag/length at the start of the * (in-place rewritten) buffer. */ @@ -1676,8 +1681,8 @@ int test_ToTraditional_ex_negative(void) ((size_t)derSz + 2 + ED25519_PUB_KEY_SIZE <= sizeof(copy))) { word32 trailerSz = 2 + ED25519_PUB_KEY_SIZE; XMEMCPY(copy, der, (size_t)derSz); - ExpectTrue(copy[1] + trailerSz < 0x80); - if (EXPECT_SUCCESS() && (copy[1] + trailerSz < 0x80)) { + ExpectTrue(copy[1] < (byte)(0x80 - trailerSz)); + if (EXPECT_SUCCESS() && copy[1] < (byte)(0x80 - trailerSz)) { copy[1] = (byte)(copy[1] + trailerSz); copy[derSz] = ASN_CONTEXT_SPECIFIC | ASN_ASYMKEY_PUBKEY; copy[derSz + 1] = ED25519_PUB_KEY_SIZE; @@ -1742,7 +1747,7 @@ int test_ToTraditional_ex_mldsa_bad_params(void) der[sz++] = ASN_SEQUENCE | ASN_CONSTRUCTED; der[sz++] = (byte)(sizeof(mldsaOid) + 2 + 2); der[sz++] = ASN_OBJECT_ID; - der[sz++] = sizeof(mldsaOid); + der[sz++] = (byte)sizeof(mldsaOid); XMEMCPY(der + sz, mldsaOid, sizeof(mldsaOid)); sz += sizeof(mldsaOid); /* Disallowed, NULL parameter after the ML-DSA OID. */ der[sz++] = ASN_TAG_NULL; @@ -1769,11 +1774,11 @@ int test_ToTraditional_ex_mldsa_bad_params(void) der[sz++] = ASN_SEQUENCE | ASN_CONSTRUCTED; der[sz++] = (byte)(sizeof(mldsaOid) + 2 + sizeof(extraOid) + 2); der[sz++] = ASN_OBJECT_ID; - der[sz++] = sizeof(mldsaOid); + der[sz++] = (byte)sizeof(mldsaOid); XMEMCPY(der + sz, mldsaOid, sizeof(mldsaOid)); sz += sizeof(mldsaOid); /* Disallowed, OBJECT_ID parameter after the ML-DSA OID. */ der[sz++] = ASN_OBJECT_ID; - der[sz++] = sizeof(extraOid); + der[sz++] = (byte)sizeof(extraOid); XMEMCPY(der + sz, extraOid, sizeof(extraOid)); sz += sizeof(extraOid); der[sz++] = ASN_OCTET_STRING; der[sz++] = (byte)(privKeySz + 2); diff --git a/tests/api/test_ossl_x509_crypto.c b/tests/api/test_ossl_x509_crypto.c index 01f5735480..4c84d40617 100644 --- a/tests/api/test_ossl_x509_crypto.c +++ b/tests/api/test_ossl_x509_crypto.c @@ -71,6 +71,114 @@ int test_wolfSSL_X509_check_private_key(void) return EXPECT_RESULT(); } +/* EVP_PKCS82PKEY() must populate pkey.ptr/pkey_sz for ML-DSA so + * X509_check_private_key() (wc_CheckPrivateKey) can redecode the DER, and + * d2i_PKCS8_PKEY() must keep the full PKCS#8 wrapper for ML-DSA level recovery + * from the AlgorithmIdentifier. */ +int test_wolfSSL_X509_check_private_key_mldsa(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && !defined(NO_FILESYSTEM) && \ + !defined(NO_BIO) && !defined(NO_CHECK_PRIVATE_KEY) && \ + defined(HAVE_DILITHIUM) && !defined(WOLFSSL_DILITHIUM_NO_SIGN) && \ + !defined(WOLFSSL_DILITHIUM_NO_VERIFY) && \ + (defined(OPENSSL_ALL) || defined(WOLFSSL_WPAS_SMALL)) && \ + (!defined(WOLFSSL_NO_ML_DSA_44) || !defined(WOLFSSL_NO_ML_DSA_65) || \ + !defined(WOLFSSL_NO_ML_DSA_87)) + static const struct { + const char* keyPath; + const char* certPath; + const char* mismatchCertPath; /* NULL if no other level available */ + } cases[] = { + #if !defined(WOLFSSL_NO_ML_DSA_44) + { "./certs/mldsa/mldsa44-key.pem", + "./certs/mldsa/mldsa44-cert.der", + #if !defined(WOLFSSL_NO_ML_DSA_65) + "./certs/mldsa/mldsa65-cert.der" + #elif !defined(WOLFSSL_NO_ML_DSA_87) + "./certs/mldsa/mldsa87-cert.der" + #else + NULL + #endif + }, + #endif + #if !defined(WOLFSSL_NO_ML_DSA_65) + { "./certs/mldsa/mldsa65-key.pem", + "./certs/mldsa/mldsa65-cert.der", + #if !defined(WOLFSSL_NO_ML_DSA_87) + "./certs/mldsa/mldsa87-cert.der" + #elif !defined(WOLFSSL_NO_ML_DSA_44) + "./certs/mldsa/mldsa44-cert.der" + #else + NULL + #endif + }, + #endif + #if !defined(WOLFSSL_NO_ML_DSA_87) + { "./certs/mldsa/mldsa87-key.pem", + "./certs/mldsa/mldsa87-cert.der", + #if !defined(WOLFSSL_NO_ML_DSA_44) + "./certs/mldsa/mldsa44-cert.der" + #elif !defined(WOLFSSL_NO_ML_DSA_65) + "./certs/mldsa/mldsa65-cert.der" + #else + NULL + #endif + }, + #endif + }; + size_t i; + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + PKCS8_PRIV_KEY_INFO* pt = NULL; + EVP_PKEY* pkey = NULL; + X509* x509 = NULL; + X509* mismatchX509 = NULL; + BIO* bio = NULL; + byte* buf = NULL; + size_t sz = 0; + + ExpectIntEQ(load_file(cases[i].keyPath, &buf, &sz), 0); + + ExpectNotNull(bio = BIO_new_mem_buf((void*)buf, (int)sz)); + ExpectNotNull(pt = d2i_PKCS8_PRIV_KEY_INFO_bio(bio, NULL)); + + ExpectNotNull(pkey = EVP_PKCS82PKEY(pt)); + if (pkey != NULL) { + ExpectIntEQ(EVP_PKEY_id(pkey), EVP_PKEY_DILITHIUM); + /* pkey.ptr must hold the DER so that X509_check_private_key() to + * wc_CheckPrivateKey() can re-decode it. */ + ExpectNotNull(pkey->pkey.ptr); + ExpectIntGT(pkey->pkey_sz, 0); + } + + ExpectNotNull(x509 = X509_load_certificate_file( + cases[i].certPath, SSL_FILETYPE_ASN1)); + ExpectIntEQ(X509_check_private_key(x509, pkey), 1); + + if (cases[i].mismatchCertPath != NULL) { + ExpectNotNull(mismatchX509 = X509_load_certificate_file( + cases[i].mismatchCertPath, SSL_FILETYPE_ASN1)); + ExpectIntEQ(X509_check_private_key(mismatchX509, pkey), 0); + } + + /* Negative check, corrupt the outer SEQ tag so the key DER fails */ + if (EXPECT_SUCCESS() && (pkey != NULL) && (pkey->pkey.ptr != NULL)) { + pkey->pkey.ptr[0] ^= 0xFF; + ExpectIntEQ(X509_check_private_key(x509, pkey), 0); + } + + X509_free(mismatchX509); + X509_free(x509); + EVP_PKEY_free(pkey); + PKCS8_PRIV_KEY_INFO_free(pt); + BIO_free(bio); + XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + } +#endif + return EXPECT_RESULT(); +} + int test_wolfSSL_X509_verify(void) { EXPECT_DECLS; diff --git a/tests/api/test_ossl_x509_crypto.h b/tests/api/test_ossl_x509_crypto.h index fe09acf6d1..cf40bca9a3 100644 --- a/tests/api/test_ossl_x509_crypto.h +++ b/tests/api/test_ossl_x509_crypto.h @@ -25,6 +25,7 @@ #include int test_wolfSSL_X509_check_private_key(void); +int test_wolfSSL_X509_check_private_key_mldsa(void); int test_wolfSSL_X509_verify(void); int test_wolfSSL_X509_sign(void); int test_wolfSSL_X509_sign2(void); @@ -32,6 +33,8 @@ int test_wolfSSL_make_cert(void); #define TEST_OSSL_X509_CRYPTO_DECLS \ TEST_DECL_GROUP("ossl_x509_crypto", test_wolfSSL_X509_check_private_key), \ + TEST_DECL_GROUP("ossl_x509_crypto", \ + test_wolfSSL_X509_check_private_key_mldsa), \ TEST_DECL_GROUP("ossl_x509_crypto", test_wolfSSL_X509_verify), \ TEST_DECL_GROUP("ossl_x509_crypto", test_wolfSSL_X509_sign), \ TEST_DECL_GROUP("ossl_x509_crypto", test_wolfSSL_X509_sign2), \ diff --git a/wolfcrypt/src/evp_pk.c b/wolfcrypt/src/evp_pk.c index b3fc0c4d17..afd6cbb0eb 100644 --- a/wolfcrypt/src/evp_pk.c +++ b/wolfcrypt/src/evp_pk.c @@ -942,7 +942,8 @@ static int d2iTryDilithiumKey(WOLFSSL_EVP_PKEY** out, const unsigned char* mem, return WOLFSSL_FATAL_ERROR; } - return d2i_make_pkey(out, NULL, 0, priv, WC_EVP_PKEY_DILITHIUM); + /* Copy the consumed DER into pkey->pkey.ptr when the input was DER */ + return d2i_make_pkey(out, mem, keyIdx, priv, WC_EVP_PKEY_DILITHIUM); } #endif /* HAVE_DILITHIUM */ @@ -1696,6 +1697,10 @@ WOLFSSL_PKCS8_PRIV_KEY_INFO* wolfSSL_d2i_PKCS8_PKEY( DerBuffer rawDer; EncryptedInfo info; int advanceLen = 0; +#ifdef HAVE_DILITHIUM + word32 outerIdx = 0; + int outerLen = 0; +#endif /* Clear the encryption information and DER buffer. */ XMEMSET(&info, 0, sizeof(info)); @@ -1737,15 +1742,40 @@ WOLFSSL_PKCS8_PRIV_KEY_INFO* wolfSSL_d2i_PKCS8_PKEY( } if (algId == DHk) { /* Special case for DH as we expect the DER buffer to be always - * be in PKCS8 format */ + * in PKCS8 format */ rawDer.buffer = pkcs8Der->buffer; rawDer.length = inOutIdx + (word32)ret; } + #ifdef HAVE_DILITHIUM + else if ( + #ifdef WOLFSSL_DILITHIUM_FIPS204_DRAFT + (algId == DILITHIUM_LEVEL2k) || + (algId == DILITHIUM_LEVEL3k) || + (algId == DILITHIUM_LEVEL5k) || + #endif + (algId == ML_DSA_LEVEL2k) || + (algId == ML_DSA_LEVEL3k) || + (algId == ML_DSA_LEVEL5k)) { + + /* Keep full PKCS#8 wrapper for level recovery from + * AlgorithmIdentifier parameters */ + rawDer.buffer = pkcs8Der->buffer; + if (GetSequence(pkcs8Der->buffer, &outerIdx, &outerLen, + pkcs8Der->length) < 0) { + ret = ASN_PARSE_E; + } + else { + rawDer.length = outerIdx + (word32)outerLen; + } + } + #endif else { rawDer.buffer = pkcs8Der->buffer + inOutIdx; rawDer.length = (word32)ret; } - ret = 0; /* good DER */ + if (ret >= 0) { + ret = 0; /* good DER */ + } } }