Skip to content

PKCS#7: add ML-DSA support#10759

Merged
dgarske merged 1 commit into
wolfSSL:masterfrom
Frauschi:pkcs7_mldsa
Jul 9, 2026
Merged

PKCS#7: add ML-DSA support#10759
dgarske merged 1 commit into
wolfSSL:masterfrom
Frauschi:pkcs7_mldsa

Conversation

@Frauschi

Copy link
Copy Markdown
Contributor

Summary

Add ML-DSA as a signature algorithm for PKCS#7/CMS SignedData, for both signing and verification, following RFC 9882 ("Use of ML-DSA in the Cryptographic Message Syntax").

Unlike RSA/ECDSA, 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 directly when no signed attributes are present - rather than over a pre-computed DigestInfo, with an empty context string and absent signatureAlgorithm parameters.

Changes

wolfcrypt/src/pkcs7.c - core implementation

  • New helpers wc_PKCS7_MlDsaLevelFromOID, wc_PKCS7_BuildPureSigMessage, wc_PKCS7_MlDsaSign, wc_PKCS7_MlDsaVerify, wired into the existing per-algorithm switch sites (GetSignSize, SignedDataGetEncAlgoId, SetPublicKeyOID, CheckPublicKeyDer) and the sign/verify dispatchers.
  • wc_MlDsaKey is always heap-allocated (due to its size), matching the ML-DSA handling in asn.c; the accompanying DecodedCert uses the WC_DECLARE_VAR/WC_ALLOC_VAR_EX macros for WOLFSSL_SMALL_STACK handling.
  • Signer public key is cached in wc_PKCS7 only for RSA/ECC (consumed by the raw-sign callback paths), with the buffer-size bound retained; large PQC keys are not stored.

wolfcrypt/src/hash.c - map the SHAKE128/SHAKE256 OIDs to their hash types in wc_OidGetHash() (RFC 8702 digest algorithms).

certs/mldsa/ - add expanded-only PKCS#8 DER private keys (mldsa44/65/87-key.der) matching the existing self-signed ML-DSA certificates.

wolfcrypt/test/test.c - pkcs7signed_mldsa_test(): round-trip encode→verify across every enabled ML-DSA level with SHA-512 and (where available) SHAKE digests, asserting:

  • recovered content matches,
  • SHAKE digest AlgorithmIdentifiers are encoded with absent parameters per RFC 8702 (both the digestAlgorithms SET and SignerInfo.digestAlgorithm occurrences),
  • a tampered signature is rejected with SIG_VERIFY_E (negative path).

Configuration

Gated behind existing ML-DSA macros - no new public config option. Active when WOLFSSL_HAVE_MLDSA is enabled with ASN.1 support; signing and verification compile independently based on the ML-DSA backend's private-/public-key availability (WOLFSSL_MLDSA_NO_SIGN / WOLFSSL_MLDSA_NO_VERIFY). No change to default builds.

Compatibility

  • No public API changes; no changes to existing struct members.
  • RSA/ECDSA/RSA-PSS SignedData paths are unchanged.

Testing

  • ./configure --enable-all --enable-debug && make && ./wolfcrypt/test/testwolfcrypt - PKCS7signed test passed!, full suite green.
  • Verified clean compilation under degenerate ML-DSA feature combinations (NO_SIGN, NO_VERIFY, both, and NO_MAKE_KEY+NO_SIGN+NO_VERIFY) with the project's -Werror flag set.

@Frauschi Frauschi self-assigned this Jun 23, 2026
@Frauschi Frauschi marked this pull request as ready for review June 23, 2026 11:52
@github-actions

Copy link
Copy Markdown

retest this please

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fenrir Automated Review — PR #10759

Scan targets checked: wolfcrypt-bugs, wolfcrypt-port-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src

Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Findings are non-blocking.

Comment thread wolfcrypt/src/pkcs7.c
Comment thread wolfcrypt/src/pkcs7.c Outdated
@Frauschi Frauschi force-pushed the pkcs7_mldsa branch 2 times, most recently from 443710e to aaaf1b3 Compare July 2, 2026 14:01

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Skoll Multi-Scan Review

Modes: review + review-securityOverall recommendation: REQUEST_CHANGES
Findings: 5 total — 5 posted, 0 skipped
5 finding(s) posted as inline comments (see file-level comments below)

Posted findings

  • [High] [review] ML-DSA verify references wc_PKCS7_CertMatchesSignerInfo which is not compiled in RSA-less/ECC-less buildswolfcrypt/src/pkcs7.c:5396
  • [Low] [review+review-security] RSA raw-sign callback output buffer tightened to modulus size with no post-write bounds checkwolfcrypt/src/pkcs7.c:2973-2978
  • [Low] [review] InitWithCert no longer size-checks non-RSA/ECC public keyswolfcrypt/src/pkcs7.c:1315-1340
  • [Info] [review-security] wc_OidGetHash SHAKE128/SHAKE256 mapping affects OCSP/CRL/PKCS12/RSA-PSS beyond the stated PKCS#7 scopewolfcrypt/src/hash.c:356-369
  • [Info] [review] ML-DSA verify masks hard internal errors from key decode as SIG_VERIFY_Ewolfcrypt/src/pkcs7.c:5416-5449

Review generated by Skoll

Comment thread wolfcrypt/src/pkcs7.c
Comment thread wolfcrypt/src/pkcs7.c
Comment thread wolfcrypt/src/pkcs7.c
Comment thread wolfcrypt/src/hash.c
Comment thread wolfcrypt/src/pkcs7.c

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@Frauschi Frauschi removed their assignment Jul 3, 2026
@Frauschi Frauschi requested a review from dgarske July 3, 2026 08:32
@Frauschi Frauschi force-pushed the pkcs7_mldsa branch 2 times, most recently from edab80f to ab350ec Compare July 8, 2026 09:06
@Frauschi

Frauschi commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Jenkins retest this please

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Skoll Multi-Scan Review

Modes: review + review-securityOverall recommendation: COMMENT
Findings: 3 total — 3 posted, 0 skipped
1 finding(s) posted as inline comments (see file-level comments below)
2 finding(s) not tied to a diff line (full detail below)

Posted findings

  • [Medium] [review] scratch set-but-unused in wc_PKCS7_CheckPublicKeyDer for ML-DSA-sign-only + no-RSA/no-ECC buildwolfcrypt/src/pkcs7.c:1094

Findings not tied to a diff line

Redundant content digest computed on ML-DSA verify path

File: wolfcrypt/src/pkcs7.c:5799
Function: wc_PKCS7_SignedDataVerifySignature
Severity: Low

For the ML-DSA case, wc_PKCS7_BuildSignedDataDigest (line 5799) still computes pkcs7Digest/plainDigest, but the ML-DSA branch ignores them and rebuilds the pure-mode message via wc_PKCS7_BuildPureSigMessage. This is harmless (and the digest is genuinely needed for the earlier wc_PKCS7_VerifyContentMessageDigest when signed attributes are present), so it is not a correctness issue - just a small amount of unused work for the no-signed-attributes ML-DSA case. Noting only for awareness; no change required.

Recommendation: Optional: no action needed. Left as-is keeps the shared verify flow simple and preserves the messageDigest binding check.

Referenced code: wolfcrypt/src/pkcs7.c:5799-5803 (5 lines)


Detached ML-DSA SignedData over empty content is rejected (functional gap, not a security defect)

File: wolfcrypt/src/pkcs7.c:4305-4310, 3447-3453
Function: wc_PKCS7_EncodeSignedData / PKCS7_EncodeSigned
Severity: Info
Category: Logic

wc_PKCS7_EncodeSignedData pre-computes a content hash and calls PKCS7_EncodeSigned with a non-NULL hashBuf for three cases, including (pkcs7->detached && pkcs7->contentSz == 0) (the SCEP empty-content case). The PR adds a guard in PKCS7_EncodeSigned (line 3447) that rejects any ML-DSA signer when hashBuf != NULL with BAD_FUNC_ARG. As a result, a detached ML-DSA SignedData over empty content (pkcs7->detached set, contentSz == 0) is unconditionally rejected, whereas RSA/ECDSA support it. This is a new-feature limitation (ML-DSA was previously unsupported in PKCS#7, so there is no regression) and is a clean, safe error return rather than a memory-safety issue. Detached/non-empty and embedded ML-DSA both work correctly. Worth documenting the limitation, but it carries no security impact.

Recommendation: No security action required. Optionally document that detached-over-empty-content SignedData is unsupported for ML-DSA, or special-case the detached && contentSz==0 path for ML-DSA so the pure-mode signature is computed over the (empty) eContent instead of taking the pre-hash branch.

Referenced code: wolfcrypt/src/pkcs7.c:4305-4310, 3447-3453 (13 lines)


Review generated by Skoll

Comment thread wolfcrypt/src/pkcs7.c
@Frauschi

Frauschi commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Jenkins retest this please

@Frauschi Frauschi requested a review from dgarske July 9, 2026 15:05

@dgarske dgarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Skoll Multi-Scan Review

Modes: review + review-securityOverall recommendation: COMMENT
Findings: 6 total — 3 posted, 3 skipped
2 finding(s) posted as inline comments (see file-level comments below)
1 finding(s) not tied to a diff line (full detail below)

Posted findings

  • [Low] [review] Unused DigestInfo buffer allocated for ML-DSA signingwolfcrypt/src/pkcs7.c:3015-3033
  • [Low] [review] Content digest recomputed then discarded on ML-DSA verifywolfcrypt/src/pkcs7.c:5945-6071

Findings not tied to a diff line

ML-DSA 'no certificates in bundle' verify path stores an unusable digest

File: wolfcrypt/src/pkcs7.c:5954-6015
Function: wc_PKCS7_SignedDataVerifySignature
Severity: Medium
Category: correctness

When a SignedData bundle contains no certificates, the verifier stores pkcs7->signature, pkcs7->plainDigest, and pkcs7->pkcs7Digest and returns PKCS7_SIGNEEDS_CHECK so the caller can verify externally with their own key. This design assumes the signature is over a digest. ML-DSA is used in 'pure' mode (RFC 9882) and signs the complete message (the re-tagged SET OF signed attributes, or the eContent), not a digest. For an ML-DSA signer with no cert in the bundle, the stored plainDigest/pkcs7Digest is insufficient to verify the signature -- the caller has no way to reconstruct the exact octet string that was signed. This runs before the switch (pkcs7->publicKeyOID) dispatch, so it applies to ML-DSA too. It fails closed (no false-accept), but silently hands back data that cannot complete an ML-DSA verification.

Recommendation: For ML_DSA_44k/65k/87k, either return a clear 'unsupported' error in the no-cert case, or store the reconstructed pure-mode message (from wc_PKCS7_BuildPureSigMessage) instead of a digest so an external verifier can use it. Document the limitation or special-case ML-DSA in the no-cert path so callers do not receive verification material they cannot use.

Referenced code: wolfcrypt/src/pkcs7.c:5954-5958 (5 lines)


Skipped findings

  • [Medium] No test coverage for ML-DSA no-certificate external-verify path
  • [Low] configure.ac PKCS7 check does not account for WOLFSSL_MLDSA_NO_ASN1
  • [Info] wc_OidGetHash() SHAKE OID mapping widens accepted digests beyond PKCS#7 (blast radius)

Review generated by Skoll

Comment thread wolfcrypt/src/pkcs7.c
Comment thread wolfcrypt/src/pkcs7.c
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.
@Frauschi

Frauschi commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

I fixed the latest "ML-DSA 'no certificates in bundle' verify path" issue.

@Frauschi Frauschi requested a review from dgarske July 9, 2026 17:11
@dgarske dgarske merged commit f44b16c into wolfSSL:master Jul 9, 2026
366 of 378 checks passed
@Frauschi Frauschi deleted the pkcs7_mldsa branch July 9, 2026 17:32
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.

4 participants