Skip to main content

hoike_sign/
ml_dsa_bridge.rs

1//! Bridge between ml-dsa (signature v3) and x509-ocsp (signature v2).
2//!
3//! `produce_bundle` requires `Signer<Sig> + DynSignatureAlgorithmIdentifier`
4//! from signature v2 / spki v0.7. The ml-dsa crate implements signature v3.
5//! This module wraps the v3 types to satisfy v2 trait bounds.
6
7use ml_dsa as mldsa;
8
9/// Wrapper around ml-dsa `SigningKey` implementing signature v2 traits.
10pub struct MlDsaSigner<P: mldsa::MlDsaParams> {
11    inner: mldsa::SigningKey<P>,
12    sig_alg_oid: const_oid::ObjectIdentifier,
13}
14
15/// Wrapper around raw signature bytes implementing v2 `SignatureBitStringEncoding`.
16#[derive(Clone)]
17pub struct MlDsaSignatureBytes {
18    bytes: Vec<u8>,
19}
20
21impl From<MlDsaSignatureBytes> for Vec<u8> {
22    fn from(sig: MlDsaSignatureBytes) -> Vec<u8> {
23        sig.bytes
24    }
25}
26
27pub const ML_DSA_44_OID: &str = "2.16.840.1.101.3.4.3.17";
28pub const ML_DSA_65_OID: &str = "2.16.840.1.101.3.4.3.18";
29pub const ML_DSA_87_OID: &str = "2.16.840.1.101.3.4.3.19";
30
31impl<P: mldsa::MlDsaParams> MlDsaSigner<P> {
32    pub fn new(inner: mldsa::SigningKey<P>, oid_str: &str) -> Self {
33        let sig_alg_oid = const_oid::ObjectIdentifier::new_unwrap(oid_str);
34        MlDsaSigner { inner, sig_alg_oid }
35    }
36}
37
38// ── signature v2 Signer ──────────────────────────────────────────
39
40impl<P> signature::Signer<MlDsaSignatureBytes> for MlDsaSigner<P>
41where
42    P: mldsa::MlDsaParams,
43    mldsa::SigningKey<P>: mldsa::Signer<mldsa::Signature<P>>,
44{
45    fn try_sign(&self, msg: &[u8]) -> Result<MlDsaSignatureBytes, signature::Error> {
46        use mldsa::Signer as Signer3;
47        let sig: mldsa::Signature<P> = self
48            .inner
49            .try_sign(msg)
50            .map_err(|_| signature::Error::new())?;
51        use mldsa::SignatureEncoding;
52        let repr = sig.to_bytes();
53        let slice: &[u8] = repr.as_ref();
54        Ok(MlDsaSignatureBytes {
55            bytes: slice.to_vec(),
56        })
57    }
58}
59
60// ── signature v2 SignatureEncoding ────────────────────────────────
61
62impl signature::SignatureEncoding for MlDsaSignatureBytes {
63    type Repr = Vec<u8>;
64}
65
66impl TryFrom<&[u8]> for MlDsaSignatureBytes {
67    type Error = signature::Error;
68    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
69        Ok(MlDsaSignatureBytes {
70            bytes: bytes.to_vec(),
71        })
72    }
73}
74
75impl AsRef<[u8]> for MlDsaSignatureBytes {
76    fn as_ref(&self) -> &[u8] {
77        &self.bytes
78    }
79}
80
81// ── spki v0.7 SignatureBitStringEncoding ──────────────────────────
82
83impl spki::SignatureBitStringEncoding for MlDsaSignatureBytes {
84    fn to_bitstring(&self) -> der::Result<der::asn1::BitString> {
85        der::asn1::BitString::from_bytes(&self.bytes)
86    }
87}
88
89// ── spki v0.7 DynSignatureAlgorithmIdentifier ────────────────────
90
91impl<P> spki::DynSignatureAlgorithmIdentifier for MlDsaSigner<P>
92where
93    P: mldsa::MlDsaParams,
94    mldsa::SigningKey<P>: mldsa::Signer<mldsa::Signature<P>>,
95{
96    fn signature_algorithm_identifier(&self) -> spki::Result<spki::AlgorithmIdentifierOwned> {
97        Ok(spki::AlgorithmIdentifierOwned {
98            oid: self.sig_alg_oid,
99            parameters: None,
100        })
101    }
102}
103
104// ── Constructors ─────────────────────────────────────────────────
105
106pub fn ml_dsa_44_signer(seed: &[u8; 32]) -> MlDsaSigner<mldsa::MlDsa44> {
107    let sk = mldsa::SigningKey::<mldsa::MlDsa44>::from_seed(seed.into());
108    MlDsaSigner::new(sk, ML_DSA_44_OID)
109}
110
111pub fn ml_dsa_65_signer(seed: &[u8; 32]) -> MlDsaSigner<mldsa::MlDsa65> {
112    let sk = mldsa::SigningKey::<mldsa::MlDsa65>::from_seed(seed.into());
113    MlDsaSigner::new(sk, ML_DSA_65_OID)
114}
115
116pub fn ml_dsa_87_signer(seed: &[u8; 32]) -> MlDsaSigner<mldsa::MlDsa87> {
117    let sk = mldsa::SigningKey::<mldsa::MlDsa87>::from_seed(seed.into());
118    MlDsaSigner::new(sk, ML_DSA_87_OID)
119}
120
121/// Signer variant that dispatches to the correct ML-DSA parameter set.
122/// Returned by [`load_ml_dsa_signer_from_pkcs8_der`] when the parameter set
123/// is auto-detected from the PKCS#8 AlgorithmIdentifier OID.
124pub enum MlDsaSignerVariant {
125    MlDsa44(MlDsaSigner<mldsa::MlDsa44>),
126    MlDsa65(MlDsaSigner<mldsa::MlDsa65>),
127    MlDsa87(MlDsaSigner<mldsa::MlDsa87>),
128}
129
130impl MlDsaSignerVariant {
131    pub fn algorithm_name(&self) -> &'static str {
132        match self {
133            Self::MlDsa44(_) => "ml-dsa-44",
134            Self::MlDsa65(_) => "ml-dsa-65",
135            Self::MlDsa87(_) => "ml-dsa-87",
136        }
137    }
138
139    /// Construct a demo signer with a random seed for the given algorithm name.
140    pub fn demo(sig_alg: &str) -> std::result::Result<Self, String> {
141        use rand_core::RngCore;
142        let mut seed = [0u8; 32];
143        rand_core::OsRng.fill_bytes(&mut seed);
144        match sig_alg {
145            "ml-dsa-44" => Ok(Self::MlDsa44(ml_dsa_44_signer(&seed))),
146            "ml-dsa-65" => Ok(Self::MlDsa65(ml_dsa_65_signer(&seed))),
147            "ml-dsa-87" => Ok(Self::MlDsa87(ml_dsa_87_signer(&seed))),
148            other => Err(format!("unknown ML-DSA variant: {other}")),
149        }
150    }
151
152    /// Produce an ahu bundle, dispatching to the correct monomorphized
153    /// `produce_bundle` instantiation based on the loaded parameter set.
154    pub fn sign_bundle(
155        &mut self,
156        ca: &crate::source::CaIdentity,
157        snapshot: &crate::source::StatusSnapshot,
158        config: &crate::generate::GenerationConfig,
159        seal_fn: impl FnOnce(&[u8]) -> crate::error::Result<Vec<u8>>,
160        responder_cert_der: Option<&[u8]>,
161    ) -> crate::error::Result<Vec<u8>> {
162        match self {
163            Self::MlDsa44(s) => crate::generate::produce_bundle::<_, MlDsaSignatureBytes>(
164                ca,
165                snapshot,
166                config,
167                s,
168                seal_fn,
169                responder_cert_der,
170            ),
171            Self::MlDsa65(s) => crate::generate::produce_bundle::<_, MlDsaSignatureBytes>(
172                ca,
173                snapshot,
174                config,
175                s,
176                seal_fn,
177                responder_cert_der,
178            ),
179            Self::MlDsa87(s) => crate::generate::produce_bundle::<_, MlDsaSignatureBytes>(
180                ca,
181                snapshot,
182                config,
183                s,
184                seal_fn,
185                responder_cert_der,
186            ),
187        }
188    }
189}
190
191/// Load an ML-DSA signer from PKCS#8 DER bytes, auto-detecting the parameter set
192/// from the AlgorithmIdentifier OID (RFC 9881).
193pub fn load_ml_dsa_signer_from_pkcs8_der(der_bytes: &[u8]) -> Result<MlDsaSignerVariant, String> {
194    use ml_dsa::pkcs8::DecodePrivateKey;
195
196    if let Ok(sk) = mldsa::SigningKey::<mldsa::MlDsa44>::from_pkcs8_der(der_bytes) {
197        return Ok(MlDsaSignerVariant::MlDsa44(MlDsaSigner::new(
198            sk,
199            ML_DSA_44_OID,
200        )));
201    }
202    if let Ok(sk) = mldsa::SigningKey::<mldsa::MlDsa65>::from_pkcs8_der(der_bytes) {
203        return Ok(MlDsaSignerVariant::MlDsa65(MlDsaSigner::new(
204            sk,
205            ML_DSA_65_OID,
206        )));
207    }
208    if let Ok(sk) = mldsa::SigningKey::<mldsa::MlDsa87>::from_pkcs8_der(der_bytes) {
209        return Ok(MlDsaSignerVariant::MlDsa87(MlDsaSigner::new(
210            sk,
211            ML_DSA_87_OID,
212        )));
213    }
214
215    Err("key does not contain a valid ML-DSA-44, ML-DSA-65, or ML-DSA-87 PKCS#8 private key".into())
216}
217
218pub fn ml_dsa_signature_size(variant: &str) -> usize {
219    match variant {
220        "ml-dsa-44" => 2420,
221        "ml-dsa-65" => 3309,
222        "ml-dsa-87" => 4627,
223        _ => 0,
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn ml_dsa_44_sign_verify_size() {
233        let signer = ml_dsa_44_signer(&[1u8; 32]);
234        use signature::Signer;
235        let sig = signer.sign(b"test message");
236        assert_eq!(sig.bytes.len(), 2420);
237
238        use spki::DynSignatureAlgorithmIdentifier;
239        let alg_id = signer.signature_algorithm_identifier().unwrap();
240        assert_eq!(alg_id.oid.to_string(), ML_DSA_44_OID);
241
242        use spki::SignatureBitStringEncoding;
243        let bits = sig.to_bitstring().unwrap();
244        assert_eq!(bits.raw_bytes().len(), 2420);
245    }
246
247    #[test]
248    fn ml_dsa_65_sign_verify_size() {
249        let signer = ml_dsa_65_signer(&[2u8; 32]);
250        use signature::Signer;
251        let sig = signer.sign(b"test message");
252        assert_eq!(sig.bytes.len(), 3309);
253    }
254
255    #[test]
256    fn ml_dsa_87_sign_verify_size() {
257        let signer = ml_dsa_87_signer(&[3u8; 32]);
258        use signature::Signer;
259        let sig = signer.sign(b"test message");
260        assert_eq!(sig.bytes.len(), 4627);
261    }
262
263    #[test]
264    fn ml_dsa_44_pkcs8_round_trip() {
265        use ml_dsa::pkcs8::EncodePrivateKey;
266        let sk = mldsa::SigningKey::<mldsa::MlDsa44>::from_seed((&[1u8; 32]).into());
267        let der_doc = sk.to_pkcs8_der().expect("encode PKCS#8");
268        let variant = load_ml_dsa_signer_from_pkcs8_der(der_doc.as_bytes()).unwrap();
269        assert_eq!(variant.algorithm_name(), "ml-dsa-44");
270    }
271
272    #[test]
273    fn ml_dsa_65_pkcs8_round_trip() {
274        use ml_dsa::pkcs8::EncodePrivateKey;
275        let sk = mldsa::SigningKey::<mldsa::MlDsa65>::from_seed((&[2u8; 32]).into());
276        let der_doc = sk.to_pkcs8_der().expect("encode PKCS#8");
277        let variant = load_ml_dsa_signer_from_pkcs8_der(der_doc.as_bytes()).unwrap();
278        assert_eq!(variant.algorithm_name(), "ml-dsa-65");
279    }
280
281    #[test]
282    fn ml_dsa_87_pkcs8_round_trip() {
283        use ml_dsa::pkcs8::EncodePrivateKey;
284        let sk = mldsa::SigningKey::<mldsa::MlDsa87>::from_seed((&[3u8; 32]).into());
285        let der_doc = sk.to_pkcs8_der().expect("encode PKCS#8");
286        let variant = load_ml_dsa_signer_from_pkcs8_der(der_doc.as_bytes()).unwrap();
287        assert_eq!(variant.algorithm_name(), "ml-dsa-87");
288    }
289
290    #[test]
291    fn ml_dsa_pkcs8_invalid_data_errors() {
292        let result = load_ml_dsa_signer_from_pkcs8_der(b"not valid pkcs8");
293        assert!(result.is_err());
294    }
295
296    #[test]
297    fn ml_dsa_pkcs8_loaded_signer_produces_correct_signature() {
298        use ml_dsa::pkcs8::EncodePrivateKey;
299        let sk = mldsa::SigningKey::<mldsa::MlDsa87>::from_seed((&[42u8; 32]).into());
300        let der_doc = sk.to_pkcs8_der().expect("encode PKCS#8");
301        let variant = load_ml_dsa_signer_from_pkcs8_der(der_doc.as_bytes()).unwrap();
302        match variant {
303            MlDsaSignerVariant::MlDsa87(ref signer) => {
304                use signature::Signer;
305                let sig = signer.sign(b"test message");
306                assert_eq!(sig.bytes.len(), 4627);
307            }
308            _ => panic!("expected ML-DSA-87 variant"),
309        }
310    }
311
312    #[test]
313    fn ml_dsa_pkcs8_deterministic_across_load() {
314        use ml_dsa::pkcs8::EncodePrivateKey;
315        let sk = mldsa::SigningKey::<mldsa::MlDsa87>::from_seed((&[7u8; 32]).into());
316        let der_doc = sk.to_pkcs8_der().expect("encode PKCS#8");
317
318        let seed_signer = ml_dsa_87_signer(&[7u8; 32]);
319        let variant = load_ml_dsa_signer_from_pkcs8_der(der_doc.as_bytes()).unwrap();
320
321        use signature::Signer;
322        let msg = b"determinism check";
323        let sig_seed = seed_signer.sign(msg);
324        match variant {
325            MlDsaSignerVariant::MlDsa87(ref loaded) => {
326                let sig_loaded = loaded.sign(msg);
327                assert_eq!(sig_seed.bytes, sig_loaded.bytes);
328            }
329            _ => panic!("expected ML-DSA-87"),
330        }
331    }
332}