1use ed25519_dalek::{
127 Signature as EdSignature, Signer, SigningKey as EdSigningKey, Verifier,
128 VerifyingKey as EdVerifyingKey,
129};
130use ml_dsa::signature::rand_core::{TryCryptoRng, TryRng};
131use ml_dsa::signature::{Keypair, Verifier as MlVerifier};
132use ml_dsa::{
133 B32, ExpandedSigningKey, KeyInit, MlDsa44, MlDsa65, MlDsa87, MlDsaParams, Signature,
134 SigningKey, VerifyingKey,
135};
136use std::convert::Infallible;
137use zeroize::{Zeroize, ZeroizeOnDrop};
138
139use crate::CryptoError;
140use crate::b64;
141
142pub const SIGN_CONTEXT_V1: &str = "metamorphic/sign/v1";
149
150const VERSION_CAT2: u8 = 0x01;
152const VERSION_CAT3: u8 = 0x02;
154const VERSION_CAT5: u8 = 0x03;
156
157const ED25519_SEED_LEN: usize = 32;
159const ED25519_PK_LEN: usize = 32;
161const ED25519_SIG_LEN: usize = 64;
163const MLDSA_SEED_LEN: usize = 32;
165
166const SECRET_KEY_LEN: usize = 1 + ED25519_SEED_LEN + MLDSA_SEED_LEN;
168
169const MLDSA44_PK_LEN: usize = 1312;
172const MLDSA44_SIG_LEN: usize = 2420;
174const MLDSA65_PK_LEN: usize = 1952;
177const MLDSA65_SIG_LEN: usize = 3309;
179const MLDSA87_PK_LEN: usize = 2592;
182const MLDSA87_SIG_LEN: usize = 4627;
184
185#[derive(Clone, Zeroize, ZeroizeOnDrop)]
192pub struct HybridSignatureKeyPair {
193 #[zeroize(skip)]
195 pub public_key: String,
196 pub secret_key: String,
198}
199
200impl std::fmt::Debug for HybridSignatureKeyPair {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.debug_struct("HybridSignatureKeyPair")
203 .field("public_key", &self.public_key)
204 .field("secret_key", &"<redacted>")
205 .finish()
206 }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
211pub enum SignatureLevel {
212 Cat2,
214 #[default]
216 Cat3,
217 Cat5,
219}
220
221impl SignatureLevel {
222 fn version_tag(self) -> u8 {
224 match self {
225 SignatureLevel::Cat2 => VERSION_CAT2,
226 SignatureLevel::Cat3 => VERSION_CAT3,
227 SignatureLevel::Cat5 => VERSION_CAT5,
228 }
229 }
230
231 fn mldsa_pk_len(self) -> usize {
233 match self {
234 SignatureLevel::Cat2 => MLDSA44_PK_LEN,
235 SignatureLevel::Cat3 => MLDSA65_PK_LEN,
236 SignatureLevel::Cat5 => MLDSA87_PK_LEN,
237 }
238 }
239
240 fn mldsa_sig_len(self) -> usize {
242 match self {
243 SignatureLevel::Cat2 => MLDSA44_SIG_LEN,
244 SignatureLevel::Cat3 => MLDSA65_SIG_LEN,
245 SignatureLevel::Cat5 => MLDSA87_SIG_LEN,
246 }
247 }
248}
249
250#[inline]
254fn random_bytes(buf: &mut [u8]) {
255 getrandom::getrandom(buf).expect("OS CSPRNG unavailable");
256}
257
258struct OsCsprng;
264
265impl TryRng for OsCsprng {
266 type Error = Infallible;
267
268 fn try_next_u32(&mut self) -> Result<u32, Infallible> {
269 let mut b = [0u8; 4];
270 random_bytes(&mut b);
271 Ok(u32::from_le_bytes(b))
272 }
273
274 fn try_next_u64(&mut self) -> Result<u64, Infallible> {
275 let mut b = [0u8; 8];
276 random_bytes(&mut b);
277 Ok(u64::from_le_bytes(b))
278 }
279
280 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Infallible> {
281 random_bytes(dst);
282 Ok(())
283 }
284}
285
286impl TryCryptoRng for OsCsprng {}
287
288fn frame(context: &str, message: &[u8]) -> Vec<u8> {
290 let mut out = Vec::with_capacity(8 + context.len() + message.len());
291 out.extend_from_slice(&(context.len() as u64).to_be_bytes());
292 out.extend_from_slice(context.as_bytes());
293 out.extend_from_slice(message);
294 out
295}
296
297fn level_from_tag(tag: Option<&u8>) -> Result<SignatureLevel, CryptoError> {
299 match tag {
300 Some(&VERSION_CAT2) => Ok(SignatureLevel::Cat2),
301 Some(&VERSION_CAT3) => Ok(SignatureLevel::Cat3),
302 Some(&VERSION_CAT5) => Ok(SignatureLevel::Cat5),
303 _ => Err(CryptoError::Signature(
304 "unknown or missing signature version tag".into(),
305 )),
306 }
307}
308
309fn mldsa_public_key<P: MlDsaParams>(seed: &B32) -> Vec<u8> {
311 let vk = SigningKey::<P>::from_seed(seed).verifying_key().encode();
312 AsRef::<[u8]>::as_ref(&vk).to_vec()
313}
314
315fn mldsa_sign<P: MlDsaParams>(seed: &B32, framed: &[u8]) -> Vec<u8> {
317 let sig = ExpandedSigningKey::<P>::from_seed(seed)
318 .sign_randomized(framed, &[], &mut OsCsprng)
319 .expect("ML-DSA randomized signing (empty context, infallible RNG)")
320 .encode();
321 AsRef::<[u8]>::as_ref(&sig).to_vec()
322}
323
324fn mldsa_verify<P: MlDsaParams>(pk: &[u8], framed: &[u8], sig: &[u8]) -> bool {
326 match (
327 VerifyingKey::<P>::new_from_slice(pk),
328 Signature::<P>::try_from(sig),
329 ) {
330 (Ok(vk), Ok(s)) => MlVerifier::verify(&vk, framed, &s).is_ok(),
331 _ => false,
332 }
333}
334
335pub fn generate_signing_keypair() -> HybridSignatureKeyPair {
339 generate_signing_keypair_with_level(SignatureLevel::Cat3)
340}
341
342pub fn generate_signing_keypair_44() -> HybridSignatureKeyPair {
344 generate_signing_keypair_with_level(SignatureLevel::Cat2)
345}
346
347pub fn generate_signing_keypair_87() -> HybridSignatureKeyPair {
349 generate_signing_keypair_with_level(SignatureLevel::Cat5)
350}
351
352pub fn generate_signing_keypair_with_level(level: SignatureLevel) -> HybridSignatureKeyPair {
357 let mut ed_seed = [0u8; ED25519_SEED_LEN];
358 random_bytes(&mut ed_seed);
359 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
360 random_bytes(&mut ml_seed_bytes);
361
362 let ed_sk = EdSigningKey::from_bytes(&ed_seed);
363 let ed_pk = ed_sk.verifying_key().to_bytes();
364
365 let ml_seed: B32 = ml_seed_bytes.into();
366 let ml_pk = match level {
367 SignatureLevel::Cat2 => mldsa_public_key::<MlDsa44>(&ml_seed),
368 SignatureLevel::Cat3 => mldsa_public_key::<MlDsa65>(&ml_seed),
369 SignatureLevel::Cat5 => mldsa_public_key::<MlDsa87>(&ml_seed),
370 };
371
372 let tag = level.version_tag();
373
374 let mut public_key = Vec::with_capacity(1 + ED25519_PK_LEN + ml_pk.len());
375 public_key.push(tag);
376 public_key.extend_from_slice(&ed_pk);
377 public_key.extend_from_slice(&ml_pk);
378
379 let mut secret_key = Vec::with_capacity(SECRET_KEY_LEN);
380 secret_key.push(tag);
381 secret_key.extend_from_slice(&ed_seed);
382 secret_key.extend_from_slice(&ml_seed_bytes);
383
384 let pair = HybridSignatureKeyPair {
385 public_key: b64::encode(&public_key),
386 secret_key: b64::encode(&secret_key),
387 };
388
389 ed_seed.zeroize();
390 ml_seed_bytes.zeroize();
391 secret_key.zeroize();
392 pair
393}
394
395pub fn derive_public_key(secret_key_b64: &str) -> Result<String, CryptoError> {
405 let mut sk_bytes = b64::decode(secret_key_b64)?;
406 let level = level_from_tag(sk_bytes.first())?;
407
408 if sk_bytes.len() != SECRET_KEY_LEN {
409 sk_bytes.zeroize();
410 return Err(CryptoError::InvalidLength {
411 expected: SECRET_KEY_LEN,
412 got: sk_bytes.len(),
413 });
414 }
415
416 let mut ed_seed = [0u8; ED25519_SEED_LEN];
417 ed_seed.copy_from_slice(&sk_bytes[1..1 + ED25519_SEED_LEN]);
418 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
419 ml_seed_bytes.copy_from_slice(&sk_bytes[1 + ED25519_SEED_LEN..SECRET_KEY_LEN]);
420 sk_bytes.zeroize();
421
422 let ed_pk = EdSigningKey::from_bytes(&ed_seed)
423 .verifying_key()
424 .to_bytes();
425 ed_seed.zeroize();
426
427 let ml_seed: B32 = ml_seed_bytes.into();
428 let ml_pk = match level {
429 SignatureLevel::Cat2 => mldsa_public_key::<MlDsa44>(&ml_seed),
430 SignatureLevel::Cat3 => mldsa_public_key::<MlDsa65>(&ml_seed),
431 SignatureLevel::Cat5 => mldsa_public_key::<MlDsa87>(&ml_seed),
432 };
433 ml_seed_bytes.zeroize();
434
435 let mut public_key = Vec::with_capacity(1 + ED25519_PK_LEN + ml_pk.len());
436 public_key.push(level.version_tag());
437 public_key.extend_from_slice(&ed_pk);
438 public_key.extend_from_slice(&ml_pk);
439
440 Ok(b64::encode(&public_key))
441}
442pub fn sign(message: &[u8], context: &str, secret_key_b64: &str) -> Result<String, CryptoError> {
453 let mut sk_bytes = b64::decode(secret_key_b64)?;
454 let level = level_from_tag(sk_bytes.first())?;
455
456 if sk_bytes.len() != SECRET_KEY_LEN {
457 sk_bytes.zeroize();
458 return Err(CryptoError::InvalidLength {
459 expected: SECRET_KEY_LEN,
460 got: sk_bytes.len(),
461 });
462 }
463
464 let mut ed_seed = [0u8; ED25519_SEED_LEN];
465 ed_seed.copy_from_slice(&sk_bytes[1..1 + ED25519_SEED_LEN]);
466 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
467 ml_seed_bytes.copy_from_slice(&sk_bytes[1 + ED25519_SEED_LEN..SECRET_KEY_LEN]);
468 sk_bytes.zeroize();
469
470 let framed = frame(context, message);
471
472 let ed_sk = EdSigningKey::from_bytes(&ed_seed);
473 let ed_sig = ed_sk.sign(&framed).to_bytes();
474 ed_seed.zeroize();
475
476 let ml_seed: B32 = ml_seed_bytes.into();
477 let ml_sig = match level {
478 SignatureLevel::Cat2 => mldsa_sign::<MlDsa44>(&ml_seed, &framed),
479 SignatureLevel::Cat3 => mldsa_sign::<MlDsa65>(&ml_seed, &framed),
480 SignatureLevel::Cat5 => mldsa_sign::<MlDsa87>(&ml_seed, &framed),
481 };
482 ml_seed_bytes.zeroize();
483
484 let mut out = Vec::with_capacity(1 + ED25519_SIG_LEN + ml_sig.len());
485 out.push(level.version_tag());
486 out.extend_from_slice(&ed_sig);
487 out.extend_from_slice(&ml_sig);
488
489 Ok(b64::encode(&out))
490}
491
492pub fn verify(
501 message: &[u8],
502 context: &str,
503 signature_b64: &str,
504 public_key_b64: &str,
505) -> Result<bool, CryptoError> {
506 let sig = b64::decode(signature_b64)?;
507 let pk = b64::decode(public_key_b64)?;
508
509 let sig_level = level_from_tag(sig.first())?;
510 let pk_level = level_from_tag(pk.first())?;
511 if sig_level != pk_level {
513 return Ok(false);
514 }
515 let level = sig_level;
516
517 if sig.len() != 1 + ED25519_SIG_LEN + level.mldsa_sig_len()
518 || pk.len() != 1 + ED25519_PK_LEN + level.mldsa_pk_len()
519 {
520 return Ok(false);
521 }
522
523 let framed = frame(context, message);
524
525 let ed_pk_bytes: [u8; ED25519_PK_LEN] = pk[1..1 + ED25519_PK_LEN].try_into().unwrap();
526 let ed_sig_bytes: [u8; ED25519_SIG_LEN] = sig[1..1 + ED25519_SIG_LEN].try_into().unwrap();
527 let ml_pk = &pk[1 + ED25519_PK_LEN..];
528 let ml_sig = &sig[1 + ED25519_SIG_LEN..];
529
530 let ed_ok = match EdVerifyingKey::from_bytes(&ed_pk_bytes) {
531 Ok(vk) => vk
532 .verify(&framed, &EdSignature::from_bytes(&ed_sig_bytes))
533 .is_ok(),
534 Err(_) => false,
535 };
536
537 let ml_ok = match level {
538 SignatureLevel::Cat2 => mldsa_verify::<MlDsa44>(ml_pk, &framed, ml_sig),
539 SignatureLevel::Cat3 => mldsa_verify::<MlDsa65>(ml_pk, &framed, ml_sig),
540 SignatureLevel::Cat5 => mldsa_verify::<MlDsa87>(ml_pk, &framed, ml_sig),
541 };
542
543 Ok(ed_ok && ml_ok)
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550
551 fn roundtrip(level: SignatureLevel) {
552 let kp = generate_signing_keypair_with_level(level);
553 let sig = sign(b"hello transparency log", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
554 assert!(
555 verify(
556 b"hello transparency log",
557 SIGN_CONTEXT_V1,
558 &sig,
559 &kp.public_key
560 )
561 .unwrap()
562 );
563 }
564
565 #[test]
566 fn cat2_roundtrip() {
567 roundtrip(SignatureLevel::Cat2);
568 }
569
570 #[test]
571 fn cat3_roundtrip() {
572 roundtrip(SignatureLevel::Cat3);
573 }
574
575 #[test]
576 fn cat5_roundtrip() {
577 roundtrip(SignatureLevel::Cat5);
578 }
579
580 #[test]
581 fn default_is_cat3() {
582 assert_eq!(SignatureLevel::default(), SignatureLevel::Cat3);
583 let kp = generate_signing_keypair();
584 let raw = b64::decode(&kp.public_key).unwrap();
585 assert_eq!(raw[0], VERSION_CAT3);
586 }
587
588 #[test]
589 fn version_tags() {
590 for (level, tag) in [
591 (SignatureLevel::Cat2, VERSION_CAT2),
592 (SignatureLevel::Cat3, VERSION_CAT3),
593 (SignatureLevel::Cat5, VERSION_CAT5),
594 ] {
595 let kp = generate_signing_keypair_with_level(level);
596 let sig = sign(b"x", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
597 assert_eq!(b64::decode(&kp.public_key).unwrap()[0], tag);
598 assert_eq!(b64::decode(&kp.secret_key).unwrap()[0], tag);
599 assert_eq!(b64::decode(&sig).unwrap()[0], tag);
600 }
601 }
602
603 #[test]
604 fn key_and_signature_sizes() {
605 for (level, pk_len, sig_len) in [
606 (SignatureLevel::Cat2, MLDSA44_PK_LEN, MLDSA44_SIG_LEN),
607 (SignatureLevel::Cat3, MLDSA65_PK_LEN, MLDSA65_SIG_LEN),
608 (SignatureLevel::Cat5, MLDSA87_PK_LEN, MLDSA87_SIG_LEN),
609 ] {
610 let kp = generate_signing_keypair_with_level(level);
611 let pk = b64::decode(&kp.public_key).unwrap();
612 let sk = b64::decode(&kp.secret_key).unwrap();
613 let sig = b64::decode(&sign(b"x", SIGN_CONTEXT_V1, &kp.secret_key).unwrap()).unwrap();
614 assert_eq!(pk.len(), 1 + ED25519_PK_LEN + pk_len);
615 assert_eq!(sk.len(), SECRET_KEY_LEN);
616 assert_eq!(sig.len(), 1 + ED25519_SIG_LEN + sig_len);
617 }
618 }
619
620 #[test]
621 fn wrong_key_fails() {
622 let kp1 = generate_signing_keypair();
623 let kp2 = generate_signing_keypair();
624 let sig = sign(b"msg", SIGN_CONTEXT_V1, &kp1.secret_key).unwrap();
625 assert!(!verify(b"msg", SIGN_CONTEXT_V1, &sig, &kp2.public_key).unwrap());
626 }
627
628 #[test]
629 fn tampered_message_fails() {
630 let kp = generate_signing_keypair();
631 let sig = sign(b"original", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
632 assert!(!verify(b"tampered", SIGN_CONTEXT_V1, &sig, &kp.public_key).unwrap());
633 }
634
635 #[test]
636 fn context_separation() {
637 let kp = generate_signing_keypair();
638 let sig = sign(b"msg", "metamorphic/sign/v1", &kp.secret_key).unwrap();
639 assert!(!verify(b"msg", "metamorphic/other/v1", &sig, &kp.public_key).unwrap());
641 }
642
643 #[test]
644 fn empty_message_and_context() {
645 let kp = generate_signing_keypair();
646 let sig = sign(b"", "", &kp.secret_key).unwrap();
647 assert!(verify(b"", "", &sig, &kp.public_key).unwrap());
648 }
649
650 #[test]
651 fn nondeterministic_but_both_valid() {
652 let kp = generate_signing_keypair();
653 let s1 = sign(b"msg", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
654 let s2 = sign(b"msg", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
655 assert_ne!(s1, s2);
657 assert!(verify(b"msg", SIGN_CONTEXT_V1, &s1, &kp.public_key).unwrap());
658 assert!(verify(b"msg", SIGN_CONTEXT_V1, &s2, &kp.public_key).unwrap());
659 }
660
661 #[test]
664 fn strict_and_requires_both() {
665 let kp = generate_signing_keypair();
666 let good = b64::decode(&sign(b"msg", SIGN_CONTEXT_V1, &kp.secret_key).unwrap()).unwrap();
667
668 let mut bad_ed = good.clone();
670 bad_ed[1] ^= 0xFF;
671 assert!(
672 !verify(
673 b"msg",
674 SIGN_CONTEXT_V1,
675 &b64::encode(&bad_ed),
676 &kp.public_key
677 )
678 .unwrap()
679 );
680
681 let mut bad_ml = good.clone();
683 let i = 1 + ED25519_SIG_LEN + 10;
684 bad_ml[i] ^= 0xFF;
685 assert!(
686 !verify(
687 b"msg",
688 SIGN_CONTEXT_V1,
689 &b64::encode(&bad_ml),
690 &kp.public_key
691 )
692 .unwrap()
693 );
694 }
695
696 #[test]
697 fn cross_level_fails() {
698 let kp3 = generate_signing_keypair_with_level(SignatureLevel::Cat3);
699 let kp5 = generate_signing_keypair_with_level(SignatureLevel::Cat5);
700 let sig3 = sign(b"msg", SIGN_CONTEXT_V1, &kp3.secret_key).unwrap();
701 assert!(!verify(b"msg", SIGN_CONTEXT_V1, &sig3, &kp5.public_key).unwrap());
703 }
704
705 #[test]
706 fn unknown_tag_errors() {
707 let bad = b64::encode(&[0x09u8; SECRET_KEY_LEN]);
708 assert!(sign(b"x", SIGN_CONTEXT_V1, &bad).is_err());
709 let pk = b64::encode(&[0x09u8; 100]);
710 let sig = b64::encode(&[0x09u8; 100]);
711 assert!(verify(b"x", SIGN_CONTEXT_V1, &sig, &pk).is_err());
712 }
713
714 #[test]
715 fn bad_base64_errors() {
716 assert!(sign(b"x", SIGN_CONTEXT_V1, "not!base64!").is_err());
717 assert!(verify(b"x", SIGN_CONTEXT_V1, "not!base64!", "also!bad!").is_err());
718 }
719
720 #[test]
721 fn frame_matches_manual() {
722 let ctx = "metamorphic/sign/v1";
723 let msg = b"payload";
724 let mut expected = Vec::new();
725 expected.extend_from_slice(&(ctx.len() as u64).to_be_bytes());
726 expected.extend_from_slice(ctx.as_bytes());
727 expected.extend_from_slice(msg);
728 assert_eq!(frame(ctx, msg), expected);
729 }
730
731 #[test]
732 fn frame_no_boundary_confusion() {
733 assert_ne!(frame("ab", b"c"), frame("a", b"bc"));
734 }
735
736 #[test]
737 fn secret_key_debug_is_redacted() {
738 let kp = generate_signing_keypair();
739 let shown = format!("{kp:?}");
740 assert!(shown.contains("<redacted>"));
741 assert!(!shown.contains(&kp.secret_key));
742 }
743
744 #[test]
745 fn keygen_public_key_matches_derived() {
746 for level in [
747 SignatureLevel::Cat2,
748 SignatureLevel::Cat3,
749 SignatureLevel::Cat5,
750 ] {
751 let kp = generate_signing_keypair_with_level(level);
752 assert_eq!(derive_public_key(&kp.secret_key).unwrap(), kp.public_key);
753 }
754 }
755
756 use proptest::prelude::*;
757
758 proptest! {
759 #[test]
760 fn roundtrip_arbitrary_message(msg: Vec<u8>, ctx in "[a-z/]{0,32}") {
761 let kp = generate_signing_keypair();
762 let sig = sign(&msg, &ctx, &kp.secret_key).unwrap();
763 prop_assert!(verify(&msg, &ctx, &sig, &kp.public_key).unwrap());
764 }
765 }
766}