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 posture_from_tag(tag: u8) -> Option<(Suite, SignatureLevel)> {
360 match tag {
361 VERSION_CAT2 => Some((Suite::Hybrid, SignatureLevel::Cat2)),
362 VERSION_CAT3 => Some((Suite::Hybrid, SignatureLevel::Cat3)),
363 VERSION_CAT5 => Some((Suite::Hybrid, SignatureLevel::Cat5)),
364 VERSION_SIG_PURE_CNSA2 => Some((Suite::PureCnsa2, SignatureLevel::Cat5)),
365 VERSION_SIG_MATCHED_CAT3 => Some((Suite::HybridMatched, SignatureLevel::Cat3)),
366 VERSION_SIG_MATCHED_CAT5 => Some((Suite::HybridMatched, SignatureLevel::Cat5)),
367 _ => None,
368 }
369}
370
371fn expected_public_key_len(tag: u8) -> Option<usize> {
373 match tag {
374 VERSION_CAT2 => Some(1 + ED25519_PK_LEN + MLDSA44_PK_LEN),
375 VERSION_CAT3 => Some(1 + ED25519_PK_LEN + MLDSA65_PK_LEN),
376 VERSION_CAT5 => Some(1 + ED25519_PK_LEN + MLDSA87_PK_LEN),
377 VERSION_SIG_PURE_CNSA2 => Some(1 + MLDSA87_PK_LEN),
378 VERSION_SIG_MATCHED_CAT3 => Some(1 + ED448_PK_LEN + MLDSA65_PK_LEN),
379 VERSION_SIG_MATCHED_CAT5 => Some(1 + P521_PK_LEN + MLDSA87_PK_LEN),
380 _ => None,
381 }
382}
383
384fn expected_signature_len(tag: u8) -> Option<usize> {
386 match tag {
387 VERSION_CAT2 => Some(1 + ED25519_SIG_LEN + MLDSA44_SIG_LEN),
388 VERSION_CAT3 => Some(1 + ED25519_SIG_LEN + MLDSA65_SIG_LEN),
389 VERSION_CAT5 => Some(1 + ED25519_SIG_LEN + MLDSA87_SIG_LEN),
390 VERSION_SIG_PURE_CNSA2 => Some(1 + MLDSA87_SIG_LEN),
391 VERSION_SIG_MATCHED_CAT3 => Some(1 + ED448_SIG_LEN + MLDSA65_SIG_LEN),
392 VERSION_SIG_MATCHED_CAT5 => Some(1 + P521_SIG_LEN + MLDSA87_SIG_LEN),
393 _ => None,
394 }
395}
396
397fn mldsa_public_key<P: MlDsaParams>(seed: &B32) -> Vec<u8> {
399 let vk = SigningKey::<P>::from_seed(seed).verifying_key().encode();
400 AsRef::<[u8]>::as_ref(&vk).to_vec()
401}
402
403fn mldsa_sign<P: MlDsaParams>(seed: &B32, framed: &[u8]) -> Vec<u8> {
405 let sig = ExpandedSigningKey::<P>::from_seed(seed)
406 .sign_randomized(framed, &[], &mut OsCsprng)
407 .expect("ML-DSA randomized signing (empty context, infallible RNG)")
408 .encode();
409 AsRef::<[u8]>::as_ref(&sig).to_vec()
410}
411
412fn mldsa_verify<P: MlDsaParams>(pk: &[u8], framed: &[u8], sig: &[u8]) -> bool {
414 match (
415 VerifyingKey::<P>::new_from_slice(pk),
416 Signature::<P>::try_from(sig),
417 ) {
418 (Ok(vk), Ok(s)) => MlVerifier::verify(&vk, framed, &s).is_ok(),
419 _ => false,
420 }
421}
422
423fn p521_signing_key_from_bytes(bytes: &[u8; P521_SK_LEN]) -> P521SigningKey {
427 let fb: FieldBytes<NistP521> = (*bytes).into();
428 let scalar = <Scalar as Reduce<FieldBytes<NistP521>>>::reduce(&fb);
429 let nz: NonZeroScalar =
430 Option::from(NonZeroScalar::new(scalar)).expect("P-521 scalar reduced to zero");
431 P521SigningKey::from(P521SecretKey::from(nz))
432}
433
434fn ed448_keypair(
436 seed: &[u8; ED448_SEED_LEN],
437) -> Result<([u8; ED448_PK_LEN], Ed448SigningKey), CryptoError> {
438 let sk = Ed448SigningKey::try_from(&seed[..])
439 .map_err(|_| CryptoError::Signature("invalid Ed448 seed".into()))?;
440 let pk = sk.verifying_key().to_bytes();
441 Ok((pk, sk))
442}
443
444pub fn generate_signing_keypair_suite(
457 suite: Suite,
458 level: SignatureLevel,
459) -> Result<HybridSignatureKeyPair, CryptoError> {
460 match (suite, level) {
461 (Suite::Hybrid, _) | (Suite::HybridMatched, SignatureLevel::Cat2) => {
462 Ok(generate_signing_keypair_with_level(level))
463 }
464 (Suite::HybridMatched, SignatureLevel::Cat3) => Ok(generate_matched_cat3_keypair()),
465 (Suite::HybridMatched, SignatureLevel::Cat5) => Ok(generate_matched_cat5_keypair()),
466 (Suite::PureCnsa2, SignatureLevel::Cat5) => Ok(generate_pure_cnsa2_keypair()),
467 (Suite::PureCnsa2, _) => Err(CryptoError::Signature(
468 "PureCnsa2 signatures are Cat-5 (ML-DSA-87) only in v0.7.0".into(),
469 )),
470 }
471}
472
473fn generate_pure_cnsa2_keypair() -> HybridSignatureKeyPair {
474 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
475 random_bytes(&mut ml_seed_bytes);
476 let ml_seed: B32 = ml_seed_bytes.into();
477 let ml_pk = mldsa_public_key::<MlDsa87>(&ml_seed);
478
479 let mut public_key = Vec::with_capacity(1 + ml_pk.len());
480 public_key.push(VERSION_SIG_PURE_CNSA2);
481 public_key.extend_from_slice(&ml_pk);
482
483 let mut secret_key = Vec::with_capacity(1 + MLDSA_SEED_LEN);
484 secret_key.push(VERSION_SIG_PURE_CNSA2);
485 secret_key.extend_from_slice(&ml_seed_bytes);
486
487 let pair = HybridSignatureKeyPair {
488 public_key: b64::encode(&public_key),
489 secret_key: b64::encode(&secret_key),
490 };
491 ml_seed_bytes.zeroize();
492 secret_key.zeroize();
493 pair
494}
495
496fn generate_matched_cat3_keypair() -> HybridSignatureKeyPair {
497 let mut ed_seed = [0u8; ED448_SEED_LEN];
498 random_bytes(&mut ed_seed);
499 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
500 random_bytes(&mut ml_seed_bytes);
501
502 let (ed_pk, _) = ed448_keypair(&ed_seed).expect("freshly generated Ed448 seed");
503 let ml_seed: B32 = ml_seed_bytes.into();
504 let ml_pk = mldsa_public_key::<MlDsa65>(&ml_seed);
505
506 let mut public_key = Vec::with_capacity(1 + ED448_PK_LEN + ml_pk.len());
507 public_key.push(VERSION_SIG_MATCHED_CAT3);
508 public_key.extend_from_slice(&ed_pk);
509 public_key.extend_from_slice(&ml_pk);
510
511 let mut secret_key = Vec::with_capacity(1 + ED448_SEED_LEN + MLDSA_SEED_LEN);
512 secret_key.push(VERSION_SIG_MATCHED_CAT3);
513 secret_key.extend_from_slice(&ed_seed);
514 secret_key.extend_from_slice(&ml_seed_bytes);
515
516 let pair = HybridSignatureKeyPair {
517 public_key: b64::encode(&public_key),
518 secret_key: b64::encode(&secret_key),
519 };
520 ed_seed.zeroize();
521 ml_seed_bytes.zeroize();
522 secret_key.zeroize();
523 pair
524}
525
526fn generate_matched_cat5_keypair() -> HybridSignatureKeyPair {
527 let mut ec_seed = [0u8; P521_SK_LEN];
528 random_bytes(&mut ec_seed);
529 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
530 random_bytes(&mut ml_seed_bytes);
531
532 let signing = p521_signing_key_from_bytes(&ec_seed);
533 let ec_pk: [u8; P521_PK_LEN] = signing
534 .verifying_key()
535 .to_sec1_point(false)
536 .as_bytes()
537 .try_into()
538 .expect("uncompressed P-521 public key is 133 bytes");
539 let ml_seed: B32 = ml_seed_bytes.into();
540 let ml_pk = mldsa_public_key::<MlDsa87>(&ml_seed);
541
542 let mut public_key = Vec::with_capacity(1 + P521_PK_LEN + ml_pk.len());
543 public_key.push(VERSION_SIG_MATCHED_CAT5);
544 public_key.extend_from_slice(&ec_pk);
545 public_key.extend_from_slice(&ml_pk);
546
547 let mut secret_key = Vec::with_capacity(1 + P521_SK_LEN + MLDSA_SEED_LEN);
548 secret_key.push(VERSION_SIG_MATCHED_CAT5);
549 secret_key.extend_from_slice(&ec_seed);
550 secret_key.extend_from_slice(&ml_seed_bytes);
551
552 let pair = HybridSignatureKeyPair {
553 public_key: b64::encode(&public_key),
554 secret_key: b64::encode(&secret_key),
555 };
556 ec_seed.zeroize();
557 ml_seed_bytes.zeroize();
558 secret_key.zeroize();
559 pair
560}
561
562pub fn generate_signing_keypair() -> HybridSignatureKeyPair {
566 generate_signing_keypair_with_level(SignatureLevel::Cat3)
567}
568
569pub fn generate_signing_keypair_44() -> HybridSignatureKeyPair {
571 generate_signing_keypair_with_level(SignatureLevel::Cat2)
572}
573
574pub fn generate_signing_keypair_87() -> HybridSignatureKeyPair {
576 generate_signing_keypair_with_level(SignatureLevel::Cat5)
577}
578
579pub fn generate_signing_keypair_with_level(level: SignatureLevel) -> HybridSignatureKeyPair {
584 let mut ed_seed = [0u8; ED25519_SEED_LEN];
585 random_bytes(&mut ed_seed);
586 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
587 random_bytes(&mut ml_seed_bytes);
588
589 let ed_sk = EdSigningKey::from_bytes(&ed_seed);
590 let ed_pk = ed_sk.verifying_key().to_bytes();
591
592 let ml_seed: B32 = ml_seed_bytes.into();
593 let ml_pk = match level {
594 SignatureLevel::Cat2 => mldsa_public_key::<MlDsa44>(&ml_seed),
595 SignatureLevel::Cat3 => mldsa_public_key::<MlDsa65>(&ml_seed),
596 SignatureLevel::Cat5 => mldsa_public_key::<MlDsa87>(&ml_seed),
597 };
598
599 let tag = level.version_tag();
600
601 let mut public_key = Vec::with_capacity(1 + ED25519_PK_LEN + ml_pk.len());
602 public_key.push(tag);
603 public_key.extend_from_slice(&ed_pk);
604 public_key.extend_from_slice(&ml_pk);
605
606 let mut secret_key = Vec::with_capacity(SECRET_KEY_LEN);
607 secret_key.push(tag);
608 secret_key.extend_from_slice(&ed_seed);
609 secret_key.extend_from_slice(&ml_seed_bytes);
610
611 let pair = HybridSignatureKeyPair {
612 public_key: b64::encode(&public_key),
613 secret_key: b64::encode(&secret_key),
614 };
615
616 ed_seed.zeroize();
617 ml_seed_bytes.zeroize();
618 secret_key.zeroize();
619 pair
620}
621
622pub fn derive_public_key(secret_key_b64: &str) -> Result<String, CryptoError> {
632 let mut sk_bytes = b64::decode(secret_key_b64)?;
633 if let Some(&tag) = sk_bytes.first() {
634 if is_new_suite_tag(tag) {
635 let out = derive_public_key_new_suite(&sk_bytes);
636 sk_bytes.zeroize();
637 return out;
638 }
639 }
640 let level = level_from_tag(sk_bytes.first())?;
641
642 if sk_bytes.len() != SECRET_KEY_LEN {
643 sk_bytes.zeroize();
644 return Err(CryptoError::InvalidLength {
645 expected: SECRET_KEY_LEN,
646 got: sk_bytes.len(),
647 });
648 }
649
650 let mut ed_seed = [0u8; ED25519_SEED_LEN];
651 ed_seed.copy_from_slice(&sk_bytes[1..1 + ED25519_SEED_LEN]);
652 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
653 ml_seed_bytes.copy_from_slice(&sk_bytes[1 + ED25519_SEED_LEN..SECRET_KEY_LEN]);
654 sk_bytes.zeroize();
655
656 let ed_pk = EdSigningKey::from_bytes(&ed_seed)
657 .verifying_key()
658 .to_bytes();
659 ed_seed.zeroize();
660
661 let ml_seed: B32 = ml_seed_bytes.into();
662 let ml_pk = match level {
663 SignatureLevel::Cat2 => mldsa_public_key::<MlDsa44>(&ml_seed),
664 SignatureLevel::Cat3 => mldsa_public_key::<MlDsa65>(&ml_seed),
665 SignatureLevel::Cat5 => mldsa_public_key::<MlDsa87>(&ml_seed),
666 };
667 ml_seed_bytes.zeroize();
668
669 let mut public_key = Vec::with_capacity(1 + ED25519_PK_LEN + ml_pk.len());
670 public_key.push(level.version_tag());
671 public_key.extend_from_slice(&ed_pk);
672 public_key.extend_from_slice(&ml_pk);
673
674 Ok(b64::encode(&public_key))
675}
676pub fn sign(message: &[u8], context: &str, secret_key_b64: &str) -> Result<String, CryptoError> {
687 let mut sk_bytes = b64::decode(secret_key_b64)?;
688 if let Some(&tag) = sk_bytes.first() {
689 if is_new_suite_tag(tag) {
690 let out = sign_new_suite(message, context, &sk_bytes);
691 sk_bytes.zeroize();
692 return out;
693 }
694 }
695 let level = level_from_tag(sk_bytes.first())?;
696
697 if sk_bytes.len() != SECRET_KEY_LEN {
698 sk_bytes.zeroize();
699 return Err(CryptoError::InvalidLength {
700 expected: SECRET_KEY_LEN,
701 got: sk_bytes.len(),
702 });
703 }
704
705 let mut ed_seed = [0u8; ED25519_SEED_LEN];
706 ed_seed.copy_from_slice(&sk_bytes[1..1 + ED25519_SEED_LEN]);
707 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
708 ml_seed_bytes.copy_from_slice(&sk_bytes[1 + ED25519_SEED_LEN..SECRET_KEY_LEN]);
709 sk_bytes.zeroize();
710
711 let framed = frame(context, message);
712
713 let ed_sk = EdSigningKey::from_bytes(&ed_seed);
714 let ed_sig = ed_sk.sign(&framed).to_bytes();
715 ed_seed.zeroize();
716
717 let ml_seed: B32 = ml_seed_bytes.into();
718 let ml_sig = match level {
719 SignatureLevel::Cat2 => mldsa_sign::<MlDsa44>(&ml_seed, &framed),
720 SignatureLevel::Cat3 => mldsa_sign::<MlDsa65>(&ml_seed, &framed),
721 SignatureLevel::Cat5 => mldsa_sign::<MlDsa87>(&ml_seed, &framed),
722 };
723 ml_seed_bytes.zeroize();
724
725 let mut out = Vec::with_capacity(1 + ED25519_SIG_LEN + ml_sig.len());
726 out.push(level.version_tag());
727 out.extend_from_slice(&ed_sig);
728 out.extend_from_slice(&ml_sig);
729
730 Ok(b64::encode(&out))
731}
732
733pub fn verify(
742 message: &[u8],
743 context: &str,
744 signature_b64: &str,
745 public_key_b64: &str,
746) -> Result<bool, CryptoError> {
747 let sig = b64::decode(signature_b64)?;
748 let pk = b64::decode(public_key_b64)?;
749
750 match (sig.first(), pk.first()) {
752 (Some(&s), _) if is_new_suite_tag(s) => {
753 return verify_new_suite(message, context, &sig, &pk);
754 }
755 (_, Some(&p)) if is_new_suite_tag(p) => return Ok(false),
756 _ => {}
757 }
758
759 let sig_level = level_from_tag(sig.first())?;
760 let pk_level = level_from_tag(pk.first())?;
761 if sig_level != pk_level {
763 return Ok(false);
764 }
765 let level = sig_level;
766
767 if sig.len() != 1 + ED25519_SIG_LEN + level.mldsa_sig_len()
768 || pk.len() != 1 + ED25519_PK_LEN + level.mldsa_pk_len()
769 {
770 return Ok(false);
771 }
772
773 let framed = frame(context, message);
774
775 let ed_pk_bytes: [u8; ED25519_PK_LEN] = pk[1..1 + ED25519_PK_LEN].try_into().unwrap();
776 let ed_sig_bytes: [u8; ED25519_SIG_LEN] = sig[1..1 + ED25519_SIG_LEN].try_into().unwrap();
777 let ml_pk = &pk[1 + ED25519_PK_LEN..];
778 let ml_sig = &sig[1 + ED25519_SIG_LEN..];
779
780 let ed_ok = match EdVerifyingKey::from_bytes(&ed_pk_bytes) {
781 Ok(vk) => vk
782 .verify(&framed, &EdSignature::from_bytes(&ed_sig_bytes))
783 .is_ok(),
784 Err(_) => false,
785 };
786
787 let ml_ok = match level {
788 SignatureLevel::Cat2 => mldsa_verify::<MlDsa44>(ml_pk, &framed, ml_sig),
789 SignatureLevel::Cat3 => mldsa_verify::<MlDsa65>(ml_pk, &framed, ml_sig),
790 SignatureLevel::Cat5 => mldsa_verify::<MlDsa87>(ml_pk, &framed, ml_sig),
791 };
792
793 Ok(ed_ok && ml_ok)
795}
796
797pub fn signature_posture(public_key_b64: &str) -> Result<(Suite, SignatureLevel), CryptoError> {
846 let pk = b64::decode(public_key_b64)?;
847 let tag = pk
848 .first()
849 .copied()
850 .ok_or_else(|| CryptoError::Signature("unknown or missing signature version tag".into()))?;
851 let posture = posture_from_tag(tag)
852 .ok_or_else(|| CryptoError::Signature("unknown or missing signature version tag".into()))?;
853 let expected =
854 expected_public_key_len(tag).expect("known tag always has an expected public-key length");
855 if pk.len() != expected {
856 return Err(CryptoError::InvalidLength {
857 expected,
858 got: pk.len(),
859 });
860 }
861 Ok(posture)
862}
863
864pub fn signature_posture_from_signature(
891 signature_b64: &str,
892) -> Result<(Suite, SignatureLevel), CryptoError> {
893 let sig = b64::decode(signature_b64)?;
894 let tag = sig
895 .first()
896 .copied()
897 .ok_or_else(|| CryptoError::Signature("unknown or missing signature version tag".into()))?;
898 let posture = posture_from_tag(tag)
899 .ok_or_else(|| CryptoError::Signature("unknown or missing signature version tag".into()))?;
900 let expected =
901 expected_signature_len(tag).expect("known tag always has an expected signature length");
902 if sig.len() != expected {
903 return Err(CryptoError::InvalidLength {
904 expected,
905 got: sig.len(),
906 });
907 }
908 Ok(posture)
909}
910
911fn is_new_suite_tag(tag: u8) -> bool {
915 matches!(
916 tag,
917 VERSION_SIG_PURE_CNSA2 | VERSION_SIG_MATCHED_CAT3 | VERSION_SIG_MATCHED_CAT5
918 )
919}
920
921fn sign_new_suite(message: &[u8], context: &str, sk: &[u8]) -> Result<String, CryptoError> {
922 let framed = frame(context, message);
923 match sk[0] {
924 VERSION_SIG_PURE_CNSA2 => {
925 if sk.len() != 1 + MLDSA_SEED_LEN {
926 return Err(CryptoError::InvalidLength {
927 expected: 1 + MLDSA_SEED_LEN,
928 got: sk.len(),
929 });
930 }
931 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
932 ml_seed_bytes.copy_from_slice(&sk[1..]);
933 let ml_seed: B32 = ml_seed_bytes.into();
934 let ml_sig = mldsa_sign::<MlDsa87>(&ml_seed, &framed);
935 ml_seed_bytes.zeroize();
936 let mut out = Vec::with_capacity(1 + ml_sig.len());
937 out.push(VERSION_SIG_PURE_CNSA2);
938 out.extend_from_slice(&ml_sig);
939 Ok(b64::encode(&out))
940 }
941 VERSION_SIG_MATCHED_CAT3 => {
942 if sk.len() != 1 + ED448_SEED_LEN + MLDSA_SEED_LEN {
943 return Err(CryptoError::InvalidLength {
944 expected: 1 + ED448_SEED_LEN + MLDSA_SEED_LEN,
945 got: sk.len(),
946 });
947 }
948 let mut ed_seed = [0u8; ED448_SEED_LEN];
949 ed_seed.copy_from_slice(&sk[1..1 + ED448_SEED_LEN]);
950 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
951 ml_seed_bytes.copy_from_slice(&sk[1 + ED448_SEED_LEN..]);
952
953 let (_, ed_sk) = ed448_keypair(&ed_seed)?;
954 let ed_sig = ed_sk.sign_raw(&framed).to_bytes();
955 ed_seed.zeroize();
956 let ml_seed: B32 = ml_seed_bytes.into();
957 let ml_sig = mldsa_sign::<MlDsa65>(&ml_seed, &framed);
958 ml_seed_bytes.zeroize();
959
960 let mut out = Vec::with_capacity(1 + ED448_SIG_LEN + ml_sig.len());
961 out.push(VERSION_SIG_MATCHED_CAT3);
962 out.extend_from_slice(&ed_sig);
963 out.extend_from_slice(&ml_sig);
964 Ok(b64::encode(&out))
965 }
966 VERSION_SIG_MATCHED_CAT5 => {
967 if sk.len() != 1 + P521_SK_LEN + MLDSA_SEED_LEN {
968 return Err(CryptoError::InvalidLength {
969 expected: 1 + P521_SK_LEN + MLDSA_SEED_LEN,
970 got: sk.len(),
971 });
972 }
973 let mut ec_seed = [0u8; P521_SK_LEN];
974 ec_seed.copy_from_slice(&sk[1..1 + P521_SK_LEN]);
975 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
976 ml_seed_bytes.copy_from_slice(&sk[1 + P521_SK_LEN..]);
977
978 let signing = p521_signing_key_from_bytes(&ec_seed);
980 let ec_sig: P521Signature = signing
981 .try_sign_with_rng(&mut OsCsprng, &framed)
982 .map_err(|_| CryptoError::Signature("ECDSA-P-521 signing failed".into()))?;
983 let ec_sig_bytes = ec_sig.to_bytes();
984 ec_seed.zeroize();
985 let ml_seed: B32 = ml_seed_bytes.into();
986 let ml_sig = mldsa_sign::<MlDsa87>(&ml_seed, &framed);
987 ml_seed_bytes.zeroize();
988
989 let mut out = Vec::with_capacity(1 + P521_SIG_LEN + ml_sig.len());
990 out.push(VERSION_SIG_MATCHED_CAT5);
991 out.extend_from_slice(ec_sig_bytes.as_slice());
992 out.extend_from_slice(&ml_sig);
993 Ok(b64::encode(&out))
994 }
995 _ => Err(CryptoError::Signature("not a CNSA-2.0 suite tag".into())),
996 }
997}
998
999fn ed448_verify(pk: &[u8], framed: &[u8], sig: &[u8]) -> bool {
1000 let Ok(pk_arr): Result<[u8; ED448_PK_LEN], _> = pk.try_into() else {
1001 return false;
1002 };
1003 let Ok(vk) = Ed448VerifyingKey::from_bytes(&pk_arr) else {
1004 return false;
1005 };
1006 let Ok(signature) = Ed448Signature::try_from(sig) else {
1007 return false;
1008 };
1009 vk.verify_raw(&signature, framed).is_ok()
1010}
1011
1012fn p521_ecdsa_verify(pk: &[u8], framed: &[u8], sig: &[u8]) -> bool {
1013 let Ok(vk) = P521VerifyingKey::from_sec1_bytes(pk) else {
1014 return false;
1015 };
1016 let Ok(signature) = P521Signature::from_slice(sig) else {
1017 return false;
1018 };
1019 vk.verify(framed, &signature).is_ok()
1020}
1021
1022fn verify_new_suite(
1023 message: &[u8],
1024 context: &str,
1025 sig: &[u8],
1026 pk: &[u8],
1027) -> Result<bool, CryptoError> {
1028 if sig[0] != pk[0] {
1031 return Ok(false);
1032 }
1033 let framed = frame(context, message);
1034 match sig[0] {
1035 VERSION_SIG_PURE_CNSA2 => {
1036 if sig.len() != 1 + MLDSA87_SIG_LEN || pk.len() != 1 + MLDSA87_PK_LEN {
1037 return Ok(false);
1038 }
1039 Ok(mldsa_verify::<MlDsa87>(&pk[1..], &framed, &sig[1..]))
1040 }
1041 VERSION_SIG_MATCHED_CAT3 => {
1042 if sig.len() != 1 + ED448_SIG_LEN + MLDSA65_SIG_LEN
1043 || pk.len() != 1 + ED448_PK_LEN + MLDSA65_PK_LEN
1044 {
1045 return Ok(false);
1046 }
1047 let ed_ok = ed448_verify(
1048 &pk[1..1 + ED448_PK_LEN],
1049 &framed,
1050 &sig[1..1 + ED448_SIG_LEN],
1051 );
1052 let ml_ok = mldsa_verify::<MlDsa65>(
1053 &pk[1 + ED448_PK_LEN..],
1054 &framed,
1055 &sig[1 + ED448_SIG_LEN..],
1056 );
1057 Ok(ed_ok && ml_ok)
1058 }
1059 VERSION_SIG_MATCHED_CAT5 => {
1060 if sig.len() != 1 + P521_SIG_LEN + MLDSA87_SIG_LEN
1061 || pk.len() != 1 + P521_PK_LEN + MLDSA87_PK_LEN
1062 {
1063 return Ok(false);
1064 }
1065 let ec_ok =
1066 p521_ecdsa_verify(&pk[1..1 + P521_PK_LEN], &framed, &sig[1..1 + P521_SIG_LEN]);
1067 let ml_ok =
1068 mldsa_verify::<MlDsa87>(&pk[1 + P521_PK_LEN..], &framed, &sig[1 + P521_SIG_LEN..]);
1069 Ok(ec_ok && ml_ok)
1070 }
1071 _ => Err(CryptoError::Signature("not a CNSA-2.0 suite tag".into())),
1072 }
1073}
1074
1075fn derive_public_key_new_suite(sk: &[u8]) -> Result<String, CryptoError> {
1076 match sk[0] {
1077 VERSION_SIG_PURE_CNSA2 => {
1078 if sk.len() != 1 + MLDSA_SEED_LEN {
1079 return Err(CryptoError::InvalidLength {
1080 expected: 1 + MLDSA_SEED_LEN,
1081 got: sk.len(),
1082 });
1083 }
1084 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
1085 ml_seed_bytes.copy_from_slice(&sk[1..]);
1086 let ml_seed: B32 = ml_seed_bytes.into();
1087 let ml_pk = mldsa_public_key::<MlDsa87>(&ml_seed);
1088 ml_seed_bytes.zeroize();
1089 let mut pk = Vec::with_capacity(1 + ml_pk.len());
1090 pk.push(VERSION_SIG_PURE_CNSA2);
1091 pk.extend_from_slice(&ml_pk);
1092 Ok(b64::encode(&pk))
1093 }
1094 VERSION_SIG_MATCHED_CAT3 => {
1095 if sk.len() != 1 + ED448_SEED_LEN + MLDSA_SEED_LEN {
1096 return Err(CryptoError::InvalidLength {
1097 expected: 1 + ED448_SEED_LEN + MLDSA_SEED_LEN,
1098 got: sk.len(),
1099 });
1100 }
1101 let mut ed_seed = [0u8; ED448_SEED_LEN];
1102 ed_seed.copy_from_slice(&sk[1..1 + ED448_SEED_LEN]);
1103 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
1104 ml_seed_bytes.copy_from_slice(&sk[1 + ED448_SEED_LEN..]);
1105 let (ed_pk, _) = ed448_keypair(&ed_seed)?;
1106 ed_seed.zeroize();
1107 let ml_seed: B32 = ml_seed_bytes.into();
1108 let ml_pk = mldsa_public_key::<MlDsa65>(&ml_seed);
1109 ml_seed_bytes.zeroize();
1110 let mut pk = Vec::with_capacity(1 + ED448_PK_LEN + ml_pk.len());
1111 pk.push(VERSION_SIG_MATCHED_CAT3);
1112 pk.extend_from_slice(&ed_pk);
1113 pk.extend_from_slice(&ml_pk);
1114 Ok(b64::encode(&pk))
1115 }
1116 VERSION_SIG_MATCHED_CAT5 => {
1117 if sk.len() != 1 + P521_SK_LEN + MLDSA_SEED_LEN {
1118 return Err(CryptoError::InvalidLength {
1119 expected: 1 + P521_SK_LEN + MLDSA_SEED_LEN,
1120 got: sk.len(),
1121 });
1122 }
1123 let mut ec_seed = [0u8; P521_SK_LEN];
1124 ec_seed.copy_from_slice(&sk[1..1 + P521_SK_LEN]);
1125 let mut ml_seed_bytes = [0u8; MLDSA_SEED_LEN];
1126 ml_seed_bytes.copy_from_slice(&sk[1 + P521_SK_LEN..]);
1127 let signing = p521_signing_key_from_bytes(&ec_seed);
1128 let ec_pk: [u8; P521_PK_LEN] = signing
1129 .verifying_key()
1130 .to_sec1_point(false)
1131 .as_bytes()
1132 .try_into()
1133 .expect("uncompressed P-521 public key is 133 bytes");
1134 ec_seed.zeroize();
1135 let ml_seed: B32 = ml_seed_bytes.into();
1136 let ml_pk = mldsa_public_key::<MlDsa87>(&ml_seed);
1137 ml_seed_bytes.zeroize();
1138 let mut pk = Vec::with_capacity(1 + P521_PK_LEN + ml_pk.len());
1139 pk.push(VERSION_SIG_MATCHED_CAT5);
1140 pk.extend_from_slice(&ec_pk);
1141 pk.extend_from_slice(&ml_pk);
1142 Ok(b64::encode(&pk))
1143 }
1144 _ => Err(CryptoError::Signature("not a CNSA-2.0 suite tag".into())),
1145 }
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150 use super::*;
1151
1152 fn roundtrip(level: SignatureLevel) {
1153 let kp = generate_signing_keypair_with_level(level);
1154 let sig = sign(b"hello transparency log", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1155 assert!(
1156 verify(
1157 b"hello transparency log",
1158 SIGN_CONTEXT_V1,
1159 &sig,
1160 &kp.public_key
1161 )
1162 .unwrap()
1163 );
1164 }
1165
1166 #[test]
1167 fn cat2_roundtrip() {
1168 roundtrip(SignatureLevel::Cat2);
1169 }
1170
1171 #[test]
1172 fn cat3_roundtrip() {
1173 roundtrip(SignatureLevel::Cat3);
1174 }
1175
1176 #[test]
1177 fn cat5_roundtrip() {
1178 roundtrip(SignatureLevel::Cat5);
1179 }
1180
1181 #[test]
1182 fn default_is_cat3() {
1183 assert_eq!(SignatureLevel::default(), SignatureLevel::Cat3);
1184 let kp = generate_signing_keypair();
1185 let raw = b64::decode(&kp.public_key).unwrap();
1186 assert_eq!(raw[0], VERSION_CAT3);
1187 }
1188
1189 #[test]
1190 fn version_tags() {
1191 for (level, tag) in [
1192 (SignatureLevel::Cat2, VERSION_CAT2),
1193 (SignatureLevel::Cat3, VERSION_CAT3),
1194 (SignatureLevel::Cat5, VERSION_CAT5),
1195 ] {
1196 let kp = generate_signing_keypair_with_level(level);
1197 let sig = sign(b"x", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1198 assert_eq!(b64::decode(&kp.public_key).unwrap()[0], tag);
1199 assert_eq!(b64::decode(&kp.secret_key).unwrap()[0], tag);
1200 assert_eq!(b64::decode(&sig).unwrap()[0], tag);
1201 }
1202 }
1203
1204 #[test]
1205 fn key_and_signature_sizes() {
1206 for (level, pk_len, sig_len) in [
1207 (SignatureLevel::Cat2, MLDSA44_PK_LEN, MLDSA44_SIG_LEN),
1208 (SignatureLevel::Cat3, MLDSA65_PK_LEN, MLDSA65_SIG_LEN),
1209 (SignatureLevel::Cat5, MLDSA87_PK_LEN, MLDSA87_SIG_LEN),
1210 ] {
1211 let kp = generate_signing_keypair_with_level(level);
1212 let pk = b64::decode(&kp.public_key).unwrap();
1213 let sk = b64::decode(&kp.secret_key).unwrap();
1214 let sig = b64::decode(&sign(b"x", SIGN_CONTEXT_V1, &kp.secret_key).unwrap()).unwrap();
1215 assert_eq!(pk.len(), 1 + ED25519_PK_LEN + pk_len);
1216 assert_eq!(sk.len(), SECRET_KEY_LEN);
1217 assert_eq!(sig.len(), 1 + ED25519_SIG_LEN + sig_len);
1218 }
1219 }
1220
1221 #[test]
1222 fn wrong_key_fails() {
1223 let kp1 = generate_signing_keypair();
1224 let kp2 = generate_signing_keypair();
1225 let sig = sign(b"msg", SIGN_CONTEXT_V1, &kp1.secret_key).unwrap();
1226 assert!(!verify(b"msg", SIGN_CONTEXT_V1, &sig, &kp2.public_key).unwrap());
1227 }
1228
1229 #[test]
1230 fn tampered_message_fails() {
1231 let kp = generate_signing_keypair();
1232 let sig = sign(b"original", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1233 assert!(!verify(b"tampered", SIGN_CONTEXT_V1, &sig, &kp.public_key).unwrap());
1234 }
1235
1236 #[test]
1237 fn context_separation() {
1238 let kp = generate_signing_keypair();
1239 let sig = sign(b"msg", "metamorphic/sign/v1", &kp.secret_key).unwrap();
1240 assert!(!verify(b"msg", "metamorphic/other/v1", &sig, &kp.public_key).unwrap());
1242 }
1243
1244 #[test]
1245 fn empty_message_and_context() {
1246 let kp = generate_signing_keypair();
1247 let sig = sign(b"", "", &kp.secret_key).unwrap();
1248 assert!(verify(b"", "", &sig, &kp.public_key).unwrap());
1249 }
1250
1251 #[test]
1252 fn nondeterministic_but_both_valid() {
1253 let kp = generate_signing_keypair();
1254 let s1 = sign(b"msg", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1255 let s2 = sign(b"msg", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1256 assert_ne!(s1, s2);
1258 assert!(verify(b"msg", SIGN_CONTEXT_V1, &s1, &kp.public_key).unwrap());
1259 assert!(verify(b"msg", SIGN_CONTEXT_V1, &s2, &kp.public_key).unwrap());
1260 }
1261
1262 #[test]
1265 fn strict_and_requires_both() {
1266 let kp = generate_signing_keypair();
1267 let good = b64::decode(&sign(b"msg", SIGN_CONTEXT_V1, &kp.secret_key).unwrap()).unwrap();
1268
1269 let mut bad_ed = good.clone();
1271 bad_ed[1] ^= 0xFF;
1272 assert!(
1273 !verify(
1274 b"msg",
1275 SIGN_CONTEXT_V1,
1276 &b64::encode(&bad_ed),
1277 &kp.public_key
1278 )
1279 .unwrap()
1280 );
1281
1282 let mut bad_ml = good.clone();
1284 let i = 1 + ED25519_SIG_LEN + 10;
1285 bad_ml[i] ^= 0xFF;
1286 assert!(
1287 !verify(
1288 b"msg",
1289 SIGN_CONTEXT_V1,
1290 &b64::encode(&bad_ml),
1291 &kp.public_key
1292 )
1293 .unwrap()
1294 );
1295 }
1296
1297 #[test]
1298 fn cross_level_fails() {
1299 let kp3 = generate_signing_keypair_with_level(SignatureLevel::Cat3);
1300 let kp5 = generate_signing_keypair_with_level(SignatureLevel::Cat5);
1301 let sig3 = sign(b"msg", SIGN_CONTEXT_V1, &kp3.secret_key).unwrap();
1302 assert!(!verify(b"msg", SIGN_CONTEXT_V1, &sig3, &kp5.public_key).unwrap());
1304 }
1305
1306 #[test]
1307 fn unknown_tag_errors() {
1308 let bad = b64::encode(&[0x09u8; SECRET_KEY_LEN]);
1309 assert!(sign(b"x", SIGN_CONTEXT_V1, &bad).is_err());
1310 let pk = b64::encode(&[0x09u8; 100]);
1311 let sig = b64::encode(&[0x09u8; 100]);
1312 assert!(verify(b"x", SIGN_CONTEXT_V1, &sig, &pk).is_err());
1313 }
1314
1315 #[test]
1316 fn bad_base64_errors() {
1317 assert!(sign(b"x", SIGN_CONTEXT_V1, "not!base64!").is_err());
1318 assert!(verify(b"x", SIGN_CONTEXT_V1, "not!base64!", "also!bad!").is_err());
1319 }
1320
1321 #[test]
1322 fn frame_matches_manual() {
1323 let ctx = "metamorphic/sign/v1";
1324 let msg = b"payload";
1325 let mut expected = Vec::new();
1326 expected.extend_from_slice(&(ctx.len() as u64).to_be_bytes());
1327 expected.extend_from_slice(ctx.as_bytes());
1328 expected.extend_from_slice(msg);
1329 assert_eq!(frame(ctx, msg), expected);
1330 }
1331
1332 #[test]
1333 fn frame_no_boundary_confusion() {
1334 assert_ne!(frame("ab", b"c"), frame("a", b"bc"));
1335 }
1336
1337 #[test]
1338 fn secret_key_debug_is_redacted() {
1339 let kp = generate_signing_keypair();
1340 let shown = format!("{kp:?}");
1341 assert!(shown.contains("<redacted>"));
1342 assert!(!shown.contains(&kp.secret_key));
1343 }
1344
1345 #[test]
1346 fn keygen_public_key_matches_derived() {
1347 for level in [
1348 SignatureLevel::Cat2,
1349 SignatureLevel::Cat3,
1350 SignatureLevel::Cat5,
1351 ] {
1352 let kp = generate_signing_keypair_with_level(level);
1353 assert_eq!(derive_public_key(&kp.secret_key).unwrap(), kp.public_key);
1354 }
1355 }
1356
1357 use proptest::prelude::*;
1358
1359 proptest! {
1360 #[test]
1361 fn roundtrip_arbitrary_message(msg: Vec<u8>, ctx in "[a-z/]{0,32}") {
1362 let kp = generate_signing_keypair();
1363 let sig = sign(&msg, &ctx, &kp.secret_key).unwrap();
1364 prop_assert!(verify(&msg, &ctx, &sig, &kp.public_key).unwrap());
1365 }
1366 }
1367
1368 fn suite_roundtrip(suite: Suite, level: SignatureLevel, tag: u8) {
1371 let kp = generate_signing_keypair_suite(suite, level).unwrap();
1372 assert_eq!(b64::decode(&kp.public_key).unwrap()[0], tag);
1373 assert_eq!(b64::decode(&kp.secret_key).unwrap()[0], tag);
1374 let sig = sign(
1375 b"transparency-log checkpoint",
1376 SIGN_CONTEXT_V1,
1377 &kp.secret_key,
1378 )
1379 .unwrap();
1380 assert_eq!(b64::decode(&sig).unwrap()[0], tag);
1381 assert!(
1382 verify(
1383 b"transparency-log checkpoint",
1384 SIGN_CONTEXT_V1,
1385 &sig,
1386 &kp.public_key
1387 )
1388 .unwrap()
1389 );
1390 assert!(!verify(b"tampered", SIGN_CONTEXT_V1, &sig, &kp.public_key).unwrap());
1392 assert_eq!(derive_public_key(&kp.secret_key).unwrap(), kp.public_key);
1394 }
1395
1396 #[test]
1397 fn pure_cnsa2_sign_roundtrip() {
1398 suite_roundtrip(
1399 Suite::PureCnsa2,
1400 SignatureLevel::Cat5,
1401 VERSION_SIG_PURE_CNSA2,
1402 );
1403 }
1404
1405 #[test]
1406 fn matched_cat3_sign_roundtrip() {
1407 suite_roundtrip(
1408 Suite::HybridMatched,
1409 SignatureLevel::Cat3,
1410 VERSION_SIG_MATCHED_CAT3,
1411 );
1412 }
1413
1414 #[test]
1415 fn matched_cat5_sign_roundtrip() {
1416 suite_roundtrip(
1417 Suite::HybridMatched,
1418 SignatureLevel::Cat5,
1419 VERSION_SIG_MATCHED_CAT5,
1420 );
1421 }
1422
1423 #[test]
1424 fn pure_cnsa2_sign_only_cat5() {
1425 assert!(generate_signing_keypair_suite(Suite::PureCnsa2, SignatureLevel::Cat3).is_err());
1426 assert!(generate_signing_keypair_suite(Suite::PureCnsa2, SignatureLevel::Cat2).is_err());
1427 }
1428
1429 #[test]
1430 fn matched_cat2_is_plain_hybrid() {
1431 let kp =
1433 generate_signing_keypair_suite(Suite::HybridMatched, SignatureLevel::Cat2).unwrap();
1434 assert_eq!(b64::decode(&kp.public_key).unwrap()[0], VERSION_CAT2);
1435 let sig = sign(b"x", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1436 assert!(verify(b"x", SIGN_CONTEXT_V1, &sig, &kp.public_key).unwrap());
1437 }
1438
1439 #[test]
1440 fn matched_sign_strict_and_requires_both_halves() {
1441 for (suite, level, classical_offset) in [
1443 (Suite::HybridMatched, SignatureLevel::Cat3, 1usize), (Suite::HybridMatched, SignatureLevel::Cat5, 1usize), ] {
1446 let kp = generate_signing_keypair_suite(suite, level).unwrap();
1447 let good = b64::decode(&sign(b"m", SIGN_CONTEXT_V1, &kp.secret_key).unwrap()).unwrap();
1448 let mut bad_c = good.clone();
1450 bad_c[classical_offset] ^= 0xFF;
1451 assert!(!verify(b"m", SIGN_CONTEXT_V1, &b64::encode(&bad_c), &kp.public_key).unwrap());
1452 let mut bad_ml = good.clone();
1454 let last = bad_ml.len() - 1;
1455 bad_ml[last] ^= 0xFF;
1456 assert!(!verify(b"m", SIGN_CONTEXT_V1, &b64::encode(&bad_ml), &kp.public_key).unwrap());
1457 }
1458 }
1459
1460 #[test]
1461 fn pure_cnsa2_nondeterministic_but_valid() {
1462 let kp = generate_signing_keypair_suite(Suite::PureCnsa2, SignatureLevel::Cat5).unwrap();
1463 let s1 = sign(b"m", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1464 let s2 = sign(b"m", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1465 assert_ne!(s1, s2, "hedged ML-DSA => non-reproducible");
1466 assert!(verify(b"m", SIGN_CONTEXT_V1, &s1, &kp.public_key).unwrap());
1467 assert!(verify(b"m", SIGN_CONTEXT_V1, &s2, &kp.public_key).unwrap());
1468 }
1469
1470 #[test]
1471 fn sign_suites_context_separation_and_cross_key() {
1472 for (suite, level) in [
1473 (Suite::PureCnsa2, SignatureLevel::Cat5),
1474 (Suite::HybridMatched, SignatureLevel::Cat3),
1475 (Suite::HybridMatched, SignatureLevel::Cat5),
1476 ] {
1477 let kp = generate_signing_keypair_suite(suite, level).unwrap();
1478 let kp2 = generate_signing_keypair_suite(suite, level).unwrap();
1479 let sig = sign(b"m", "metamorphic/sign/v1", &kp.secret_key).unwrap();
1480 assert!(!verify(b"m", "metamorphic/other/v1", &sig, &kp.public_key).unwrap());
1482 assert!(!verify(b"m", "metamorphic/sign/v1", &sig, &kp2.public_key).unwrap());
1484 }
1485 }
1486
1487 #[test]
1488 fn sign_cross_suite_pk_rejected() {
1489 let pure = generate_signing_keypair_suite(Suite::PureCnsa2, SignatureLevel::Cat5).unwrap();
1492 let legacy = generate_signing_keypair(); let sig_pure = sign(b"m", SIGN_CONTEXT_V1, &pure.secret_key).unwrap();
1494 assert!(!verify(b"m", SIGN_CONTEXT_V1, &sig_pure, &legacy.public_key).unwrap());
1495 let sig_legacy = sign(b"m", SIGN_CONTEXT_V1, &legacy.secret_key).unwrap();
1496 assert!(!verify(b"m", SIGN_CONTEXT_V1, &sig_legacy, &pure.public_key).unwrap());
1497 }
1498
1499 #[test]
1506 fn posture_all_six_and_pk_sig_agree() {
1507 let cases = [
1508 (Suite::Hybrid, SignatureLevel::Cat2, Suite::Hybrid),
1509 (Suite::Hybrid, SignatureLevel::Cat3, Suite::Hybrid),
1510 (Suite::Hybrid, SignatureLevel::Cat5, Suite::Hybrid),
1511 (Suite::PureCnsa2, SignatureLevel::Cat5, Suite::PureCnsa2),
1512 (
1513 Suite::HybridMatched,
1514 SignatureLevel::Cat3,
1515 Suite::HybridMatched,
1516 ),
1517 (
1518 Suite::HybridMatched,
1519 SignatureLevel::Cat5,
1520 Suite::HybridMatched,
1521 ),
1522 (Suite::HybridMatched, SignatureLevel::Cat2, Suite::Hybrid),
1524 ];
1525 for (suite, level, observed_suite) in cases {
1526 let kp = generate_signing_keypair_suite(suite, level).unwrap();
1527 assert_eq!(
1528 signature_posture(&kp.public_key).unwrap(),
1529 (observed_suite, level),
1530 "public-key posture for {suite:?}/{level:?}"
1531 );
1532 let sig = sign(b"checkpoint", SIGN_CONTEXT_V1, &kp.secret_key).unwrap();
1533 assert_eq!(
1534 signature_posture_from_signature(&sig).unwrap(),
1535 (observed_suite, level),
1536 "signature posture for {suite:?}/{level:?}"
1537 );
1538 assert_eq!(
1540 signature_posture(&kp.public_key).unwrap(),
1541 signature_posture_from_signature(&sig).unwrap(),
1542 "pk/sig posture agreement for {suite:?}/{level:?}"
1543 );
1544 }
1545 }
1546
1547 #[test]
1548 fn posture_invalid_base64_errors() {
1549 assert!(signature_posture("not!base64!").is_err());
1550 assert!(signature_posture_from_signature("also!bad!").is_err());
1551 }
1552
1553 #[test]
1554 fn posture_empty_input_errors() {
1555 let empty = b64::encode(&[]);
1556 assert!(signature_posture(&empty).is_err());
1557 assert!(signature_posture_from_signature(&empty).is_err());
1558 }
1559
1560 #[test]
1561 fn posture_unknown_tag_errors() {
1562 let blob = b64::encode(&[0x7fu8; 128]);
1564 assert!(signature_posture(&blob).is_err());
1565 assert!(signature_posture_from_signature(&blob).is_err());
1566 }
1567
1568 #[test]
1571 fn posture_truncated_blob_errors() {
1572 let kp = generate_signing_keypair_with_level(SignatureLevel::Cat3);
1573 let mut pk = b64::decode(&kp.public_key).unwrap();
1574 pk.truncate(pk.len() - 1);
1575 assert!(signature_posture(&b64::encode(&pk)).is_err());
1576
1577 let sig = b64::decode(&sign(b"m", SIGN_CONTEXT_V1, &kp.secret_key).unwrap()).unwrap();
1578 let mut short = sig.clone();
1579 short.truncate(short.len() - 1);
1580 assert!(signature_posture_from_signature(&b64::encode(&short)).is_err());
1581
1582 let mut long = sig;
1584 long.push(0u8);
1585 assert!(signature_posture_from_signature(&b64::encode(&long)).is_err());
1586 }
1587}