Skip to content

Commit bc32882

Browse files
committed
PKCS#7: add ML-DSA (FIPS 204) SignedData support
Add ML-DSA signing and verification for CMS/PKCS#7 SignedData, following RFC 9882. ML-DSA is used in CMS "pure" mode: the signature is computed over the complete message (the DER SET OF signed attributes, or the eContent when none are present) with an empty context string and absent signatureAlgorithm parameters, rather than over a pre-computed DigestInfo as with RSA/ECDSA. wolfcrypt/src/pkcs7.c: - New ML-DSA helpers: wc_PKCS7_MlDsaLevelFromOID, wc_PKCS7_BuildPureSigMessage, wc_PKCS7_MlDsaSign and wc_PKCS7_MlDsaVerify, wired into the per-algorithm switch sites (GetSignSize, SignedDataGetEncAlgoId, SetPublicKeyOID, CheckPublicKeyDer) and the sign/verify dispatchers. Only the final FIPS 204 ML-DSA OIDs are accepted; pre-standard draft Dilithium OIDs are not. - GetSignSize derives the ML-DSA signature length from the parameter set. - InitWithCert copies the signer public key into the RSA-sized publicKey buffer only for RSA/ECC certs (the raw-sign callback consumers); large PQC keys such as ML-DSA would overflow it and are never read back, so publicKeySz stays 0. - wc_MlDsaKey is always heap allocated (it embeds multi-KB key buffers); the accompanying DecodedCert uses the WC_DECLARE_VAR/WC_ALLOC_VAR_EX macros for stack-vs-heap handling under WOLFSSL_SMALL_STACK. - wc_PKCS7_SignedDataBuildSignature skips building the DigestInfo for ML-DSA, which signs the full message in pure mode and never consumes it. - wc_PKCS7_MlDsaSign wraps the ML-DSA private-key decode in PRIVATE_KEY_UNLOCK/PRIVATE_KEY_LOCK. Unlike RSA/ECC, the FIPS module gates wc_MlDsaKey_PrivateKeyDecode behind the private-key read lock, so signing would otherwise fail with FIPS_PRIVATE_KEY_LOCKED_E under --enable-fips. The macros are no-ops in non-FIPS builds. wolfssl/wolfcrypt/pkcs7.h: - Document that the fixed-size signer public key buffer (publicKey/publicKeySz) holds only RSA/ECC keys; it stays RSA-sized. wolfcrypt/src/hash.c: - Map the SHAKE128/SHAKE256 OIDs to their hash types in wc_OidGetHash(). certs/mldsa: - Add expanded-only PKCS#8 DER private keys (mldsa44/65/87-key.der) matching the self-signed ML-DSA certificates, with README and include.am updates. The expanded-only shape (no seed) decodes via wc_MlDsaKey_ImportPrivRaw without keygen-from-seed or the ASN template, so pkcs7signed_mldsa_test also passes in WOLFSSL_MLDSA_NO_MAKE_KEY and non-WOLFSSL_ASN_TEMPLATE builds. certs/renewcerts.sh: - Generate the mldsa<N>-key.der files from the matching mldsa<N>-key.pem in the expanded-only shape (openssl pkey -provparam ml-dsa.output_formats=priv), so a regeneration keeps the DER key in step with the cert. The OpenSSL detection probe now requires both ML-DSA keygen and that conversion across all three levels, so the block runs fully (matched cert+key) or is skipped entirely rather than aborting mid-way. wolfcrypt/test/test.c: - Add pkcs7signed_mldsa_test(): round-trip encode/verify of SignedData across ML-DSA-44/65/87, with and without signed attributes, including a check that the digest algorithm parameters are encoded as expected. The message-digest OID is selected from the enabled hash set (SHA-512, else SHA-256, else SHA-1) so the test builds when SHA-512 is disabled. A negative case confirms ML-DSA rejects a caller-supplied pre-computed content hash with BAD_FUNC_ARG.
1 parent 9e72e8d commit bc32882

13 files changed

Lines changed: 1148 additions & 41 deletions

File tree

.github/workflows/pq-all.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,11 @@ jobs:
233233
{"name": "mldsa-no-asn1-fips204-draft-opensslextra", "minutes": 1.2,
234234
"configure": ["--enable-intelasm", "--enable-sp-asm",
235235
"--enable-dilithium=yes,draft", "--enable-opensslextra",
236-
"CPPFLAGS=-DWOLFSSL_MLDSA_NO_ASN1"]}
236+
"CPPFLAGS=-DWOLFSSL_MLDSA_NO_ASN1"]},
237+
{"name": "pkcs7-mldsa-only", "minutes": 1.2,
238+
"comment": "PKCS#7 SignedData with ML-DSA as the only signature algorithm (no RSA, no ECC); guards the ML-DSA-only PKCS7 build path",
239+
"configure": ["--enable-cryptonly", "--enable-mldsa",
240+
"--enable-pkcs7", "--disable-rsa", "--disable-ecc"]}
237241
]
238242
EOF
239243
.github/scripts/parallel-make-check.py \

certs/mldsa/README.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ File variants, per level N in {44, 65, 87}:
99
mldsa<N>_oqskeypair.der liboqs concatenated (priv || pub) format
1010
mldsa<N>_pub-spki.der SubjectPublicKeyInfo wrapping the public key
1111

12+
Self-signed certificates and their matching keys (used by the PKCS#7/CMS
13+
SignedData tests), per level N in {44, 65, 87}:
14+
mldsa<N>-cert.pem / mldsa<N>-cert.der self-signed ML-DSA certificate
15+
mldsa<N>-key.pem matching private key (PEM,
16+
seed-and-expanded PKCS#8)
17+
mldsa<N>-key.der matching private key (DER,
18+
expanded-only PKCS#8)
19+
20+
The mldsa<N>-key.der files were derived from the matching mldsa<N>-key.pem
21+
using OpenSSL 3.5+, selecting the portable expanded-only private-key shape:
22+
23+
openssl pkey -in mldsa<N>-key.pem \
24+
-provparam ml-dsa.output_formats=priv -outform DER \
25+
-out mldsa<N>-key.der
26+
27+
Unlike the standalone mldsa<N>_priv-only.der vectors above, these correspond
28+
to the public key in mldsa<N>-cert.der.
29+
1230
The *_pub-spki.der files were derived from the matching *_priv-only.der files
1331
using OpenSSL 3.5+:
1432

certs/mldsa/include.am

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ EXTRA_DIST += \
2626
certs/mldsa/mldsa87_bare-seed.der \
2727
certs/mldsa/mldsa87_bare-priv.der \
2828
certs/mldsa/mldsa44-key.pem \
29+
certs/mldsa/mldsa44-key.der \
2930
certs/mldsa/mldsa44-cert.pem \
3031
certs/mldsa/mldsa44-cert.der \
3132
certs/mldsa/ecc-leaf-mldsa44.pem \
3233
certs/mldsa/mldsa65-key.pem \
34+
certs/mldsa/mldsa65-key.der \
3335
certs/mldsa/mldsa65-cert.pem \
3436
certs/mldsa/mldsa65-cert.der \
3537
certs/mldsa/mldsa87-key.pem \
38+
certs/mldsa/mldsa87-key.der \
3639
certs/mldsa/mldsa87-cert.pem \
3740
certs/mldsa/mldsa87-cert.der \
3841
certs/mldsa/bench_mldsa_44_key.der \

certs/mldsa/mldsa44-key.der

2.53 KB
Binary file not shown.

certs/mldsa/mldsa65-key.der

3.96 KB
Binary file not shown.

certs/mldsa/mldsa87-key.der

4.81 KB
Binary file not shown.

certs/renewcerts.sh

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,13 +1187,19 @@ EOF
11871187
############################################################
11881188
#### ML-DSA (FIPS 204) self-signed certificates ###
11891189
############################################################
1190-
# ML-DSA requires an OpenSSL 3.x binary with ML-DSA support
1191-
# (via oqsprovider or built-in). Detect support by probing candidates.
1190+
# ML-DSA requires an OpenSSL 3.5+ binary with the built-in ML-DSA provider.
1191+
# Besides key/cert generation the block also produces the expanded-only
1192+
# PKCS#8 key.der (-provparam ml-dsa.output_formats=priv, a 3.5+ built-in
1193+
# construct) that the PKCS#7 tests decode without keygen-from-seed. The
1194+
# probe below requires both keygen and that conversion, so the common
1195+
# unsuitable binaries (oqsprovider or pre-3.5, which lack the expanded-only
1196+
# conversion) are rejected here and the block is skipped cleanly rather
1197+
# than aborting after writing a cert.der but no matching key.der.
11921198
OPENSSL3=""
11931199
for candidate in \
1194-
"/usr/local/opt/openssl@3.2/bin/openssl" \
1200+
"/usr/local/opt/openssl@3.5/bin/openssl" \
11951201
"/usr/local/opt/openssl@3/bin/openssl" \
1196-
"/opt/homebrew/opt/openssl@3.2/bin/openssl" \
1202+
"/opt/homebrew/opt/openssl@3.5/bin/openssl" \
11971203
"/opt/homebrew/opt/openssl@3/bin/openssl" \
11981204
"openssl"; do
11991205
if [ "$candidate" = "openssl" ]; then
@@ -1203,7 +1209,23 @@ EOF
12031209
# Skip non-existent or non-executable absolute paths.
12041210
[ -x "$candidate" ] || continue
12051211
fi
1206-
if "$candidate" genpkey -algorithm mldsa44 -out /dev/null 2>/dev/null; then
1212+
probe_key="$(mktemp)" || continue
1213+
# Probe every level the loop below generates (44/65/87), for both
1214+
# keygen and the expanded-only conversion, so a partially-capable
1215+
# binary is rejected up front rather than aborting mid-loop.
1216+
probe_ok=1
1217+
for probe_level in 44 65 87; do
1218+
if ! "$candidate" genpkey -algorithm "mldsa${probe_level}" \
1219+
-out "$probe_key" 2>/dev/null || \
1220+
! "$candidate" pkey -in "$probe_key" \
1221+
-provparam ml-dsa.output_formats=priv -outform DER \
1222+
-out /dev/null 2>/dev/null; then
1223+
probe_ok=0
1224+
break
1225+
fi
1226+
done
1227+
rm -f "$probe_key"
1228+
if [ "$probe_ok" -eq 1 ]; then
12071229
OPENSSL3="$candidate"
12081230
break
12091231
fi
@@ -1230,6 +1252,19 @@ EOF
12301252
-outform DER -out "mldsa/mldsa${level}-cert.der"
12311253
check_result $? "ML-DSA-${level} DER conversion"
12321254

1255+
# Matching private key in the portable expanded-only PKCS#8 DER
1256+
# shape, used by the PKCS#7/CMS SignedData tests. Derived from the
1257+
# same mldsa${level}-key.pem so it corresponds to the public key in
1258+
# mldsa${level}-cert.der. The expanded-only form (no seed) decodes
1259+
# via ImportPrivRaw without keygen-from-seed or the ASN template,
1260+
# so the tests pass in WOLFSSL_MLDSA_NO_MAKE_KEY and non-template
1261+
# builds too; the seed-and-expanded default would not. The probe
1262+
# above already verified this binary supports the conversion.
1263+
"$OPENSSL3" pkey -in "mldsa/mldsa${level}-key.pem" \
1264+
-provparam ml-dsa.output_formats=priv -outform DER \
1265+
-out "mldsa/mldsa${level}-key.der"
1266+
check_result $? "ML-DSA-${level} key DER conversion"
1267+
12331268
echo "End of ML-DSA-${level} section"
12341269
done
12351270

@@ -1254,7 +1289,7 @@ EOF
12541289
echo "End of ecc-leaf-mldsa44 section"
12551290
echo "---------------------------------------------------------------------"
12561291
else
1257-
echo "Skipping ML-DSA cert generation (no OpenSSL 3.3+ with ML-DSA support found)"
1292+
echo "Skipping ML-DSA cert generation (no OpenSSL 3.5+ built-in ML-DSA provider found)"
12581293
echo "---------------------------------------------------------------------"
12591294
fi
12601295

configure.ac

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11802,8 +11802,9 @@ AS_IF([test "x$ENABLED_INTEL_QA_SYNC" = "xyes" || test "x$ENABLED_OCTEON_SYNC" =
1180211802
# checks for pkcs7 needed enables
1180311803
AS_IF([test "x$ENABLED_PKCS7" = "xyes" && \
1180411804
test "x$ENABLED_RSA" = "xno" && \
11805-
test "x$ENABLED_ECC" = "xno"],
11806-
[AC_MSG_ERROR([please enable ecc or rsa if enabling pkcs7.])])
11805+
test "x$ENABLED_ECC" = "xno" && \
11806+
test "x$ENABLED_MLDSA" = "xno"],
11807+
[AC_MSG_ERROR([please enable ecc, rsa or mldsa if enabling pkcs7.])])
1180711808
1180811809
AS_IF([test "x$ENABLED_PKCS7" = "xyes" && \
1180911810
test "x$ENABLED_SHA" = "xno" && \

wolfcrypt/src/hash.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,20 @@ enum wc_HashType wc_OidGetHash(int oid)
353353
hash_type = WC_ERR_TRACE(WC_HASH_TYPE_NONE);
354354
#endif
355355
break;
356+
case SHAKE128h:
357+
#if defined(WOLFSSL_SHA3) && defined(WOLFSSL_SHAKE128)
358+
hash_type = WC_HASH_TYPE_SHAKE128;
359+
#else
360+
hash_type = WC_ERR_TRACE(WC_HASH_TYPE_NONE);
361+
#endif
362+
break;
363+
case SHAKE256h:
364+
#if defined(WOLFSSL_SHA3) && defined(WOLFSSL_SHAKE256)
365+
hash_type = WC_HASH_TYPE_SHAKE256;
366+
#else
367+
hash_type = WC_ERR_TRACE(WC_HASH_TYPE_NONE);
368+
#endif
369+
break;
356370
case SM3h:
357371
#ifdef WOLFSSL_SM3
358372
hash_type = WC_HASH_TYPE_SM3;

0 commit comments

Comments
 (0)