diff --git a/.github/workflows/pq-all.yml b/.github/workflows/pq-all.yml index 5b370e3f5f..50b7300529 100644 --- a/.github/workflows/pq-all.yml +++ b/.github/workflows/pq-all.yml @@ -225,7 +225,15 @@ jobs: {"name": "mldsa-verify-only-dynamic-keys", "minutes": 1.2, "configure": ["--disable-intelasm", "--enable-dilithium=44,65,87,verify-only", - "CPPFLAGS=-DWOLFSSL_MLDSA_DYNAMIC_KEYS"]} + "CPPFLAGS=-DWOLFSSL_MLDSA_DYNAMIC_KEYS"]}, + {"name": "mldsa-no-asn1-opensslextra", "minutes": 1.2, + "configure": ["--enable-intelasm", "--enable-sp-asm", + "--enable-dilithium=yes", "--enable-opensslextra", + "CPPFLAGS=-DWOLFSSL_MLDSA_NO_ASN1"]}, + {"name": "mldsa-no-asn1-fips204-draft-opensslextra", "minutes": 1.2, + "configure": ["--enable-intelasm", "--enable-sp-asm", + "--enable-dilithium=yes,draft", "--enable-opensslextra", + "CPPFLAGS=-DWOLFSSL_MLDSA_NO_ASN1"]} ] EOF .github/scripts/parallel-make-check.py \ diff --git a/tests/api.c b/tests/api.c index 559ba45f1b..55f3faf952 100644 --- a/tests/api.c +++ b/tests/api.c @@ -18751,10 +18751,14 @@ static int test_wolfSSL_d2i_SSL_SESSION_bounds_check(void) #ifndef NO_BIO #if defined(OPENSSL_EXTRA) && defined(WOLFSSL_HAVE_MLDSA) -/* Verify wc_MlDsaKey auto detects the expected ML-DSA level from the OID - * in a SPKI / PKCS#8 DER buffer. Returns 0 on match. */ +/* Guarded to match its callers' build conditions, else unused. */ +#if !defined(WOLFSSL_MLDSA_NO_VERIFY) || \ + (!defined(WOLFSSL_MLDSA_NO_ASN1) && !defined(WOLFSSL_MLDSA_NO_SIGN)) +/* Verify wc_MlDsaKey auto-detects expectedLevel from a SPKI/PKCS#8 DER + * buffer. If presetLevel is non-zero, set it on the key first to also + * exercise the match/mismatch check. Returns 0 on match. */ static int check_mldsa_der_level(const byte* der, word32 derSz, - byte expectedLevel, int isPrivate) + byte expectedLevel, int isPrivate, byte presetLevel) { wc_MlDsaKey key; word32 idx = 0; @@ -18764,15 +18768,31 @@ static int check_mldsa_der_level(const byte* der, word32 derSz, (void)isPrivate; #endif - if ((rc = wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID)) != 0) { + rc = wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID); + if (rc != 0) { return rc; } -#if defined(WOLFSSL_MLDSA_PRIVATE_KEY) + if (presetLevel != 0) { + rc = wc_MlDsaKey_SetParams(&key, presetLevel); + } + + if (rc != 0) { + wc_MlDsaKey_Free(&key); + return rc; + } + +#if defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) if (isPrivate) { rc = wc_MlDsaKey_PrivateKeyDecode(&key, der, derSz, &idx); } else +#elif defined(WOLFSSL_MLDSA_PRIVATE_KEY) && defined(WOLFSSL_MLDSA_NO_ASN1) + if (isPrivate) { + /* No PrivateKeyDecode without ASN.1 support. */ + rc = NOT_COMPILED_IN; + } + else #endif { rc = wc_MlDsaKey_PublicKeyDecode(&key, der, derSz, &idx); @@ -18790,6 +18810,292 @@ static int check_mldsa_der_level(const byte* der, word32 derSz, return rc; } +#endif /* !WOLFSSL_MLDSA_NO_VERIFY || + * (!WOLFSSL_MLDSA_NO_ASN1 && !WOLFSSL_MLDSA_NO_SIGN) */ + +#if defined(WOLFSSL_MLDSA_NO_ASN1) && !defined(WOLFSSL_NO_ML_DSA_44) && \ + !defined(WOLFSSL_NO_ML_DSA_65) && !defined(WOLFSSL_MLDSA_NO_VERIFY) +/* Cover the level-preset match/mismatch branches in + * wc_MlDsaKey_PublicKeyDecode for WOLFSSL_MLDSA_NO_ASN1 builds. */ +static int test_mldsa_der_level_preset(void) +{ + EXPECT_DECLS; + + /* Preset level matches the level encoded in the DER: succeeds. */ + ExpectIntEQ(check_mldsa_der_level(mldsa44_pub_spki, + sizeof_mldsa44_pub_spki, WC_ML_DSA_44, 0, WC_ML_DSA_44), 0); + + /* Preset level does not match the level encoded in the DER: rejected. */ + ExpectIntEQ(check_mldsa_der_level(mldsa65_pub_spki, + sizeof_mldsa65_pub_spki, WC_ML_DSA_65, 0, WC_ML_DSA_44), + ASN_PARSE_E); + + /* Private-key DER decode is rejected when ASN.1 support is compiled + * out. */ +#if !defined(WOLFSSL_MLDSA_NO_SIGN) + ExpectIntEQ(check_mldsa_der_level(mldsa44_priv_only, + sizeof_mldsa44_priv_only, WC_ML_DSA_44, 1, 0), NOT_COMPILED_IN); +#endif + + return EXPECT_RESULT(); +} + +#if defined(WOLFSSL_MLDSA_FIPS204_DRAFT) +/* Build a FIPS 204 draft SPKI from a standard one by swapping in the + * 11-byte draft OID and fixing up the SEQUENCE lengths, then decode it. + * Verifies the resulting level against expectedLevel. Returns the decode + * result, or -1 if decode succeeded but the level is wrong. */ +static int check_mldsa_draft_der_level(const byte* base, word32 baseSz, + const byte* draftOid, byte expectedLevel, byte presetLevel) +{ + wc_MlDsaKey key; + byte* der = NULL; + word32 derSz = baseSz + 2; + word32 idx = 0; + word32 outerLen = 0; + int rc; + + /* All standard ML-DSA SPKIs in certs_test.h use this fixed header: + * SEQUENCE (0x30 0x82 hh ll) SEQUENCE (0x30 0x0B) OID (0x06 0x09 ...) */ + if ((baseSz < 17) || (base[0] != 0x30) || (base[1] != 0x82) || + (base[4] != 0x30) || (base[5] != 0x0B) || (base[6] != 0x06) || + (base[7] != 0x09)) { + return BAD_FUNC_ARG; + } + + der = (byte*)XMALLOC(derSz, NULL, DYNAMIC_TYPE_TMP_BUFFER); + if (der == NULL) { + return MEMORY_E; + } + + /* Outer SEQUENCE grows by two bytes for the longer draft OID. */ + outerLen = ((word32)base[2] << 8) + base[3] + 2; + der[0] = 0x30; + der[1] = 0x82; + der[2] = (byte)(outerLen >> 8); + der[3] = (byte)outerLen; + der[4] = 0x30; /* AlgorithmIdentifier SEQUENCE */ + der[5] = 0x0D; + der[6] = 0x06; /* OID */ + der[7] = 0x0B; + XMEMCPY(der + 8, draftOid, 11); + /* BIT STRING onward is unchanged. */ + XMEMCPY(der + 19, base + 17, baseSz - 17); + + rc = wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID); + if (rc == 0) { + if (presetLevel != 0) { + rc = wc_MlDsaKey_SetParams(&key, presetLevel); + } + if (rc == 0) { + rc = wc_MlDsaKey_PublicKeyDecode(&key, der, derSz, &idx); + } + if ((rc == 0) && ((key.params == NULL) || + (key.params->level != expectedLevel))) { + rc = -1; + } + wc_MlDsaKey_Free(&key); + } + + XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER); + + return rc; +} + +/* Cover draft-OID auto-detection and preset match/mismatch in + * mldsa_oid_to_level(). */ +static int test_mldsa_der_level_draft_oid(void) +{ + EXPECT_DECLS; + static const byte draftOid44[] = { + 0x2b, 0x06, 0x01, 0x04, 0x01, 0x02, 0x82, 0x0b, 0x0c, 0x04, 0x04 + }; + static const byte draftOid65[] = { + 0x2b, 0x06, 0x01, 0x04, 0x01, 0x02, 0x82, 0x0b, 0x0c, 0x06, 0x05 + }; +#ifndef WOLFSSL_NO_ML_DSA_87 + static const byte draftOid87[] = { + 0x2b, 0x06, 0x01, 0x04, 0x01, 0x02, 0x82, 0x0b, 0x0c, 0x08, 0x07 + }; +#endif + + /* Auto-detect the draft level from the draft OID. */ + ExpectIntEQ(check_mldsa_draft_der_level(mldsa44_pub_spki, + sizeof_mldsa44_pub_spki, draftOid44, WC_ML_DSA_44_DRAFT, 0), 0); + ExpectIntEQ(check_mldsa_draft_der_level(mldsa65_pub_spki, + sizeof_mldsa65_pub_spki, draftOid65, WC_ML_DSA_65_DRAFT, 0), 0); +#ifndef WOLFSSL_NO_ML_DSA_87 + ExpectIntEQ(check_mldsa_draft_der_level(mldsa87_pub_spki, + sizeof_mldsa87_pub_spki, draftOid87, WC_ML_DSA_87_DRAFT, 0), 0); +#endif + + /* Preset draft level matches the draft OID: succeeds. */ + ExpectIntEQ(check_mldsa_draft_der_level(mldsa44_pub_spki, + sizeof_mldsa44_pub_spki, draftOid44, WC_ML_DSA_44_DRAFT, + WC_ML_DSA_44_DRAFT), 0); + + /* Preset standard level does not match a draft OID: rejected. */ + ExpectIntEQ(check_mldsa_draft_der_level(mldsa44_pub_spki, + sizeof_mldsa44_pub_spki, draftOid44, WC_ML_DSA_44_DRAFT, + WC_ML_DSA_44), ASN_PARSE_E); + + /* Preset draft level does not match a different draft OID: rejected. */ + ExpectIntEQ(check_mldsa_draft_der_level(mldsa44_pub_spki, + sizeof_mldsa44_pub_spki, draftOid44, WC_ML_DSA_44_DRAFT, + WC_ML_DSA_65_DRAFT), ASN_PARSE_E); + + /* Preset draft level does not match a standard (non-draft) OID: + * rejected. */ + ExpectIntEQ(check_mldsa_der_level(mldsa44_pub_spki, + sizeof_mldsa44_pub_spki, WC_ML_DSA_44, 0, WC_ML_DSA_44_DRAFT), + ASN_PARSE_E); + + return EXPECT_RESULT(); +} +#endif /* WOLFSSL_MLDSA_FIPS204_DRAFT */ +#endif /* WOLFSSL_MLDSA_NO_ASN1 && !WOLFSSL_NO_ML_DSA_44 && + * !WOLFSSL_NO_ML_DSA_65 && !WOLFSSL_MLDSA_NO_VERIFY */ + +#if defined(WOLFSSL_MLDSA_NO_ASN1) && !defined(WOLFSSL_NO_ML_DSA_44) && \ + !defined(WOLFSSL_MLDSA_NO_VERIFY) +/* Copy mldsa44_pub_spki, apply a mutation and/or truncation to derSz, then + * decode. mutateOff < 0 means no byte mutation. Returns the decode result. */ +static int mldsa_der_mutate_and_decode(int mutateOff, byte mutateVal, + word32 derSz) +{ + byte der[sizeof(mldsa44_pub_spki)] = {0}; + int rc; + + /* Clamp so static analysis sees der's bounds respected. */ + if (derSz > sizeof(der)) { + derSz = sizeof(der); + } + + XMEMCPY(der, mldsa44_pub_spki, derSz); + if ((mutateOff >= 0) && ((word32)mutateOff < sizeof(der))) { + der[mutateOff] = mutateVal; + } + + rc = check_mldsa_der_level(der, derSz, WC_ML_DSA_44, 0, 0); + + return rc; +} + +/* Malformed/truncated DER coverage for the WOLFSSL_MLDSA_NO_ASN1 SPKI + * parser's bounds checks and zero-length BIT STRING guard. */ +static int test_mldsa_der_level_preset_malformed(void) +{ + EXPECT_DECLS; + word32 fullSz = (word32)sizeof_mldsa44_pub_spki; + + /* Empty buffer is rejected before any parsing is attempted. */ + ExpectIntEQ(mldsa_der_mutate_and_decode(-1, 0, 0), BAD_FUNC_ARG); + + /* Truncated buffers at various lengths, all rejected by + * mldsa_get_der_length()'s bounds check. */ + ExpectIntEQ(mldsa_der_mutate_and_decode(-1, 0, 1), ASN_PARSE_E); + ExpectIntEQ(mldsa_der_mutate_and_decode(-1, 0, 4), ASN_PARSE_E); + ExpectIntEQ(mldsa_der_mutate_and_decode(-1, 0, 6), ASN_PARSE_E); + ExpectIntEQ(mldsa_der_mutate_and_decode(-1, 0, 10), ASN_PARSE_E); + ExpectIntEQ(mldsa_der_mutate_and_decode(-1, 0, 19), ASN_PARSE_E); + ExpectIntEQ(mldsa_der_mutate_and_decode(-1, 0, fullSz - 1), ASN_PARSE_E); + + /* Wrong outer SEQUENCE tag. */ + ExpectIntEQ(mldsa_der_mutate_and_decode(0, 0x31, fullSz), ASN_PARSE_E); + /* Wrong inner SEQUENCE (AlgorithmIdentifier) tag. */ + ExpectIntEQ(mldsa_der_mutate_and_decode(4, 0x02, fullSz), ASN_PARSE_E); + /* Wrong OID tag. */ + ExpectIntEQ(mldsa_der_mutate_and_decode(6, 0x04, fullSz), ASN_PARSE_E); + /* Wrong BIT STRING tag. */ + ExpectIntEQ(mldsa_der_mutate_and_decode(17, 0x04, fullSz), ASN_PARSE_E); + /* Non-zero unused-bits byte in the BIT STRING. */ + ExpectIntEQ(mldsa_der_mutate_and_decode(21, 0x01, fullSz), ASN_PARSE_E); + /* BIT STRING length claims more bytes than remain in the buffer. */ + ExpectIntEQ(mldsa_der_mutate_and_decode(20, 0x7F, fullSz), ASN_PARSE_E); + + /* Well-formed OID TLV whose content matches no known OID, exercising + * mldsa_oid_to_level()'s "no match" fallback. */ + ExpectIntEQ(mldsa_der_mutate_and_decode(16, 0x00, fullSz), ASN_PARSE_E); + + /* Zero-length BIT STRING (tag 0x03, length 0x00, no unused-bits byte). + * Without the length == 0 guard this reads one byte past the end. */ + { + byte der[17]; + + der[0] = 0x30; /* outer SEQUENCE tag */ + der[1] = 0x0F; /* outer length: inner SEQUENCE (13) + BIT STRING (2) */ + der[2] = 0x30; /* inner SEQUENCE (AlgorithmIdentifier) tag */ + der[3] = 0x0B; /* inner length: OID tag+length+content (11) */ + der[4] = 0x06; /* OID tag */ + der[5] = 0x09; /* OID length */ + XMEMCPY(der + 6, mldsa44_pub_spki + 8, 9); /* OID content */ + der[15] = 0x03; /* BIT STRING tag */ + der[16] = 0x00; /* BIT STRING length 0 */ + + ExpectIntEQ(check_mldsa_der_level(der, sizeof(der), WC_ML_DSA_44, + 0, 0), ASN_PARSE_E); + } + + /* Outer SEQUENCE claims 4 bytes more than the BIT STRING fills; only + * the idx + length == outerEnd check at the end catches this. */ + { + byte der[sizeof(mldsa44_pub_spki) + 4]; + word32 outerLen; + + XMEMCPY(der, mldsa44_pub_spki, sizeof(mldsa44_pub_spki)); + XMEMSET(der + sizeof(mldsa44_pub_spki), 0, 4); + + /* Outer SEQUENCE uses the long form (0x30 0x82 hh ll); inflate + * the declared length by 4 to claim the trailing padding as + * part of the outer SEQUENCE's content. */ + outerLen = ((word32)der[2] << 8) + der[3] + 4; + der[2] = (byte)(outerLen >> 8); + der[3] = (byte)outerLen; + + ExpectIntEQ(check_mldsa_der_level(der, sizeof(der), WC_ML_DSA_44, + 0, 0), ASN_PARSE_E); + } + + return EXPECT_RESULT(); +} + +/* Verify wc_MlDsaKey_PublicKeyDecode() honors a non-zero starting + * *inOutIdx and shifts the output index by the same offset, rather than + * assuming the DER starts at 0 or double-adding the offset. */ +static int test_mldsa_der_level_nonzero_idx(void) +{ + EXPECT_DECLS; + wc_MlDsaKey key; + byte der[4 + sizeof(mldsa44_pub_spki)]; + word32 idx; + word32 baseIdx = 0; + int rc; + + rc = wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID); + ExpectIntEQ(rc, 0); + if (rc == 0) { + ExpectIntEQ(wc_MlDsaKey_PublicKeyDecode(&key, mldsa44_pub_spki, + (word32)sizeof_mldsa44_pub_spki, &baseIdx), 0); + wc_MlDsaKey_Free(&key); + } + + XMEMSET(der, 0xFF, 4); + XMEMCPY(der + 4, mldsa44_pub_spki, sizeof(mldsa44_pub_spki)); + idx = 4; + + rc = wc_MlDsaKey_Init(&key, NULL, INVALID_DEVID); + ExpectIntEQ(rc, 0); + if (rc == 0) { + ExpectIntEQ(wc_MlDsaKey_PublicKeyDecode(&key, der, sizeof(der), + &idx), 0); + ExpectIntEQ((int)idx, (int)(baseIdx + 4)); + wc_MlDsaKey_Free(&key); + } + + return EXPECT_RESULT(); +} +#endif /* WOLFSSL_MLDSA_NO_ASN1 && !WOLFSSL_NO_ML_DSA_44 && + * !WOLFSSL_MLDSA_NO_VERIFY */ #endif /* OPENSSL_EXTRA && WOLFSSL_HAVE_MLDSA */ static int test_wolfSSL_d2i_PUBKEY(void) @@ -18858,7 +19164,7 @@ defined(OPENSSL_EXTRA) && defined(WOLFSSL_DH_EXTRA) ExpectNotNull(pkey = d2i_PUBKEY_bio(bio, NULL)); ExpectIntEQ(EVP_PKEY_id(pkey), EVP_PKEY_DILITHIUM); ExpectIntEQ(check_mldsa_der_level(mldsa44_pub_spki, - sizeof_mldsa44_pub_spki, WC_ML_DSA_44, 0), 0); + sizeof_mldsa44_pub_spki, WC_ML_DSA_44, 0, 0), 0); EVP_PKEY_free(pkey); pkey = NULL; #endif @@ -18877,7 +19183,7 @@ defined(OPENSSL_EXTRA) && defined(WOLFSSL_DH_EXTRA) ExpectNotNull(pkey = d2i_PUBKEY_bio(bio, NULL)); ExpectIntEQ(EVP_PKEY_id(pkey), EVP_PKEY_DILITHIUM); ExpectIntEQ(check_mldsa_der_level(mldsa65_pub_spki, - sizeof_mldsa65_pub_spki, WC_ML_DSA_65, 0), 0); + sizeof_mldsa65_pub_spki, WC_ML_DSA_65, 0, 0), 0); EVP_PKEY_free(pkey); pkey = NULL; #endif @@ -18896,7 +19202,7 @@ defined(OPENSSL_EXTRA) && defined(WOLFSSL_DH_EXTRA) ExpectNotNull(pkey = d2i_PUBKEY_bio(bio, NULL)); ExpectIntEQ(EVP_PKEY_id(pkey), EVP_PKEY_DILITHIUM); ExpectIntEQ(check_mldsa_der_level(mldsa87_pub_spki, - sizeof_mldsa87_pub_spki, WC_ML_DSA_87, 0), 0); + sizeof_mldsa87_pub_spki, WC_ML_DSA_87, 0, 0), 0); EVP_PKEY_free(pkey); pkey = NULL; #endif @@ -19028,6 +19334,7 @@ static int test_wolfSSL_d2i_PrivateKeys_bio(void) BIO_free(bio); bio = NULL; +#ifndef WOLFSSL_MLDSA_NO_ASN1 /* ML-DSA-44 PrivateKey test (LAMPS PKCS#8 priv-only DER) */ ExpectNotNull(bio = BIO_new(BIO_s_mem())); ExpectIntGT(BIO_write(bio, mldsa44_priv_only, @@ -19035,7 +19342,7 @@ static int test_wolfSSL_d2i_PrivateKeys_bio(void) ExpectNotNull(pkey = d2i_PrivateKey_bio(bio, NULL)); ExpectIntEQ(EVP_PKEY_id(pkey), EVP_PKEY_DILITHIUM); ExpectIntEQ(check_mldsa_der_level(mldsa44_priv_only, - sizeof_mldsa44_priv_only, WC_ML_DSA_44, 1), 0); + sizeof_mldsa44_priv_only, WC_ML_DSA_44, 1, 0), 0); EVP_PKEY_free(pkey); pkey = NULL; BIO_free(bio); @@ -19065,6 +19372,16 @@ static int test_wolfSSL_d2i_PrivateKeys_bio(void) BIO_free(bio); bio = NULL; #endif +#else /* WOLFSSL_MLDSA_NO_ASN1 */ + /* d2i_PrivateKey_bio must reject DER private keys without ASN.1 + * support. */ + ExpectNotNull(bio = BIO_new(BIO_s_mem())); + ExpectIntGT(BIO_write(bio, mldsa44_priv_only, + sizeof_mldsa44_priv_only), 0); + ExpectNull(pkey = d2i_PrivateKey_bio(bio, NULL)); + BIO_free(bio); + bio = NULL; +#endif /* !WOLFSSL_MLDSA_NO_ASN1 */ #endif #if !defined(WOLFSSL_NO_ML_DSA_65) @@ -19079,6 +19396,7 @@ static int test_wolfSSL_d2i_PrivateKeys_bio(void) BIO_free(bio); bio = NULL; +#ifndef WOLFSSL_MLDSA_NO_ASN1 /* ML-DSA-65 PrivateKey test (LAMPS PKCS#8 priv-only DER) */ ExpectNotNull(bio = BIO_new(BIO_s_mem())); ExpectIntGT(BIO_write(bio, mldsa65_priv_only, @@ -19086,7 +19404,7 @@ static int test_wolfSSL_d2i_PrivateKeys_bio(void) ExpectNotNull(pkey = d2i_PrivateKey_bio(bio, NULL)); ExpectIntEQ(EVP_PKEY_id(pkey), EVP_PKEY_DILITHIUM); ExpectIntEQ(check_mldsa_der_level(mldsa65_priv_only, - sizeof_mldsa65_priv_only, WC_ML_DSA_65, 1), 0); + sizeof_mldsa65_priv_only, WC_ML_DSA_65, 1, 0), 0); EVP_PKEY_free(pkey); pkey = NULL; BIO_free(bio); @@ -19116,6 +19434,16 @@ static int test_wolfSSL_d2i_PrivateKeys_bio(void) BIO_free(bio); bio = NULL; #endif +#else /* WOLFSSL_MLDSA_NO_ASN1 */ + /* d2i_PrivateKey_bio must reject DER private keys without ASN.1 + * support. */ + ExpectNotNull(bio = BIO_new(BIO_s_mem())); + ExpectIntGT(BIO_write(bio, mldsa65_priv_only, + sizeof_mldsa65_priv_only), 0); + ExpectNull(pkey = d2i_PrivateKey_bio(bio, NULL)); + BIO_free(bio); + bio = NULL; +#endif /* !WOLFSSL_MLDSA_NO_ASN1 */ #endif #if !defined(WOLFSSL_NO_ML_DSA_87) @@ -19130,6 +19458,7 @@ static int test_wolfSSL_d2i_PrivateKeys_bio(void) BIO_free(bio); bio = NULL; +#ifndef WOLFSSL_MLDSA_NO_ASN1 /* ML-DSA-87 PrivateKey test (LAMPS PKCS#8 priv-only DER) */ ExpectNotNull(bio = BIO_new(BIO_s_mem())); ExpectIntGT(BIO_write(bio, mldsa87_priv_only, @@ -19137,7 +19466,7 @@ static int test_wolfSSL_d2i_PrivateKeys_bio(void) ExpectNotNull(pkey = d2i_PrivateKey_bio(bio, NULL)); ExpectIntEQ(EVP_PKEY_id(pkey), EVP_PKEY_DILITHIUM); ExpectIntEQ(check_mldsa_der_level(mldsa87_priv_only, - sizeof_mldsa87_priv_only, WC_ML_DSA_87, 1), 0); + sizeof_mldsa87_priv_only, WC_ML_DSA_87, 1, 0), 0); EVP_PKEY_free(pkey); pkey = NULL; BIO_free(bio); @@ -19167,6 +19496,16 @@ static int test_wolfSSL_d2i_PrivateKeys_bio(void) BIO_free(bio); bio = NULL; #endif +#else /* WOLFSSL_MLDSA_NO_ASN1 */ + /* d2i_PrivateKey_bio must reject DER private keys without ASN.1 + * support. */ + ExpectNotNull(bio = BIO_new(BIO_s_mem())); + ExpectIntGT(BIO_write(bio, mldsa87_priv_only, + sizeof_mldsa87_priv_only), 0); + ExpectNull(pkey = d2i_PrivateKey_bio(bio, NULL)); + BIO_free(bio); + bio = NULL; +#endif /* !WOLFSSL_MLDSA_NO_ASN1 */ #endif #endif /* WOLFSSL_HAVE_MLDSA && !NO_SIGN */ @@ -35428,6 +35767,20 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_d2i_and_i2d_PublicKey_ecc), #ifndef NO_BIO TEST_DECL(test_wolfSSL_d2i_PUBKEY), +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WOLFSSL_MLDSA_NO_ASN1) && !defined(WOLFSSL_NO_ML_DSA_44) && \ + !defined(WOLFSSL_NO_ML_DSA_65) && !defined(WOLFSSL_MLDSA_NO_VERIFY) + TEST_DECL(test_mldsa_der_level_preset), +#if defined(WOLFSSL_MLDSA_FIPS204_DRAFT) + TEST_DECL(test_mldsa_der_level_draft_oid), +#endif +#endif +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_HAVE_MLDSA) && \ + defined(WOLFSSL_MLDSA_NO_ASN1) && !defined(WOLFSSL_NO_ML_DSA_44) && \ + !defined(WOLFSSL_MLDSA_NO_VERIFY) + TEST_DECL(test_mldsa_der_level_preset_malformed), + TEST_DECL(test_mldsa_der_level_nonzero_idx), +#endif #endif TEST_DECL(test_wolfSSL_d2i_and_i2d_DSAparams), TEST_DECL(test_wolfSSL_i2d_PrivateKey), diff --git a/wolfcrypt/src/evp_pk.c b/wolfcrypt/src/evp_pk.c index 777f012e3c..e4ae5f918b 100644 --- a/wolfcrypt/src/evp_pk.c +++ b/wolfcrypt/src/evp_pk.c @@ -980,6 +980,9 @@ static int d2iTryFalconKey(WOLFSSL_EVP_PKEY** out, const unsigned char* mem, * size-keyed, so each level is tried in turn. DER input is decoded once, * letting the decoder auto-detect the level from the OID. * + * Under WOLFSSL_MLDSA_NO_ASN1 there is no PKCS#8 decoder, so a DER private + * key is always treated as not this key type; only raw bytes are accepted. + * * @param [in, out] out On in, an EVP PKEY or NULL. * On out, an EVP PKEY or NULL. * @param [in] mem Memory containing key data. @@ -1041,11 +1044,17 @@ static int d2iTryMlDsaKey(WOLFSSL_EVP_PKEY** out, const unsigned char* mem, WC_FREE_VAR_EX(mldsa, NULL, DYNAMIC_TYPE_MLDSA); return 0; } - #if defined(WOLFSSL_MLDSA_PRIVATE_KEY) + #if defined(WOLFSSL_MLDSA_PRIVATE_KEY) && !defined(WOLFSSL_MLDSA_NO_ASN1) if (priv) { rc = wc_MlDsaKey_PrivateKeyDecode(mldsa, mem, inSz, &keyIdx); } else + #elif defined(WOLFSSL_MLDSA_PRIVATE_KEY) && defined(WOLFSSL_MLDSA_NO_ASN1) + if (priv) { + /* No PrivateKeyDecode without ASN.1 support. */ + rc = NOT_COMPILED_IN; + } + else #endif { rc = wc_MlDsaKey_PublicKeyDecode(mldsa, mem, inSz, &keyIdx); diff --git a/wolfcrypt/src/wc_mldsa.c b/wolfcrypt/src/wc_mldsa.c index 8431fb3ae6..9ba9b177f8 100644 --- a/wolfcrypt/src/wc_mldsa.c +++ b/wolfcrypt/src/wc_mldsa.c @@ -12451,6 +12451,66 @@ static int mldsa_check_type(const byte* input, word32* inOutIdx, byte type, return ret; } +/* Map an AlgorithmIdentifier OID byte string to a security level. + * Used to auto-detect the level when it is not already set on the key. */ +static int mldsa_oid_to_level(const byte* oid, word32 oidLen, byte* level) +{ + int ret = WC_NO_ERR_TRACE(ASN_PARSE_E); + +#if defined(WOLFSSL_MLDSA_FIPS204_DRAFT) +#ifndef WOLFSSL_NO_ML_DSA_44 + if ((oidLen == sizeof(mldsa_oid_44)) && + (XMEMCMP(oid, mldsa_oid_44, oidLen) == 0)) { + *level = WC_ML_DSA_44_DRAFT; + ret = 0; + } +#endif +#ifndef WOLFSSL_NO_ML_DSA_65 +#ifndef WOLFSSL_NO_ML_DSA_44 + else +#endif + if ((oidLen == sizeof(mldsa_oid_65)) && + (XMEMCMP(oid, mldsa_oid_65, oidLen) == 0)) { + *level = WC_ML_DSA_65_DRAFT; + ret = 0; + } +#endif +#ifndef WOLFSSL_NO_ML_DSA_87 +#if !defined(WOLFSSL_NO_ML_DSA_44) || !defined(WOLFSSL_NO_ML_DSA_65) + else +#endif + if ((oidLen == sizeof(mldsa_oid_87)) && + (XMEMCMP(oid, mldsa_oid_87, oidLen) == 0)) { + *level = WC_ML_DSA_87_DRAFT; + ret = 0; + } +#endif +#endif /* WOLFSSL_MLDSA_FIPS204_DRAFT */ +#ifndef WOLFSSL_NO_ML_DSA_44 + if ((ret != 0) && (oidLen == sizeof(ml_dsa_oid_44)) && + (XMEMCMP(oid, ml_dsa_oid_44, oidLen) == 0)) { + *level = WC_ML_DSA_44; + ret = 0; + } +#endif +#ifndef WOLFSSL_NO_ML_DSA_65 + if ((ret != 0) && (oidLen == sizeof(ml_dsa_oid_65)) && + (XMEMCMP(oid, ml_dsa_oid_65, oidLen) == 0)) { + *level = WC_ML_DSA_65; + ret = 0; + } +#endif +#ifndef WOLFSSL_NO_ML_DSA_87 + if ((ret != 0) && (oidLen == sizeof(ml_dsa_oid_87)) && + (XMEMCMP(oid, ml_dsa_oid_87, oidLen) == 0)) { + *level = WC_ML_DSA_87; + ret = 0; + } +#endif + + return ret; +} + #endif /* WOLFSSL_MLDSA_NO_ASN1 */ /* Decode the DER encoded ML-DSA public key. @@ -12466,13 +12526,13 @@ static int mldsa_check_type(const byte* input, word32* inOutIdx, byte type, * input, the level will be detected from the DER * file based on the algorithm OID, appropriately * decoded, then updated in the key structure on - * output. Auto-detection of the security level is - * not supported if compiled for FIPS 204 - * draft mode. + * output. Auto-detection is not supported for + * FIPS 204 draft mode unless WOLFSSL_MLDSA_NO_ASN1 + * is also defined. * @param [in] inSz Total size of data in array. * @return 0 on success. * @return BAD_FUNC_ARG when input, inOutIdx or key is NULL or inSz is 0. - * @return BAD_FUNC_ARG when level not set. + * @return BAD_FUNC_ARG when level not set and auto-detection unsupported. * @return Other negative on parse error. */ int wc_MlDsaKey_PublicKeyDecode(wc_MlDsaKey* key, const byte* input, @@ -12495,9 +12555,10 @@ int wc_MlDsaKey_PublicKeyDecode(wc_MlDsaKey* key, const byte* input, int keyType = 0; #else int length = 0; - unsigned char* oid = NULL; + const unsigned char* oid = NULL; word32 oidLen = 0; - word32 idx = 0; + word32 idx = *inOutIdx; + word32 outerEnd = 0; #endif /* Start again. */ @@ -12547,59 +12608,8 @@ int wc_MlDsaKey_PublicKeyDecode(wc_MlDsaKey* key, const byte* input, } } #else - /* Get OID sum for level. */ - #if defined(WOLFSSL_MLDSA_FIPS204_DRAFT) - if (key->params == NULL) { - ret = BAD_FUNC_ARG; - } - else - #ifndef WOLFSSL_NO_ML_DSA_44 - if (key->params->level == WC_ML_DSA_44_DRAFT) { - oid = mldsa_oid_44; - oidLen = (word32)sizeof(mldsa_oid_44); - } - else - #endif - #ifndef WOLFSSL_NO_ML_DSA_65 - if (key->params->level == WC_ML_DSA_65_DRAFT) { - oid = mldsa_oid_65; - oidLen = (word32)sizeof(mldsa_oid_65); - } - else - #endif - #ifndef WOLFSSL_NO_ML_DSA_87 - if (key->params->level == WC_ML_DSA_87_DRAFT) { - oid = mldsa_oid_87; - oidLen = (word32)sizeof(mldsa_oid_87); - } - else - #endif - #endif - #ifndef WOLFSSL_NO_ML_DSA_44 - if (key->level == WC_ML_DSA_44) { - oid = ml_dsa_oid_44; - oidLen = (word32)sizeof(ml_dsa_oid_44); - } - else - #endif - #ifndef WOLFSSL_NO_ML_DSA_65 - if (key->level == WC_ML_DSA_65) { - oid = ml_dsa_oid_65; - oidLen = (word32)sizeof(ml_dsa_oid_65); - } - else - #endif - #ifndef WOLFSSL_NO_ML_DSA_87 - if (key->level == WC_ML_DSA_87) { - oid = ml_dsa_oid_87; - oidLen = (word32)sizeof(ml_dsa_oid_87); - } - else - #endif - { - /* Level not set. */ - ret = BAD_FUNC_ARG; - } + /* Parse: SEQUENCE { SEQUENCE { OID } BIT STRING }, detecting the + * level from the OID. */ if (ret == 0) { ret = mldsa_check_type(input, &idx, 0x30, inSz); } @@ -12607,43 +12617,81 @@ int wc_MlDsaKey_PublicKeyDecode(wc_MlDsaKey* key, const byte* input, ret = mldsa_get_der_length(input, &idx, &length, inSz); } if (ret == 0) { - ret = mldsa_check_type(input, &idx, 0x30, inSz); - } - if (ret == 0) { - ret = mldsa_get_der_length(input, &idx, &length, inSz); - } - if (ret == 0) { - ret = mldsa_check_type(input, &idx, 0x06, inSz); - } - if (ret == 0) { - ret = mldsa_get_der_length(input, &idx, &length, inSz); - } - if (ret == 0) { - if (((word32)length != oidLen) || - (XMEMCMP(input + idx, oid, oidLen) != 0)) { + outerEnd = idx + (word32)length; + /* Reject trailing bytes after the outer SEQUENCE. */ + if (outerEnd != inSz) { ret = ASN_PARSE_E; } + } + if (ret == 0) { + ret = mldsa_check_type(input, &idx, 0x30, outerEnd); + } + if (ret == 0) { + ret = mldsa_get_der_length(input, &idx, &length, outerEnd); + } + if (ret == 0) { + ret = mldsa_check_type(input, &idx, 0x06, outerEnd); + } + if (ret == 0) { + ret = mldsa_get_der_length(input, &idx, &length, outerEnd); + } + if (ret == 0) { + oid = input + idx; + oidLen = (word32)length; idx += oidLen; } if (ret == 0) { - ret = mldsa_check_type(input, &idx, 0x03, inSz); + byte detectedLevel = 0; + byte curLevel = 0; + + ret = mldsa_oid_to_level(oid, oidLen, &detectedLevel); + #if defined(WOLFSSL_MLDSA_FIPS204_DRAFT) + if ((ret == 0) && (key->params != NULL)) { + curLevel = key->params->level; + } + #else + if (ret == 0) { + curLevel = key->level; + } + #endif + if (ret == 0) { + if (curLevel == 0) { + /* Level not set by caller - use the detected one. */ + ret = wc_MlDsaKey_SetParams(key, detectedLevel); + } + else if (curLevel != detectedLevel) { + ret = ASN_PARSE_E; + } + } } if (ret == 0) { - ret = mldsa_get_der_length(input, &idx, &length, inSz); + ret = mldsa_check_type(input, &idx, 0x03, outerEnd); } if (ret == 0) { - if ((input[idx] != 0) || (length == 0)) { + ret = mldsa_get_der_length(input, &idx, &length, outerEnd); + } + if ((ret == 0) && (length == 0)) { + /* Reject before reading input[idx] below, else a + * zero-length BIT STRING causes a one-byte over-read. */ + ret = ASN_PARSE_E; + } + if (ret == 0) { + if (input[idx] != 0) { ret = ASN_PARSE_E; } idx++; length--; } + if ((ret == 0) && ((idx + (word32)length) != outerEnd)) { + /* BIT STRING must exactly fill the outer SEQUENCE. */ + ret = ASN_PARSE_E; + } if (ret == 0) { /* This is the raw point data compressed or uncompressed. */ pubKeyLen = (word32)length; pubKey = input + idx; - *inOutIdx += idx; + *inOutIdx = idx; } #endif if (ret == 0) {