Add external mu (message representative) support for ML-DSA#14979
Add external mu (message representative) support for ML-DSA#14979reaperhulk wants to merge 8 commits into
Conversation
|
|
||
| Sign a precomputed ``mu`` (message representative) using ML-DSA-44, | ||
| the "external mu" variant from FIPS 204. ``mu`` already incorporates | ||
| the public key and any context string, so no context is accepted here. |
There was a problem hiding this comment.
I feel like this second sentence is low value, and instead there should be something about how you compuete mu (even if just to say we don't have an API for it yet)
|
|
||
| Verify a signature over a precomputed ``mu`` (message representative), | ||
| the "external mu" variant from FIPS 204. ``mu`` already incorporates | ||
| the public key and any context string. |
| ) -> OpenSSLResult<Vec<u8>> { | ||
| cfg_if::cfg_if! { | ||
| if #[cfg(CRYPTOGRAPHY_IS_BORINGSSL)] { | ||
| // BoringSSL has no EVP-level external mu support, so we drop down to |
There was a problem hiding this comment.
if humanly possible, I'd like to wait on that, since it makes this 100x shorter
| // passing the 64-byte mu in place of the message. | ||
| let _ = variant; | ||
| let mut md_ctx = openssl::md_ctx::MdCtx::new()?; | ||
| let pkey_ctx = md_ctx.digest_sign_init(None, pkey)?; |
There was a problem hiding this comment.
Does this work if you do a pkeyctx with sign_init on openssl? (with or without set_mu)? that'd nicely let us consolidate with the aws-lc path
There was a problem hiding this comment.
I tried this against OpenSSL 4.0 and it doesn't work: EVP_PKEY_sign_init (and verify_init) fail for ML-DSA with provider signature not supported (crypto/evp/signature.c). The ML-DSA provider only registers the digest-sign / sign-message flow (OSSL_FUNC_SIGNATURE_DIGEST_SIGN*, plus a one-shot SIGN with no SIGN_INIT), so we can't drive it through a PkeyCtx like the AWS-LC path — it has to go through digest_sign_init + the mu param. I left a comment to that effect.
Generated by Claude Code
| /// context is accepted here. | ||
| pub fn sign_mu( | ||
| pkey: &openssl::pkey::PKeyRef<openssl::pkey::Private>, | ||
| variant: MlDsaVariant, |
There was a problem hiding this comment.
this should not take variant as an argument, it should be inferred from pkey, as sign does. otherwise this is unsound
| /// `mu` must be [`MLDSA_MU_BYTES`] long. | ||
| pub fn verify_mu( | ||
| pkey: &openssl::pkey::PKeyRef<openssl::pkey::Public>, | ||
| variant: MlDsaVariant, |
|
|
||
| #[pyo3::pymethods] | ||
| impl MlDsa87PublicKey { | ||
| #[pyo3(signature = (signature, mu))] |
There was a problem hiding this comment.
why does this one have a signature = block?
| # The sign vectors carry a precomputed mu ("External Mu") for every case | ||
| # that has a valid signature, including the "Internal" cases that NIST | ||
| # provides as bare mu values with no message or context. Those are | ||
| # skipped by the signing tests above (we don't expose Sign_internal) but | ||
| # exercise the precomputed-mu verification interface here. | ||
| if "mu" not in wycheproof.testcase or not wycheproof.valid: | ||
| return |
There was a problem hiding this comment.
This comment makes no sense to me, if mu exists in all of them, what's the first clause doing?
| the "external mu" variant from FIPS 204. ``mu`` already incorporates | ||
| the public key and any context string, so no context is accepted here. | ||
| the "external mu" variant from FIPS 204. ``mu`` is computed as | ||
| ``SHAKE256(SHAKE256(public_key, 64) || M', 64)``, where ``M'`` encodes |
|
Claude you've got merge conflicts, can you rebase? |
Adds sign_mu()/verify_mu() to the ML-DSA private/public key classes for
signing and verifying a precomputed 64-byte external mu, as defined in
FIPS 204. mu already incorporates the public key and any context string,
so these methods take no context.
Per-backend mechanism:
- OpenSSL 3.5+: set the integer "mu" signature parameter
(OSSL_SIGNATURE_PARAM_MU) and pass mu through EVP_DigestSign.
- AWS-LC: EVP_PKEY_sign/EVP_PKEY_verify, which use the "ExternalMu"
format for ML-DSA keys.
- BoringSSL: the EVP layer has no external-mu support, so use the
low-level MLDSA*_{sign,verify}_message_representative functions.
Derive mu from the existing deterministic pure KAT vectors (whose signatures come from an independent reference implementation) and check that verify_mu accepts each reference signature for its derived mu and rejects it for a tampered mu, across ML-DSA-44/65/87. https://claude.ai/code/session_01MmjphxZ6ookRpjUhQouKjf
The Wycheproof ML-DSA sign vectors carry a precomputed mu ('External Mu')
for every case with a valid signature, including the mu-only 'Internal'
cases NIST provides without an accompanying message or context. The
signing tests skip those, so add external-mu tests that exercise
verify_mu against the vector mu/signature pairs, confirm a perturbed mu
fails, and (when msg/ctx are present) check the derived mu matches the
vector.
https://claude.ai/code/session_01MmjphxZ6ookRpjUhQouKjf
Point openssl and openssl-sys at the rust-openssl master branch via [patch.crates-io] so the BoringSSL build picks up the merged mldsa.h addition to the bindgen wrapper (rust-openssl/rust-openssl#2650), which the external-mu BoringSSL path needs for the MLDSA* low-level functions and the CBS public-key parser. Temporary until a release including it ships. https://claude.ai/code/session_01MmjphxZ6ookRpjUhQouKjf
Replace the allocating OSSL_PARAM_BLD in set_mu with a fixed two-element OSSL_PARAM array constructed via OSSL_PARAM_construct_uint. The provider reads the 'mu' flag back with OSSL_PARAM_get_int, which accepts an unsigned integer param. This removes the builder's allocation-failure branch, which was unreachable in practice and left uncovered. https://claude.ai/code/session_01MmjphxZ6ookRpjUhQouKjf
- docs: describe how mu is computed and note there is no API to compute it yet, replacing the lower-value note about context. - backend: drop the redundant pyo3 signature attribute on verify_mu (no optional arguments, matching sign_mu). - openssl: note that the OpenSSL external-mu path must use digest-sign because ML-DSA does not implement EVP_PKEY_sign_init. - tests: clarify why the wycheproof external-mu helper filters on both the presence of mu and a valid result. https://claude.ai/code/session_01MmjphxZ6ookRpjUhQouKjf
sign_mu and verify_mu took the ML-DSA variant as a caller-supplied argument, which is unsound: nothing tied it to the key. Infer it from the pkey via MlDsaVariant::from_pkey instead, matching how sign/verify work, and drop the argument from both functions and their callers. Only the BoringSSL low-level path actually needs the variant. Also trim the sign_mu/verify_mu docs back to a concise note that there is no API to compute mu. https://claude.ai/code/session_01MmjphxZ6ookRpjUhQouKjf
971d6b8 to
401f6cd
Compare
The BoringSSL verify_mu path re-parses the raw public key, which was already validated when the key was constructed, so the parse cannot fail in practice. Returning Ok(false) on that impossible failure left three unreachable lines uncovered. Propagate the error with ? instead (as sign_mu already does), which keeps coverage at 100%. https://claude.ai/code/session_01MmjphxZ6ookRpjUhQouKjf
This PR adds support for the "external mu" variant of ML-DSA signing and verification as defined in FIPS 204. This allows applications to precompute the message representative (mu) and sign/verify it directly, which is useful for protocols that need to work with the intermediate mu value.
Key Changes
MLDSA_MU_BYTES(64 bytes) defines the length of an ML-DSA external mu valuesign_mu(mu)on all three private key classes (MlDsa44, MlDsa65, MlDsa87) to sign a precomputed 64-byte muverify_mu(signature, mu)on all three public key classes to verify a signature over a precomputed muCRYPTOGRAPHY_OPENSSL_350_OR_GREATERto the conditional compilation flags for the new functionsValueErrorif notImplementation Details
The external mu interface is implemented with conditional compilation to support three different backends:
MLDSA*_sign_message_representativefunctionsset_mu()configures the signature context with the "mu" parameter before signing/verifyingThe external mu signatures are fully compatible with ordinary ML-DSA signatures—a signature produced via
sign_mu()can be verified with the standardverify()method if the message and context are provided, and vice versa.https://claude.ai/code/session_01MmjphxZ6ookRpjUhQouKjf