huddle_protocol/crypto/
mldsa.rs1use hkdf::Hkdf;
17use ml_dsa::{
18 EncodedSignature, EncodedVerifyingKey, Keypair, MlDsa65, Signature, Signer, SigningKey,
19 VerifyingKey, B32,
20};
21use sha2::Sha256;
22use zeroize::Zeroizing;
23
24pub const MLDSA_PK_LEN: usize = 1952;
26pub const MLDSA_SIG_LEN: usize = 3309;
28
29const MLDSA_SEED_LABEL: &[u8] = b"huddle-mldsa-65-seed-v1";
33
34pub struct MlDsaKeypair {
36 sk: SigningKey<MlDsa65>,
37}
38
39impl MlDsaKeypair {
40 pub fn from_identity_seed(ed25519_seed: &[u8; 32]) -> Self {
44 let mut seed = Zeroizing::new([0u8; 32]);
45 let hk = Hkdf::<Sha256>::new(Some(MLDSA_SEED_LABEL), ed25519_seed);
46 hk.expand(b"", seed.as_mut_slice())
47 .expect("HKDF expand to 32 bytes is within SHA-256's output limit");
48 let seed_arr: B32 = (*seed).into();
49 let sk = SigningKey::<MlDsa65>::from_seed(&seed_arr);
50 Self { sk }
51 }
52
53 pub fn public_bytes(&self) -> [u8; MLDSA_PK_LEN] {
55 let enc: EncodedVerifyingKey<MlDsa65> = self.sk.verifying_key().encode();
56 let mut out = [0u8; MLDSA_PK_LEN];
57 out.copy_from_slice(enc.as_slice());
58 out
59 }
60
61 pub fn sign(&self, msg: &[u8]) -> [u8; MLDSA_SIG_LEN] {
64 let sig: Signature<MlDsa65> = self.sk.sign(msg);
65 let enc: EncodedSignature<MlDsa65> = sig.encode();
66 let mut out = [0u8; MLDSA_SIG_LEN];
67 out.copy_from_slice(enc.as_slice());
68 out
69 }
70}
71
72pub fn verify(pubkey_bytes: &[u8], msg: &[u8], sig_bytes: &[u8]) -> bool {
75 let pk_enc = match EncodedVerifyingKey::<MlDsa65>::try_from(pubkey_bytes) {
76 Ok(a) => a,
77 Err(_) => return false,
78 };
79 let vk = VerifyingKey::<MlDsa65>::decode(&pk_enc);
80 let sig_enc = match EncodedSignature::<MlDsa65>::try_from(sig_bytes) {
81 Ok(a) => a,
82 Err(_) => return false,
83 };
84 let sig = match Signature::<MlDsa65>::decode(&sig_enc) {
85 Some(s) => s,
86 None => return false,
87 };
88 vk.verify_with_context(msg, &[], &sig)
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn deterministic_keypair_and_sizes() {
97 let a = MlDsaKeypair::from_identity_seed(&[7u8; 32]);
98 let b = MlDsaKeypair::from_identity_seed(&[7u8; 32]);
99 assert_eq!(a.public_bytes(), b.public_bytes());
100 assert_eq!(a.public_bytes().len(), MLDSA_PK_LEN);
101 let c = MlDsaKeypair::from_identity_seed(&[8u8; 32]);
102 assert_ne!(a.public_bytes(), c.public_bytes());
103 }
104
105 #[test]
106 fn sign_verify_round_trip() {
107 let kp = MlDsaKeypair::from_identity_seed(&[1u8; 32]);
108 let pk = kp.public_bytes();
109 let sig = kp.sign(b"authority message");
110 assert_eq!(sig.len(), MLDSA_SIG_LEN);
111 assert!(verify(&pk, b"authority message", &sig));
112 assert!(!verify(&pk, b"different message", &sig));
114 let mut bad = sig;
115 bad[0] ^= 1;
116 assert!(!verify(&pk, b"authority message", &bad));
117 let other = MlDsaKeypair::from_identity_seed(&[2u8; 32]).public_bytes();
118 assert!(!verify(&other, b"authority message", &sig));
119 }
120
121 #[test]
122 fn malformed_inputs_are_rejected() {
123 let kp = MlDsaKeypair::from_identity_seed(&[3u8; 32]);
124 let sig = kp.sign(b"m");
125 assert!(!verify(b"too short", b"m", &sig));
126 assert!(!verify(&kp.public_bytes(), b"m", b"short sig"));
127 }
128}