use hkdf::Hkdf;
use ml_dsa::{
EncodedSignature, EncodedVerifyingKey, Keypair, MlDsa65, Signature, Signer, SigningKey,
VerifyingKey, B32,
};
use sha2::Sha256;
use zeroize::Zeroizing;
pub const MLDSA_PK_LEN: usize = 1952;
pub const MLDSA_SIG_LEN: usize = 3309;
const MLDSA_SEED_LABEL: &[u8] = b"huddle-mldsa-65-seed-v1";
pub struct MlDsaKeypair {
sk: SigningKey<MlDsa65>,
}
impl MlDsaKeypair {
pub fn from_identity_seed(ed25519_seed: &[u8; 32]) -> Self {
let mut seed = Zeroizing::new([0u8; 32]);
let hk = Hkdf::<Sha256>::new(Some(MLDSA_SEED_LABEL), ed25519_seed);
hk.expand(b"", seed.as_mut_slice())
.expect("HKDF expand to 32 bytes is within SHA-256's output limit");
let seed_arr: B32 = (*seed).into();
let sk = SigningKey::<MlDsa65>::from_seed(&seed_arr);
Self { sk }
}
pub fn public_bytes(&self) -> [u8; MLDSA_PK_LEN] {
let enc: EncodedVerifyingKey<MlDsa65> = self.sk.verifying_key().encode();
let mut out = [0u8; MLDSA_PK_LEN];
out.copy_from_slice(enc.as_slice());
out
}
pub fn sign(&self, msg: &[u8]) -> [u8; MLDSA_SIG_LEN] {
let sig: Signature<MlDsa65> = self.sk.sign(msg);
let enc: EncodedSignature<MlDsa65> = sig.encode();
let mut out = [0u8; MLDSA_SIG_LEN];
out.copy_from_slice(enc.as_slice());
out
}
}
pub fn verify(pubkey_bytes: &[u8], msg: &[u8], sig_bytes: &[u8]) -> bool {
let pk_enc = match EncodedVerifyingKey::<MlDsa65>::try_from(pubkey_bytes) {
Ok(a) => a,
Err(_) => return false,
};
let vk = VerifyingKey::<MlDsa65>::decode(&pk_enc);
let sig_enc = match EncodedSignature::<MlDsa65>::try_from(sig_bytes) {
Ok(a) => a,
Err(_) => return false,
};
let sig = match Signature::<MlDsa65>::decode(&sig_enc) {
Some(s) => s,
None => return false,
};
vk.verify_with_context(msg, &[], &sig)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deterministic_keypair_and_sizes() {
let a = MlDsaKeypair::from_identity_seed(&[7u8; 32]);
let b = MlDsaKeypair::from_identity_seed(&[7u8; 32]);
assert_eq!(a.public_bytes(), b.public_bytes());
assert_eq!(a.public_bytes().len(), MLDSA_PK_LEN);
let c = MlDsaKeypair::from_identity_seed(&[8u8; 32]);
assert_ne!(a.public_bytes(), c.public_bytes());
}
#[test]
fn sign_verify_round_trip() {
let kp = MlDsaKeypair::from_identity_seed(&[1u8; 32]);
let pk = kp.public_bytes();
let sig = kp.sign(b"authority message");
assert_eq!(sig.len(), MLDSA_SIG_LEN);
assert!(verify(&pk, b"authority message", &sig));
assert!(!verify(&pk, b"different message", &sig));
let mut bad = sig;
bad[0] ^= 1;
assert!(!verify(&pk, b"authority message", &bad));
let other = MlDsaKeypair::from_identity_seed(&[2u8; 32]).public_bytes();
assert!(!verify(&other, b"authority message", &sig));
}
#[test]
fn malformed_inputs_are_rejected() {
let kp = MlDsaKeypair::from_identity_seed(&[3u8; 32]);
let sig = kp.sign(b"m");
assert!(!verify(b"too short", b"m", &sig));
assert!(!verify(&kp.public_bytes(), b"m", b"short sig"));
}
}