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 ed448_goldilocks::{
141 Signature as Ed448Signature, SigningKey as Ed448SigningKey, VerifyingKey as Ed448VerifyingKey,
142};
143use p521::ecdsa::signature::RandomizedSigner;
144use p521::ecdsa::{
145 Signature as P521Signature, SigningKey as P521SigningKey, VerifyingKey as P521VerifyingKey,
146};
147use p521::elliptic_curve::FieldBytes;
148use p521::elliptic_curve::ops::Reduce;
149use p521::{NistP521, NonZeroScalar, Scalar, SecretKey as P521SecretKey};
150
151use crate::CryptoError;
152use crate::b64;
153use crate::suite::Suite;
154
155pub const SIGN_CONTEXT_V1: &str = "metamorphic/sign/v1";
162
163const VERSION_CAT2: u8 = 0x01;
165const VERSION_CAT3: u8 = 0x02;
167const VERSION_CAT5: u8 = 0x03;
169
170const VERSION_SIG_PURE_CNSA2: u8 = 0x10;
178const VERSION_SIG_MATCHED_CAT3: u8 = 0x13;
180const VERSION_SIG_MATCHED_CAT5: u8 = 0x14;
182
183const ED448_SEED_LEN: usize = 57;
185const ED448_PK_LEN: usize = 57;
187const ED448_SIG_LEN: usize = 114;
189
190const P521_SK_LEN: usize = 66;
192const P521_PK_LEN: usize = 133;
194const P521_SIG_LEN: usize = 132;
196
197const ED25519_SEED_LEN: usize = 32;
199const ED25519_PK_LEN: usize = 32;
201const ED25519_SIG_LEN: usize = 64;
203const MLDSA_SEED_LEN: usize = 32;
205
206const SECRET_KEY_LEN: usize = 1 + ED25519_SEED_LEN + MLDSA_SEED_LEN;
208
209const MLDSA44_PK_LEN: usize = 1312;
212const MLDSA44_SIG_LEN: usize = 2420;
214const MLDSA65_PK_LEN: usize = 1952;
217const MLDSA65_SIG_LEN: usize = 3309;
219const MLDSA87_PK_LEN: usize = 2592;
222const MLDSA87_SIG_LEN: usize = 4627;
224
225#[derive(Clone, Zeroize, ZeroizeOnDrop)]
232pub struct HybridSignatureKeyPair {
233 #[zeroize(skip)]
235 pub public_key: String,
236 pub secret_key: String,
238}
239
240impl std::fmt::Debug for HybridSignatureKeyPair {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 f.debug_struct("HybridSignatureKeyPair")
243 .field("public_key", &self.public_key)
244 .field("secret_key", &"<redacted>")
245 .finish()
246 }
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
251pub enum SignatureLevel {
252 Cat2,
254 #[default]
256 Cat3,
257 Cat5,
259}
260
261impl SignatureLevel {
262 fn version_tag(self) -> u8 {
264 match self {
265 SignatureLevel::Cat2 => VERSION_CAT2,
266 SignatureLevel::Cat3 => VERSION_CAT3,
267 SignatureLevel::Cat5 => VERSION_CAT5,
268 }
269 }
270
271 fn mldsa_pk_len(self) -> usize {
273 match self {
274 SignatureLevel::Cat2 => MLDSA44_PK_LEN,
275 SignatureLevel::Cat3 => MLDSA65_PK_LEN,
276 SignatureLevel::Cat5 => MLDSA87_PK_LEN,
277 }
278 }
279
280 fn mldsa_sig_len(self) -> usize {
282 match self {
283 SignatureLevel::Cat2 => MLDSA44_SIG_LEN,
284 SignatureLevel::Cat3 => MLDSA65_SIG_LEN,
285 SignatureLevel::Cat5 => MLDSA87_SIG_LEN,
286 }
287 }
288}
289
290#[inline]
294fn random_bytes(buf: &mut [u8]) {
295 getrandom::getrandom(buf).expect("OS CSPRNG unavailable");
296}
297
298struct OsCsprng;
304
305impl TryRng for OsCsprng {
306 type Error = Infallible;
307
308 fn try_next_u32(&mut self) -> Result<u32, Infallible> {
309 let mut b = [0u8; 4];
310 random_bytes(&mut b);
311 Ok(u32::from_le_bytes(b))
312 }
313
314 fn try_next_u64(&mut self) -> Result<u64, Infallible> {
315 let mut b = [0u8; 8];
316 random_bytes(&mut b);
317 Ok(u64::from_le_bytes(b))
318 }
319
320 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Infallible> {
321 random_bytes(dst);
322 Ok(())
323 }
324}
325
326impl TryCryptoRng for OsCsprng {}
327
328fn frame(context: &str, message: &[u8]) -> Vec<u8> {
330 let mut out = Vec::with_capacity(8 + context.len() + message.len());
331 out.extend_from_slice(&(context.len() as u64).to_be_bytes());
332 out.extend_from_slice(context.as_bytes());
333 out.extend_from_slice(message);
334 out
335}
336
337fn level_from_tag(tag: Option<&u8>) -> Result<SignatureLevel, CryptoError> {
339 match tag {
340 Some(&VERSION_CAT2) => Ok(SignatureLevel::Cat2),
341 Some(&VERSION_CAT3) => Ok(SignatureLevel::Cat3),
342 Some(&VERSION_CAT5) => Ok(SignatureLevel::Cat5),
343 _ => Err(CryptoError::Signature(
344 "unknown or missing signature version tag".into(),
345 )),
346 }
347}
348
349fn mldsa_public_key<P: MlDsaParams>(seed: &B32) -> Vec<u8> {
351 let vk = SigningKey::<P>::from_seed(seed).verifying_key().encode();
352 AsRef::<[u8]>::as_ref(&vk).to_vec()
353}
354
355fn mldsa_sign<P: MlDsaParams>(seed: &B32, framed: &[u8]) -> Vec<u8> {
357 let sig = ExpandedSigningKey::<P>::from_seed(seed)
358 .sign_randomized(framed, &[], &mut OsCsprng)
359 .expect("ML-DSA randomized signing (empty context, infallible RNG)")
360 .encode();
361 AsRef::<[u8]>::as_ref(&sig).to_vec()
362}
363
364fn mldsa_verify<P: MlDsaParams>(pk: &[u8], framed: &[u8], sig: &[u8]) -> bool {
366 match (
367 VerifyingKey::<P>::new_from_slice(pk),
368 Signature::<P>::try_from(sig),
369 ) {
370 (Ok(vk), Ok(s)) => MlVerifier::verify(&vk, framed, &s).is_ok(),
371 _ => false,
372 }
373}
374
375fn p521_signing_key_from_bytes(bytes: &[u8; P521_SK_LEN]) -> P521SigningKey {
379 let fb: FieldBytes<NistP521> = (*bytes).into();
380 let scalar = <Scalar as Reduce<FieldBytes<NistP521>>>::reduce(&fb);
381 let nz: NonZeroScalar =
382 Option::from(NonZeroScalar::new(scalar)).expect("P-521 scalar reduced to zero");
383 P521SigningKey::from(P521SecretKey::from(nz))
384}
385
386fn ed448_keypair(
388 seed: &[u8; ED448_SEED_LEN],
389) -> Result<([u8; ED448_PK_LEN], Ed448SigningKey), CryptoError> {
390 let sk = Ed448SigningKey::try_from(&seed[..])
391 .map_err(|_| CryptoError::Signature("invalid Ed448 seed".into()))?;
392 let pk = sk.verifying_key().to_bytes();
393 Ok((pk, sk))
394}
395
396pub fn generate_signing_keypair_suite(
409 suite: Suite,
410 level: SignatureLevel,
411) -> Result<HybridSignatureKeyPair, CryptoError> {
412 match (suite, level) {
413 (Suite::Hybrid, _) | (Suite::HybridMatched, SignatureLevel::Cat2) => {
414 Ok(generate_signing_keypair_with_level(level))
415 }
416 (Suite::HybridMatched, SignatureLevel::Cat3) => Ok(generate_matched_cat3_keypair()),
417 (Suite::HybridMatched, SignatureLevel::Cat5) => Ok(generate_matched_cat5_keypair()),
418 (Suite::PureCnsa2, SignatureLevel::Cat5) => Ok(generate_pure_cnsa2_keypair()),
419 (Suite::PureCnsa2, _) => Err(CryptoError::Signature(
420 "PureCnsa2 signatures are Cat-5 (ML-DSA-87) only in v0.7.0".into(),
421 )),
422 }
423}
424
425fn generate_pure_cnsa2_keypair() -> HybridSignatureKeyPair {
426 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
427 random_bytes(&mut ml_seed_bytes);
428 let ml_seed: B32 = ml_seed_bytes.into();
429 let ml_pk = mldsa_public_key::<MlDsa87>(&ml_seed);
430
431 let mut public_key = Vec::with_capacity(1 + ml_pk.len());
432 public_key.push(VERSION_SIG_PURE_CNSA2);
433 public_key.extend_from_slice(&ml_pk);
434
435 let mut secret_key = Vec::with_capacity(1 + MLDSA_SEED_LEN);
436 secret_key.push(VERSION_SIG_PURE_CNSA2);
437 secret_key.extend_from_slice(&ml_seed_bytes);
438
439 let pair = HybridSignatureKeyPair {
440 public_key: b64::encode(&public_key),
441 secret_key: b64::encode(&secret_key),
442 };
443 ml_seed_bytes.zeroize();
444 secret_key.zeroize();
445 pair
446}
447
448fn generate_matched_cat3_keypair() -> HybridSignatureKeyPair {
449 let mut ed_seed = [0u8; ED448_SEED_LEN];
450 random_bytes(&mut ed_seed);
451 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
452 random_bytes(&mut ml_seed_bytes);
453
454 let (ed_pk, _) = ed448_keypair(&ed_seed).expect("freshly generated Ed448 seed");
455 let ml_seed: B32 = ml_seed_bytes.into();
456 let ml_pk = mldsa_public_key::<MlDsa65>(&ml_seed);
457
458 let mut public_key = Vec::with_capacity(1 + ED448_PK_LEN + ml_pk.len());
459 public_key.push(VERSION_SIG_MATCHED_CAT3);
460 public_key.extend_from_slice(&ed_pk);
461 public_key.extend_from_slice(&ml_pk);
462
463 let mut secret_key = Vec::with_capacity(1 + ED448_SEED_LEN + MLDSA_SEED_LEN);
464 secret_key.push(VERSION_SIG_MATCHED_CAT3);
465 secret_key.extend_from_slice(&ed_seed);
466 secret_key.extend_from_slice(&ml_seed_bytes);
467
468 let pair = HybridSignatureKeyPair {
469 public_key: b64::encode(&public_key),
470 secret_key: b64::encode(&secret_key),
471 };
472 ed_seed.zeroize();
473 ml_seed_bytes.zeroize();
474 secret_key.zeroize();
475 pair
476}
477
478fn generate_matched_cat5_keypair() -> HybridSignatureKeyPair {
479 let mut ec_seed = [0u8; P521_SK_LEN];
480 random_bytes(&mut ec_seed);
481 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
482 random_bytes(&mut ml_seed_bytes);
483
484 let signing = p521_signing_key_from_bytes(&ec_seed);
485 let ec_pk: [u8; P521_PK_LEN] = signing
486 .verifying_key()
487 .to_sec1_point(false)
488 .as_bytes()
489 .try_into()
490 .expect("uncompressed P-521 public key is 133 bytes");
491 let ml_seed: B32 = ml_seed_bytes.into();
492 let ml_pk = mldsa_public_key::<MlDsa87>(&ml_seed);
493
494 let mut public_key = Vec::with_capacity(1 + P521_PK_LEN + ml_pk.len());
495 public_key.push(VERSION_SIG_MATCHED_CAT5);
496 public_key.extend_from_slice(&ec_pk);
497 public_key.extend_from_slice(&ml_pk);
498
499 let mut secret_key = Vec::with_capacity(1 + P521_SK_LEN + MLDSA_SEED_LEN);
500 secret_key.push(VERSION_SIG_MATCHED_CAT5);
501 secret_key.extend_from_slice(&ec_seed);
502 secret_key.extend_from_slice(&ml_seed_bytes);
503
504 let pair = HybridSignatureKeyPair {
505 public_key: b64::encode(&public_key),
506 secret_key: b64::encode(&secret_key),
507 };
508 ec_seed.zeroize();
509 ml_seed_bytes.zeroize();
510 secret_key.zeroize();
511 pair
512}
513
514pub fn generate_signing_keypair() -> HybridSignatureKeyPair {
518 generate_signing_keypair_with_level(SignatureLevel::Cat3)
519}
520
521pub fn generate_signing_keypair_44() -> HybridSignatureKeyPair {
523 generate_signing_keypair_with_level(SignatureLevel::Cat2)
524}
525
526pub fn generate_signing_keypair_87() -> HybridSignatureKeyPair {
528 generate_signing_keypair_with_level(SignatureLevel::Cat5)
529}
530
531pub fn generate_signing_keypair_with_level(level: SignatureLevel) -> HybridSignatureKeyPair {
536 let mut ed_seed = [0u8; ED25519_SEED_LEN];
537 random_bytes(&mut ed_seed);
538 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
539 random_bytes(&mut ml_seed_bytes);
540
541 let ed_sk = EdSigningKey::from_bytes(&ed_seed);
542 let ed_pk = ed_sk.verifying_key().to_bytes();
543
544 let ml_seed: B32 = ml_seed_bytes.into();
545 let ml_pk = match level {
546 SignatureLevel::Cat2 => mldsa_public_key::<MlDsa44>(&ml_seed),
547 SignatureLevel::Cat3 => mldsa_public_key::<MlDsa65>(&ml_seed),
548 SignatureLevel::Cat5 => mldsa_public_key::<MlDsa87>(&ml_seed),
549 };
550
551 let tag = level.version_tag();
552
553 let mut public_key = Vec::with_capacity(1 + ED25519_PK_LEN + ml_pk.len());
554 public_key.push(tag);
555 public_key.extend_from_slice(&ed_pk);
556 public_key.extend_from_slice(&ml_pk);
557
558 let mut secret_key = Vec::with_capacity(SECRET_KEY_LEN);
559 secret_key.push(tag);
560 secret_key.extend_from_slice(&ed_seed);
561 secret_key.extend_from_slice(&ml_seed_bytes);
562
563 let pair = HybridSignatureKeyPair {
564 public_key: b64::encode(&public_key),
565 secret_key: b64::encode(&secret_key),
566 };
567
568 ed_seed.zeroize();
569 ml_seed_bytes.zeroize();
570 secret_key.zeroize();
571 pair
572}
573
574pub fn derive_public_key(secret_key_b64: &str) -> Result<String, CryptoError> {
584 let mut sk_bytes = b64::decode(secret_key_b64)?;
585 if let Some(&tag) = sk_bytes.first() {
586 if is_new_suite_tag(tag) {
587 let out = derive_public_key_new_suite(&sk_bytes);
588 sk_bytes.zeroize();
589 return out;
590 }
591 }
592 let level = level_from_tag(sk_bytes.first())?;
593
594 if sk_bytes.len() != SECRET_KEY_LEN {
595 sk_bytes.zeroize();
596 return Err(CryptoError::InvalidLength {
597 expected: SECRET_KEY_LEN,
598 got: sk_bytes.len(),
599 });
600 }
601
602 let mut ed_seed = [0u8; ED25519_SEED_LEN];
603 ed_seed.copy_from_slice(&sk_bytes[1..1 + ED25519_SEED_LEN]);
604 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
605 ml_seed_bytes.copy_from_slice(&sk_bytes[1 + ED25519_SEED_LEN..SECRET_KEY_LEN]);
606 sk_bytes.zeroize();
607
608 let ed_pk = EdSigningKey::from_bytes(&ed_seed)
609 .verifying_key()
610 .to_bytes();
611 ed_seed.zeroize();
612
613 let ml_seed: B32 = ml_seed_bytes.into();
614 let ml_pk = match level {
615 SignatureLevel::Cat2 => mldsa_public_key::<MlDsa44>(&ml_seed),
616 SignatureLevel::Cat3 => mldsa_public_key::<MlDsa65>(&ml_seed),
617 SignatureLevel::Cat5 => mldsa_public_key::<MlDsa87>(&ml_seed),
618 };
619 ml_seed_bytes.zeroize();
620
621 let mut public_key = Vec::with_capacity(1 + ED25519_PK_LEN + ml_pk.len());
622 public_key.push(level.version_tag());
623 public_key.extend_from_slice(&ed_pk);
624 public_key.extend_from_slice(&ml_pk);
625
626 Ok(b64::encode(&public_key))
627}
628pub fn sign(message: &[u8], context: &str, secret_key_b64: &str) -> Result<String, CryptoError> {
639 let mut sk_bytes = b64::decode(secret_key_b64)?;
640 if let Some(&tag) = sk_bytes.first() {
641 if is_new_suite_tag(tag) {
642 let out = sign_new_suite(message, context, &sk_bytes);
643 sk_bytes.zeroize();
644 return out;
645 }
646 }
647 let level = level_from_tag(sk_bytes.first())?;
648
649 if sk_bytes.len() != SECRET_KEY_LEN {
650 sk_bytes.zeroize();
651 return Err(CryptoError::InvalidLength {
652 expected: SECRET_KEY_LEN,
653 got: sk_bytes.len(),
654 });
655 }
656
657 let mut ed_seed = [0u8; ED25519_SEED_LEN];
658 ed_seed.copy_from_slice(&sk_bytes[1..1 + ED25519_SEED_LEN]);
659 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
660 ml_seed_bytes.copy_from_slice(&sk_bytes[1 + ED25519_SEED_LEN..SECRET_KEY_LEN]);
661 sk_bytes.zeroize();
662
663 let framed = frame(context, message);
664
665 let ed_sk = EdSigningKey::from_bytes(&ed_seed);
666 let ed_sig = ed_sk.sign(&framed).to_bytes();
667 ed_seed.zeroize();
668
669 let ml_seed: B32 = ml_seed_bytes.into();
670 let ml_sig = match level {
671 SignatureLevel::Cat2 => mldsa_sign::<MlDsa44>(&ml_seed, &framed),
672 SignatureLevel::Cat3 => mldsa_sign::<MlDsa65>(&ml_seed, &framed),
673 SignatureLevel::Cat5 => mldsa_sign::<MlDsa87>(&ml_seed, &framed),
674 };
675 ml_seed_bytes.zeroize();
676
677 let mut out = Vec::with_capacity(1 + ED25519_SIG_LEN + ml_sig.len());
678 out.push(level.version_tag());
679 out.extend_from_slice(&ed_sig);
680 out.extend_from_slice(&ml_sig);
681
682 Ok(b64::encode(&out))
683}
684
685pub fn verify(
694 message: &[u8],
695 context: &str,
696 signature_b64: &str,
697 public_key_b64: &str,
698) -> Result<bool, CryptoError> {
699 let sig = b64::decode(signature_b64)?;
700 let pk = b64::decode(public_key_b64)?;
701
702 match (sig.first(), pk.first()) {
704 (Some(&s), _) if is_new_suite_tag(s) => {
705 return verify_new_suite(message, context, &sig, &pk);
706 }
707 (_, Some(&p)) if is_new_suite_tag(p) => return Ok(false),
708 _ => {}
709 }
710
711 let sig_level = level_from_tag(sig.first())?;
712 let pk_level = level_from_tag(pk.first())?;
713 if sig_level != pk_level {
715 return Ok(false);
716 }
717 let level = sig_level;
718
719 if sig.len() != 1 + ED25519_SIG_LEN + level.mldsa_sig_len()
720 || pk.len() != 1 + ED25519_PK_LEN + level.mldsa_pk_len()
721 {
722 return Ok(false);
723 }
724
725 let framed = frame(context, message);
726
727 let ed_pk_bytes: [u8; ED25519_PK_LEN] = pk[1..1 + ED25519_PK_LEN].try_into().unwrap();
728 let ed_sig_bytes: [u8; ED25519_SIG_LEN] = sig[1..1 + ED25519_SIG_LEN].try_into().unwrap();
729 let ml_pk = &pk[1 + ED25519_PK_LEN..];
730 let ml_sig = &sig[1 + ED25519_SIG_LEN..];
731
732 let ed_ok = match EdVerifyingKey::from_bytes(&ed_pk_bytes) {
733 Ok(vk) => vk
734 .verify(&framed, &EdSignature::from_bytes(&ed_sig_bytes))
735 .is_ok(),
736 Err(_) => false,
737 };
738
739 let ml_ok = match level {
740 SignatureLevel::Cat2 => mldsa_verify::<MlDsa44>(ml_pk, &framed, ml_sig),
741 SignatureLevel::Cat3 => mldsa_verify::<MlDsa65>(ml_pk, &framed, ml_sig),
742 SignatureLevel::Cat5 => mldsa_verify::<MlDsa87>(ml_pk, &framed, ml_sig),
743 };
744
745 Ok(ed_ok && ml_ok)
747}
748
749fn is_new_suite_tag(tag: u8) -> bool {
753 matches!(
754 tag,
755 VERSION_SIG_PURE_CNSA2 | VERSION_SIG_MATCHED_CAT3 | VERSION_SIG_MATCHED_CAT5
756 )
757}
758
759fn sign_new_suite(message: &[u8], context: &str, sk: &[u8]) -> Result<String, CryptoError> {
760 let framed = frame(context, message);
761 match sk[0] {
762 VERSION_SIG_PURE_CNSA2 => {
763 if sk.len() != 1 + MLDSA_SEED_LEN {
764 return Err(CryptoError::InvalidLength {
765 expected: 1 + MLDSA_SEED_LEN,
766 got: sk.len(),
767 });
768 }
769 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
770 ml_seed_bytes.copy_from_slice(&sk[1..]);
771 let ml_seed: B32 = ml_seed_bytes.into();
772 let ml_sig = mldsa_sign::<MlDsa87>(&ml_seed, &framed);
773 ml_seed_bytes.zeroize();
774 let mut out = Vec::with_capacity(1 + ml_sig.len());
775 out.push(VERSION_SIG_PURE_CNSA2);
776 out.extend_from_slice(&ml_sig);
777 Ok(b64::encode(&out))
778 }
779 VERSION_SIG_MATCHED_CAT3 => {
780 if sk.len() != 1 + ED448_SEED_LEN + MLDSA_SEED_LEN {
781 return Err(CryptoError::InvalidLength {
782 expected: 1 + ED448_SEED_LEN + MLDSA_SEED_LEN,
783 got: sk.len(),
784 });
785 }
786 let mut ed_seed = [0u8; ED448_SEED_LEN];
787 ed_seed.copy_from_slice(&sk[1..1 + ED448_SEED_LEN]);
788 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
789 ml_seed_bytes.copy_from_slice(&sk[1 + ED448_SEED_LEN..]);
790
791 let (_, ed_sk) = ed448_keypair(&ed_seed)?;
792 let ed_sig = ed_sk.sign_raw(&framed).to_bytes();
793 ed_seed.zeroize();
794 let ml_seed: B32 = ml_seed_bytes.into();
795 let ml_sig = mldsa_sign::<MlDsa65>(&ml_seed, &framed);
796 ml_seed_bytes.zeroize();
797
798 let mut out = Vec::with_capacity(1 + ED448_SIG_LEN + ml_sig.len());
799 out.push(VERSION_SIG_MATCHED_CAT3);
800 out.extend_from_slice(&ed_sig);
801 out.extend_from_slice(&ml_sig);
802 Ok(b64::encode(&out))
803 }
804 VERSION_SIG_MATCHED_CAT5 => {
805 if sk.len() != 1 + P521_SK_LEN + MLDSA_SEED_LEN {
806 return Err(CryptoError::InvalidLength {
807 expected: 1 + P521_SK_LEN + MLDSA_SEED_LEN,
808 got: sk.len(),
809 });
810 }
811 let mut ec_seed = [0u8; P521_SK_LEN];
812 ec_seed.copy_from_slice(&sk[1..1 + P521_SK_LEN]);
813 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
814 ml_seed_bytes.copy_from_slice(&sk[1 + P521_SK_LEN..]);
815
816 let signing = p521_signing_key_from_bytes(&ec_seed);
818 let ec_sig: P521Signature = signing
819 .try_sign_with_rng(&mut OsCsprng, &framed)
820 .map_err(|_| CryptoError::Signature("ECDSA-P-521 signing failed".into()))?;
821 let ec_sig_bytes = ec_sig.to_bytes();
822 ec_seed.zeroize();
823 let ml_seed: B32 = ml_seed_bytes.into();
824 let ml_sig = mldsa_sign::<MlDsa87>(&ml_seed, &framed);
825 ml_seed_bytes.zeroize();
826
827 let mut out = Vec::with_capacity(1 + P521_SIG_LEN + ml_sig.len());
828 out.push(VERSION_SIG_MATCHED_CAT5);
829 out.extend_from_slice(ec_sig_bytes.as_slice());
830 out.extend_from_slice(&ml_sig);
831 Ok(b64::encode(&out))
832 }
833 _ => Err(CryptoError::Signature("not a CNSA-2.0 suite tag".into())),
834 }
835}
836
837fn ed448_verify(pk: &[u8], framed: &[u8], sig: &[u8]) -> bool {
838 let Ok(pk_arr): Result<[u8; ED448_PK_LEN], _> = pk.try_into() else {
839 return false;
840 };
841 let Ok(vk) = Ed448VerifyingKey::from_bytes(&pk_arr) else {
842 return false;
843 };
844 let Ok(signature) = Ed448Signature::try_from(sig) else {
845 return false;
846 };
847 vk.verify_raw(&signature, framed).is_ok()
848}
849
850fn p521_ecdsa_verify(pk: &[u8], framed: &[u8], sig: &[u8]) -> bool {
851 let Ok(vk) = P521VerifyingKey::from_sec1_bytes(pk) else {
852 return false;
853 };
854 let Ok(signature) = P521Signature::from_slice(sig) else {
855 return false;
856 };
857 vk.verify(framed, &signature).is_ok()
858}
859
860fn verify_new_suite(
861 message: &[u8],
862 context: &str,
863 sig: &[u8],
864 pk: &[u8],
865) -> Result<bool, CryptoError> {
866 if sig[0] != pk[0] {
869 return Ok(false);
870 }
871 let framed = frame(context, message);
872 match sig[0] {
873 VERSION_SIG_PURE_CNSA2 => {
874 if sig.len() != 1 + MLDSA87_SIG_LEN || pk.len() != 1 + MLDSA87_PK_LEN {
875 return Ok(false);
876 }
877 Ok(mldsa_verify::<MlDsa87>(&pk[1..], &framed, &sig[1..]))
878 }
879 VERSION_SIG_MATCHED_CAT3 => {
880 if sig.len() != 1 + ED448_SIG_LEN + MLDSA65_SIG_LEN
881 || pk.len() != 1 + ED448_PK_LEN + MLDSA65_PK_LEN
882 {
883 return Ok(false);
884 }
885 let ed_ok = ed448_verify(
886 &pk[1..1 + ED448_PK_LEN],
887 &framed,
888 &sig[1..1 + ED448_SIG_LEN],
889 );
890 let ml_ok = mldsa_verify::<MlDsa65>(
891 &pk[1 + ED448_PK_LEN..],
892 &framed,
893 &sig[1 + ED448_SIG_LEN..],
894 );
895 Ok(ed_ok && ml_ok)
896 }
897 VERSION_SIG_MATCHED_CAT5 => {
898 if sig.len() != 1 + P521_SIG_LEN + MLDSA87_SIG_LEN
899 || pk.len() != 1 + P521_PK_LEN + MLDSA87_PK_LEN
900 {
901 return Ok(false);
902 }
903 let ec_ok =
904 p521_ecdsa_verify(&pk[1..1 + P521_PK_LEN], &framed, &sig[1..1 + P521_SIG_LEN]);
905 let ml_ok =
906 mldsa_verify::<MlDsa87>(&pk[1 + P521_PK_LEN..], &framed, &sig[1 + P521_SIG_LEN..]);
907 Ok(ec_ok && ml_ok)
908 }
909 _ => Err(CryptoError::Signature("not a CNSA-2.0 suite tag".into())),
910 }
911}
912
913fn derive_public_key_new_suite(sk: &[u8]) -> Result<String, CryptoError> {
914 match sk[0] {
915 VERSION_SIG_PURE_CNSA2 => {
916 if sk.len() != 1 + MLDSA_SEED_LEN {
917 return Err(CryptoError::InvalidLength {
918 expected: 1 + MLDSA_SEED_LEN,
919 got: sk.len(),
920 });
921 }
922 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
923 ml_seed_bytes.copy_from_slice(&sk[1..]);
924 let ml_seed: B32 = ml_seed_bytes.into();
925 let ml_pk = mldsa_public_key::<MlDsa87>(&ml_seed);
926 ml_seed_bytes.zeroize();
927 let mut pk = Vec::with_capacity(1 + ml_pk.len());
928 pk.push(VERSION_SIG_PURE_CNSA2);
929 pk.extend_from_slice(&ml_pk);
930 Ok(b64::encode(&pk))
931 }
932 VERSION_SIG_MATCHED_CAT3 => {
933 if sk.len() != 1 + ED448_SEED_LEN + MLDSA_SEED_LEN {
934 return Err(CryptoError::InvalidLength {
935 expected: 1 + ED448_SEED_LEN + MLDSA_SEED_LEN,
936 got: sk.len(),
937 });
938 }
939 let mut ed_seed = [0u8; ED448_SEED_LEN];
940 ed_seed.copy_from_slice(&sk[1..1 + ED448_SEED_LEN]);
941 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
942 ml_seed_bytes.copy_from_slice(&sk[1 + ED448_SEED_LEN..]);
943 let (ed_pk, _) = ed448_keypair(&ed_seed)?;
944 ed_seed.zeroize();
945 let ml_seed: B32 = ml_seed_bytes.into();
946 let ml_pk = mldsa_public_key::<MlDsa65>(&ml_seed);
947 ml_seed_bytes.zeroize();
948 let mut pk = Vec::with_capacity(1 + ED448_PK_LEN + ml_pk.len());
949 pk.push(VERSION_SIG_MATCHED_CAT3);
950 pk.extend_from_slice(&ed_pk);
951 pk.extend_from_slice(&ml_pk);
952 Ok(b64::encode(&pk))
953 }
954 VERSION_SIG_MATCHED_CAT5 => {
955 if sk.len() != 1 + P521_SK_LEN + MLDSA_SEED_LEN {
956 return Err(CryptoError::InvalidLength {
957 expected: 1 + P521_SK_LEN + MLDSA_SEED_LEN,
958 got: sk.len(),
959 });
960 }
961 let mut ec_seed = [0u8; P521_SK_LEN];
962 ec_seed.copy_from_slice(&sk[1..1 + P521_SK_LEN]);
963 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
964 ml_seed_bytes.copy_from_slice(&sk[1 + P521_SK_LEN..]);
965 let signing = p521_signing_key_from_bytes(&ec_seed);
966 let ec_pk: [u8; P521_PK_LEN] = signing
967 .verifying_key()
968 .to_sec1_point(false)
969 .as_bytes()
970 .try_into()
971 .expect("uncompressed P-521 public key is 133 bytes");
972 ec_seed.zeroize();
973 let ml_seed: B32 = ml_seed_bytes.into();
974 let ml_pk = mldsa_public_key::<MlDsa87>(&ml_seed);
975 ml_seed_bytes.zeroize();
976 let mut pk = Vec::with_capacity(1 + P521_PK_LEN + ml_pk.len());
977 pk.push(VERSION_SIG_MATCHED_CAT5);
978 pk.extend_from_slice(&ec_pk);
979 pk.extend_from_slice(&ml_pk);
980 Ok(b64::encode(&pk))
981 }
982 _ => Err(CryptoError::Signature("not a CNSA-2.0 suite tag".into())),
983 }
984}
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989
990 fn roundtrip(level: SignatureLevel) {
991 let kp = generate_signing_keypair_with_level(level);
992 let sig = sign(b"hello transparency log", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
993 assert!(
994 verify(
995 b"hello transparency log",
996 SIGN_CONTEXT_V1,
997 &sig,
998 &kp.public_key
999 )
1000 .unwrap()
1001 );
1002 }
1003
1004 #[test]
1005 fn cat2_roundtrip() {
1006 roundtrip(SignatureLevel::Cat2);
1007 }
1008
1009 #[test]
1010 fn cat3_roundtrip() {
1011 roundtrip(SignatureLevel::Cat3);
1012 }
1013
1014 #[test]
1015 fn cat5_roundtrip() {
1016 roundtrip(SignatureLevel::Cat5);
1017 }
1018
1019 #[test]
1020 fn default_is_cat3() {
1021 assert_eq!(SignatureLevel::default(), SignatureLevel::Cat3);
1022 let kp = generate_signing_keypair();
1023 let raw = b64::decode(&kp.public_key).unwrap();
1024 assert_eq!(raw[0], VERSION_CAT3);
1025 }
1026
1027 #[test]
1028 fn version_tags() {
1029 for (level, tag) in [
1030 (SignatureLevel::Cat2, VERSION_CAT2),
1031 (SignatureLevel::Cat3, VERSION_CAT3),
1032 (SignatureLevel::Cat5, VERSION_CAT5),
1033 ] {
1034 let kp = generate_signing_keypair_with_level(level);
1035 let sig = sign(b"x", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1036 assert_eq!(b64::decode(&kp.public_key).unwrap()[0], tag);
1037 assert_eq!(b64::decode(&kp.secret_key).unwrap()[0], tag);
1038 assert_eq!(b64::decode(&sig).unwrap()[0], tag);
1039 }
1040 }
1041
1042 #[test]
1043 fn key_and_signature_sizes() {
1044 for (level, pk_len, sig_len) in [
1045 (SignatureLevel::Cat2, MLDSA44_PK_LEN, MLDSA44_SIG_LEN),
1046 (SignatureLevel::Cat3, MLDSA65_PK_LEN, MLDSA65_SIG_LEN),
1047 (SignatureLevel::Cat5, MLDSA87_PK_LEN, MLDSA87_SIG_LEN),
1048 ] {
1049 let kp = generate_signing_keypair_with_level(level);
1050 let pk = b64::decode(&kp.public_key).unwrap();
1051 let sk = b64::decode(&kp.secret_key).unwrap();
1052 let sig = b64::decode(&sign(b"x", SIGN_CONTEXT_V1, &kp.secret_key).unwrap()).unwrap();
1053 assert_eq!(pk.len(), 1 + ED25519_PK_LEN + pk_len);
1054 assert_eq!(sk.len(), SECRET_KEY_LEN);
1055 assert_eq!(sig.len(), 1 + ED25519_SIG_LEN + sig_len);
1056 }
1057 }
1058
1059 #[test]
1060 fn wrong_key_fails() {
1061 let kp1 = generate_signing_keypair();
1062 let kp2 = generate_signing_keypair();
1063 let sig = sign(b"msg", SIGN_CONTEXT_V1, &kp1.secret_key).unwrap();
1064 assert!(!verify(b"msg", SIGN_CONTEXT_V1, &sig, &kp2.public_key).unwrap());
1065 }
1066
1067 #[test]
1068 fn tampered_message_fails() {
1069 let kp = generate_signing_keypair();
1070 let sig = sign(b"original", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1071 assert!(!verify(b"tampered", SIGN_CONTEXT_V1, &sig, &kp.public_key).unwrap());
1072 }
1073
1074 #[test]
1075 fn context_separation() {
1076 let kp = generate_signing_keypair();
1077 let sig = sign(b"msg", "metamorphic/sign/v1", &kp.secret_key).unwrap();
1078 assert!(!verify(b"msg", "metamorphic/other/v1", &sig, &kp.public_key).unwrap());
1080 }
1081
1082 #[test]
1083 fn empty_message_and_context() {
1084 let kp = generate_signing_keypair();
1085 let sig = sign(b"", "", &kp.secret_key).unwrap();
1086 assert!(verify(b"", "", &sig, &kp.public_key).unwrap());
1087 }
1088
1089 #[test]
1090 fn nondeterministic_but_both_valid() {
1091 let kp = generate_signing_keypair();
1092 let s1 = sign(b"msg", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1093 let s2 = sign(b"msg", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1094 assert_ne!(s1, s2);
1096 assert!(verify(b"msg", SIGN_CONTEXT_V1, &s1, &kp.public_key).unwrap());
1097 assert!(verify(b"msg", SIGN_CONTEXT_V1, &s2, &kp.public_key).unwrap());
1098 }
1099
1100 #[test]
1103 fn strict_and_requires_both() {
1104 let kp = generate_signing_keypair();
1105 let good = b64::decode(&sign(b"msg", SIGN_CONTEXT_V1, &kp.secret_key).unwrap()).unwrap();
1106
1107 let mut bad_ed = good.clone();
1109 bad_ed[1] ^= 0xFF;
1110 assert!(
1111 !verify(
1112 b"msg",
1113 SIGN_CONTEXT_V1,
1114 &b64::encode(&bad_ed),
1115 &kp.public_key
1116 )
1117 .unwrap()
1118 );
1119
1120 let mut bad_ml = good.clone();
1122 let i = 1 + ED25519_SIG_LEN + 10;
1123 bad_ml[i] ^= 0xFF;
1124 assert!(
1125 !verify(
1126 b"msg",
1127 SIGN_CONTEXT_V1,
1128 &b64::encode(&bad_ml),
1129 &kp.public_key
1130 )
1131 .unwrap()
1132 );
1133 }
1134
1135 #[test]
1136 fn cross_level_fails() {
1137 let kp3 = generate_signing_keypair_with_level(SignatureLevel::Cat3);
1138 let kp5 = generate_signing_keypair_with_level(SignatureLevel::Cat5);
1139 let sig3 = sign(b"msg", SIGN_CONTEXT_V1, &kp3.secret_key).unwrap();
1140 assert!(!verify(b"msg", SIGN_CONTEXT_V1, &sig3, &kp5.public_key).unwrap());
1142 }
1143
1144 #[test]
1145 fn unknown_tag_errors() {
1146 let bad = b64::encode(&[0x09u8; SECRET_KEY_LEN]);
1147 assert!(sign(b"x", SIGN_CONTEXT_V1, &bad).is_err());
1148 let pk = b64::encode(&[0x09u8; 100]);
1149 let sig = b64::encode(&[0x09u8; 100]);
1150 assert!(verify(b"x", SIGN_CONTEXT_V1, &sig, &pk).is_err());
1151 }
1152
1153 #[test]
1154 fn bad_base64_errors() {
1155 assert!(sign(b"x", SIGN_CONTEXT_V1, "not!base64!").is_err());
1156 assert!(verify(b"x", SIGN_CONTEXT_V1, "not!base64!", "also!bad!").is_err());
1157 }
1158
1159 #[test]
1160 fn frame_matches_manual() {
1161 let ctx = "metamorphic/sign/v1";
1162 let msg = b"payload";
1163 let mut expected = Vec::new();
1164 expected.extend_from_slice(&(ctx.len() as u64).to_be_bytes());
1165 expected.extend_from_slice(ctx.as_bytes());
1166 expected.extend_from_slice(msg);
1167 assert_eq!(frame(ctx, msg), expected);
1168 }
1169
1170 #[test]
1171 fn frame_no_boundary_confusion() {
1172 assert_ne!(frame("ab", b"c"), frame("a", b"bc"));
1173 }
1174
1175 #[test]
1176 fn secret_key_debug_is_redacted() {
1177 let kp = generate_signing_keypair();
1178 let shown = format!("{kp:?}");
1179 assert!(shown.contains("<redacted>"));
1180 assert!(!shown.contains(&kp.secret_key));
1181 }
1182
1183 #[test]
1184 fn keygen_public_key_matches_derived() {
1185 for level in [
1186 SignatureLevel::Cat2,
1187 SignatureLevel::Cat3,
1188 SignatureLevel::Cat5,
1189 ] {
1190 let kp = generate_signing_keypair_with_level(level);
1191 assert_eq!(derive_public_key(&kp.secret_key).unwrap(), kp.public_key);
1192 }
1193 }
1194
1195 use proptest::prelude::*;
1196
1197 proptest! {
1198 #[test]
1199 fn roundtrip_arbitrary_message(msg: Vec<u8>, ctx in "[a-z/]{0,32}") {
1200 let kp = generate_signing_keypair();
1201 let sig = sign(&msg, &ctx, &kp.secret_key).unwrap();
1202 prop_assert!(verify(&msg, &ctx, &sig, &kp.public_key).unwrap());
1203 }
1204 }
1205
1206 fn suite_roundtrip(suite: Suite, level: SignatureLevel, tag: u8) {
1209 let kp = generate_signing_keypair_suite(suite, level).unwrap();
1210 assert_eq!(b64::decode(&kp.public_key).unwrap()[0], tag);
1211 assert_eq!(b64::decode(&kp.secret_key).unwrap()[0], tag);
1212 let sig = sign(
1213 b"transparency-log checkpoint",
1214 SIGN_CONTEXT_V1,
1215 &kp.secret_key,
1216 )
1217 .unwrap();
1218 assert_eq!(b64::decode(&sig).unwrap()[0], tag);
1219 assert!(
1220 verify(
1221 b"transparency-log checkpoint",
1222 SIGN_CONTEXT_V1,
1223 &sig,
1224 &kp.public_key
1225 )
1226 .unwrap()
1227 );
1228 assert!(!verify(b"tampered", SIGN_CONTEXT_V1, &sig, &kp.public_key).unwrap());
1230 assert_eq!(derive_public_key(&kp.secret_key).unwrap(), kp.public_key);
1232 }
1233
1234 #[test]
1235 fn pure_cnsa2_sign_roundtrip() {
1236 suite_roundtrip(
1237 Suite::PureCnsa2,
1238 SignatureLevel::Cat5,
1239 VERSION_SIG_PURE_CNSA2,
1240 );
1241 }
1242
1243 #[test]
1244 fn matched_cat3_sign_roundtrip() {
1245 suite_roundtrip(
1246 Suite::HybridMatched,
1247 SignatureLevel::Cat3,
1248 VERSION_SIG_MATCHED_CAT3,
1249 );
1250 }
1251
1252 #[test]
1253 fn matched_cat5_sign_roundtrip() {
1254 suite_roundtrip(
1255 Suite::HybridMatched,
1256 SignatureLevel::Cat5,
1257 VERSION_SIG_MATCHED_CAT5,
1258 );
1259 }
1260
1261 #[test]
1262 fn pure_cnsa2_sign_only_cat5() {
1263 assert!(generate_signing_keypair_suite(Suite::PureCnsa2, SignatureLevel::Cat3).is_err());
1264 assert!(generate_signing_keypair_suite(Suite::PureCnsa2, SignatureLevel::Cat2).is_err());
1265 }
1266
1267 #[test]
1268 fn matched_cat2_is_plain_hybrid() {
1269 let kp =
1271 generate_signing_keypair_suite(Suite::HybridMatched, SignatureLevel::Cat2).unwrap();
1272 assert_eq!(b64::decode(&kp.public_key).unwrap()[0], VERSION_CAT2);
1273 let sig = sign(b"x", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1274 assert!(verify(b"x", SIGN_CONTEXT_V1, &sig, &kp.public_key).unwrap());
1275 }
1276
1277 #[test]
1278 fn matched_sign_strict_and_requires_both_halves() {
1279 for (suite, level, classical_offset) in [
1281 (Suite::HybridMatched, SignatureLevel::Cat3, 1usize), (Suite::HybridMatched, SignatureLevel::Cat5, 1usize), ] {
1284 let kp = generate_signing_keypair_suite(suite, level).unwrap();
1285 let good = b64::decode(&sign(b"m", SIGN_CONTEXT_V1, &kp.secret_key).unwrap()).unwrap();
1286 let mut bad_c = good.clone();
1288 bad_c[classical_offset] ^= 0xFF;
1289 assert!(!verify(b"m", SIGN_CONTEXT_V1, &b64::encode(&bad_c), &kp.public_key).unwrap());
1290 let mut bad_ml = good.clone();
1292 let last = bad_ml.len() - 1;
1293 bad_ml[last] ^= 0xFF;
1294 assert!(!verify(b"m", SIGN_CONTEXT_V1, &b64::encode(&bad_ml), &kp.public_key).unwrap());
1295 }
1296 }
1297
1298 #[test]
1299 fn pure_cnsa2_nondeterministic_but_valid() {
1300 let kp = generate_signing_keypair_suite(Suite::PureCnsa2, SignatureLevel::Cat5).unwrap();
1301 let s1 = sign(b"m", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1302 let s2 = sign(b"m", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1303 assert_ne!(s1, s2, "hedged ML-DSA => non-reproducible");
1304 assert!(verify(b"m", SIGN_CONTEXT_V1, &s1, &kp.public_key).unwrap());
1305 assert!(verify(b"m", SIGN_CONTEXT_V1, &s2, &kp.public_key).unwrap());
1306 }
1307
1308 #[test]
1309 fn sign_suites_context_separation_and_cross_key() {
1310 for (suite, level) in [
1311 (Suite::PureCnsa2, SignatureLevel::Cat5),
1312 (Suite::HybridMatched, SignatureLevel::Cat3),
1313 (Suite::HybridMatched, SignatureLevel::Cat5),
1314 ] {
1315 let kp = generate_signing_keypair_suite(suite, level).unwrap();
1316 let kp2 = generate_signing_keypair_suite(suite, level).unwrap();
1317 let sig = sign(b"m", "metamorphic/sign/v1", &kp.secret_key).unwrap();
1318 assert!(!verify(b"m", "metamorphic/other/v1", &sig, &kp.public_key).unwrap());
1320 assert!(!verify(b"m", "metamorphic/sign/v1", &sig, &kp2.public_key).unwrap());
1322 }
1323 }
1324
1325 #[test]
1326 fn sign_cross_suite_pk_rejected() {
1327 let pure = generate_signing_keypair_suite(Suite::PureCnsa2, SignatureLevel::Cat5).unwrap();
1330 let legacy = generate_signing_keypair(); let sig_pure = sign(b"m", SIGN_CONTEXT_V1, &pure.secret_key).unwrap();
1332 assert!(!verify(b"m", SIGN_CONTEXT_V1, &sig_pure, &legacy.public_key).unwrap());
1333 let sig_legacy = sign(b"m", SIGN_CONTEXT_V1, &legacy.secret_key).unwrap();
1334 assert!(!verify(b"m", SIGN_CONTEXT_V1, &sig_legacy, &pure.public_key).unwrap());
1335 }
1336}