Expand description
Hybrid post-quantum digital signature
combining ed25519 and ML-DSA-65, for signing JWTs (crate::misc::jwt) and
Signed messages.
A signature consists of both an ML-DSA-65 and an ed25519 signature, and verification requires both to be valid.
§Relation to the standard, and the one deviation
This tracks draft-ietf-jose-pq-composite-sigs-01 (built on
draft-ietf-lamps-pq-composite-sigs-19), which standardises this ML-DSA-65-Ed25519
composite. We follow it rather than an ad-hoc combiner so that, should Yivi adopt it for
signing its JWTs in the future, conforming (see below) would let us verify Yivi’s signatures
directly.
We replicate that combiner except for one detail: the standard signs the ML-DSA component
with a non-empty context (mldsa_ctx = LABEL), but aws_lc_rs’s ML-DSA API (still unstable
as of 1.17) exposes no context parameter, so we use the empty context (see ML_DSA_CTX).
This makes our signatures non-conformant with the standard, and means the official test
vectors (which use mldsa_ctx=LABEL) cannot be verified here yet.
Everything else — M', the SHA-512 prehash, the concatenation order, the key/signature byte
formats, and the AKP JWK — is standard-correct. When aws_lc_rs exposes an ML-DSA context
parameter (its C core already supports it), conforming is small and local:
- set
ML_DSA_CTXtoLABELand thread it intoml_dsa_sign/ml_dsa_verify; - rename
ALGto"ML-DSA-65-Ed25519"; - add the official
MLDSA65-Ed25519test-vector check.
Structs§
- Signing
Key - Hybrid signing key, the ‘private key’. Generate using
SigningKey::generate. - Signing
KeyBytes - Encoded form of
SigningKey, for storage. - Verifying
Key - Hybrid verifying key, the ‘public key’. Obtain via
SigningKey::verifying_key. - Verifying
KeyBytes - Encoded form of
VerifyingKey, for the wire.
Constants§
- ALG
- Value for the
algJWT header. Interim, non-conformant name; the standard reserves"ML-DSA-65-Ed25519"for the conformant variant (see module docs). - CTX 🔒
- The composite application context
ctx, encoded inM'aslen(ctx) ‖ ctx— empty for our JWTs. Distinct fromML_DSA_CTX, the context of the underlying ML-DSA primitive. - ED25519_
SIG_ 🔒LEN - ed25519 signature length, in bytes.
- LABEL 🔒
Labelfor theML-DSA-65-Ed25519combination, from draft-ietf-lamps-pq-composite-sigs-19 §6 “Algorithm Identifiers and Parameters”. It is the per-algorithm domain separator in the message representativeM' = PREFIX ‖ LABEL ‖ … ‖ SHA-512(message)built bymessage_representative— and, in the conformant variant, the ML-DSA context (seeML_DSA_CTX).- ML_
DSA_ 🔒CTX - Context passed to the underlying ML-DSA primitive.
- M_
PRIME_ 🔒LEN - Length of the message representative
M'(seemessage_representative):PREFIX ‖ LABEL ‖ len(ctx) ‖ ctx ‖ SHA-512(..), a fixed size (the+ 1is thelen(ctx)byte). - PREFIX 🔒
Prefixfrom the composite-signatures combiner — the fixed domain-separation string defined in draft-ietf-lamps-pq-composite-sigs-19 §2.2 “Prefix, Label, and CTX”.
Functions§
- jwk_
thumbprint 🔒 - RFC 7638 JWK thumbprint of the AKP public key:
base64url(SHA-256(canonical JSON of the required members alg, kty, pub, in lexicographic order)).pub_bytesis the composite public keyML-DSA-65 pk ‖ ed25519 pk(matching the JWKpubmember); all member values are ASCII, so the canonical JSON needs no escaping. This value is the JWKkid. AKP key type and required members per RFC 9964 §6. - message_
representative 🔒 - The composite message representative
M' = PREFIX ‖ LABEL ‖ len(ctx) ‖ ctx ‖ SHA-512(message).M'is not a hash we take but the message both components sign (each hashes it internally), so it must be passed whole. The application context is empty, solen(ctx) ‖ ctxis a single0x00, makingM'a fixedM_PRIME_LENbytes. See draft-ietf-lamps-pq-composite-sigs-19 §2.2. - ml_
dsa_ 🔒sign - Signs
m_primewith ML-DSA-65 intoout, which must hold at leastML_DSA_65_SIGNING.signature_len()bytes. - ml_
dsa_ 🔒verify - Verifies an ML-DSA-65
signatureonm_prime. Seeml_dsa_signfor the context caveat.