1use crate::error::{Error, Result};
11use crate::payment::metrics::QuotingMetricsTracker;
12use evmlib::merkle_payments::MerklePaymentCandidateNode;
13use evmlib::quoting_metrics::QuotingMetrics;
14use evmlib::PaymentQuote;
15use evmlib::RewardsAddress;
16use saorsa_core::MlDsa65;
17use saorsa_pqc::pqc::types::{MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature};
18use saorsa_pqc::pqc::MlDsaOperations;
19use std::time::SystemTime;
20use tracing::debug;
21
22pub type XorName = [u8; 32];
24
25pub type SignFn = Box<dyn Fn(&[u8]) -> Vec<u8> + Send + Sync>;
27
28pub struct QuoteGenerator {
33 rewards_address: RewardsAddress,
35 metrics_tracker: QuotingMetricsTracker,
37 sign_fn: Option<SignFn>,
40 pub_key: Vec<u8>,
42}
43
44impl QuoteGenerator {
45 #[must_use]
54 pub fn new(rewards_address: RewardsAddress, metrics_tracker: QuotingMetricsTracker) -> Self {
55 Self {
56 rewards_address,
57 metrics_tracker,
58 sign_fn: None,
59 pub_key: Vec::new(),
60 }
61 }
62
63 pub fn set_signer<F>(&mut self, pub_key: Vec<u8>, sign_fn: F)
70 where
71 F: Fn(&[u8]) -> Vec<u8> + Send + Sync + 'static,
72 {
73 self.pub_key = pub_key;
74 self.sign_fn = Some(Box::new(sign_fn));
75 }
76
77 #[must_use]
79 pub fn can_sign(&self) -> bool {
80 self.sign_fn.is_some()
81 }
82
83 pub fn probe_signer(&self) -> Result<()> {
89 let sign_fn = self
90 .sign_fn
91 .as_ref()
92 .ok_or_else(|| Error::Payment("Signer not set".to_string()))?;
93 let test_msg = b"ant-signing-probe";
94 let test_sig = sign_fn(test_msg);
95 if test_sig.is_empty() {
96 return Err(Error::Payment(
97 "ML-DSA-65 signing probe failed: empty signature produced".to_string(),
98 ));
99 }
100 Ok(())
101 }
102
103 pub fn create_quote(
119 &self,
120 content: XorName,
121 data_size: usize,
122 data_type: u32,
123 ) -> Result<PaymentQuote> {
124 let sign_fn = self
125 .sign_fn
126 .as_ref()
127 .ok_or_else(|| Error::Payment("Quote signing not configured".to_string()))?;
128
129 let timestamp = SystemTime::now();
130
131 let quoting_metrics = self.metrics_tracker.get_metrics(data_size, data_type);
133
134 let xor_name = xor_name::XorName(content);
136
137 let bytes = PaymentQuote::bytes_for_signing(
139 xor_name,
140 timestamp,
141 "ing_metrics,
142 &self.rewards_address,
143 );
144
145 let signature = sign_fn(&bytes);
147 if signature.is_empty() {
148 return Err(Error::Payment(
149 "Signing produced empty signature".to_string(),
150 ));
151 }
152
153 let quote = PaymentQuote {
154 content: xor_name,
155 timestamp,
156 quoting_metrics,
157 pub_key: self.pub_key.clone(),
158 rewards_address: self.rewards_address,
159 signature,
160 };
161
162 if tracing::enabled!(tracing::Level::DEBUG) {
163 let content_hex = hex::encode(content);
164 debug!("Generated quote for {content_hex} (size: {data_size}, type: {data_type})");
165 }
166
167 Ok(quote)
168 }
169
170 #[must_use]
172 pub fn rewards_address(&self) -> &RewardsAddress {
173 &self.rewards_address
174 }
175
176 #[must_use]
178 pub fn current_metrics(&self) -> QuotingMetrics {
179 self.metrics_tracker.get_metrics(0, 0)
180 }
181
182 pub fn record_payment(&self) {
184 self.metrics_tracker.record_payment();
185 }
186
187 pub fn record_store(&self, data_type: u32) {
189 self.metrics_tracker.record_store(data_type);
190 }
191
192 pub fn create_merkle_candidate_quote(
207 &self,
208 data_size: usize,
209 data_type: u32,
210 merkle_payment_timestamp: u64,
211 ) -> Result<MerklePaymentCandidateNode> {
212 let sign_fn = self
213 .sign_fn
214 .as_ref()
215 .ok_or_else(|| Error::Payment("Quote signing not configured".to_string()))?;
216
217 let quoting_metrics = self.metrics_tracker.get_metrics(data_size, data_type);
218
219 let msg = MerklePaymentCandidateNode::bytes_to_sign(
221 "ing_metrics,
222 &self.rewards_address,
223 merkle_payment_timestamp,
224 );
225
226 let signature = sign_fn(&msg);
228 if signature.is_empty() {
229 return Err(Error::Payment(
230 "ML-DSA-65 signing produced empty signature for merkle candidate".to_string(),
231 ));
232 }
233
234 let candidate = MerklePaymentCandidateNode {
235 pub_key: self.pub_key.clone(),
236 quoting_metrics,
237 reward_address: self.rewards_address,
238 merkle_payment_timestamp,
239 signature,
240 };
241
242 if tracing::enabled!(tracing::Level::DEBUG) {
243 debug!(
244 "Generated ML-DSA-65 merkle candidate quote (size: {data_size}, type: {data_type}, ts: {merkle_payment_timestamp})"
245 );
246 }
247
248 Ok(candidate)
249 }
250}
251
252#[must_use]
263pub fn verify_quote_content(quote: &PaymentQuote, expected_content: &XorName) -> bool {
264 if quote.content.0 != *expected_content {
266 if tracing::enabled!(tracing::Level::DEBUG) {
267 debug!(
268 "Quote content mismatch: expected {}, got {}",
269 hex::encode(expected_content),
270 hex::encode(quote.content.0)
271 );
272 }
273 return false;
274 }
275 true
276}
277
278#[must_use]
292pub fn verify_quote_signature(quote: &PaymentQuote) -> bool {
293 let pub_key = match MlDsaPublicKey::from_bytes("e.pub_key) {
295 Ok(pk) => pk,
296 Err(e) => {
297 debug!("Failed to parse ML-DSA-65 public key from quote: {e}");
298 return false;
299 }
300 };
301
302 let signature = match MlDsaSignature::from_bytes("e.signature) {
304 Ok(sig) => sig,
305 Err(e) => {
306 debug!("Failed to parse ML-DSA-65 signature from quote: {e}");
307 return false;
308 }
309 };
310
311 let bytes = quote.bytes_for_sig();
313
314 let ml_dsa = MlDsa65::new();
316 match ml_dsa.verify(&pub_key, &bytes, &signature) {
317 Ok(valid) => {
318 if !valid {
319 debug!("ML-DSA-65 quote signature verification failed");
320 }
321 valid
322 }
323 Err(e) => {
324 debug!("ML-DSA-65 verification error: {e}");
325 false
326 }
327 }
328}
329
330#[must_use]
340pub fn verify_merkle_candidate_signature(candidate: &MerklePaymentCandidateNode) -> bool {
341 let pub_key = match MlDsaPublicKey::from_bytes(&candidate.pub_key) {
342 Ok(pk) => pk,
343 Err(e) => {
344 debug!("Failed to parse ML-DSA-65 public key from merkle candidate: {e}");
345 return false;
346 }
347 };
348
349 let signature = match MlDsaSignature::from_bytes(&candidate.signature) {
350 Ok(sig) => sig,
351 Err(e) => {
352 debug!("Failed to parse ML-DSA-65 signature from merkle candidate: {e}");
353 return false;
354 }
355 };
356
357 let msg = MerklePaymentCandidateNode::bytes_to_sign(
358 &candidate.quoting_metrics,
359 &candidate.reward_address,
360 candidate.merkle_payment_timestamp,
361 );
362
363 let ml_dsa = MlDsa65::new();
364 match ml_dsa.verify(&pub_key, &msg, &signature) {
365 Ok(valid) => {
366 if !valid {
367 debug!("ML-DSA-65 merkle candidate signature verification failed");
368 }
369 valid
370 }
371 Err(e) => {
372 debug!("ML-DSA-65 merkle candidate verification error: {e}");
373 false
374 }
375 }
376}
377
378pub fn wire_ml_dsa_signer(
393 generator: &mut QuoteGenerator,
394 identity: &saorsa_core::identity::NodeIdentity,
395) -> Result<()> {
396 let pub_key_bytes = identity.public_key().as_bytes().to_vec();
397 let sk_bytes = identity.secret_key_bytes().to_vec();
398 let sk = MlDsaSecretKey::from_bytes(&sk_bytes)
399 .map_err(|e| Error::Crypto(format!("Failed to deserialize ML-DSA-65 secret key: {e}")))?;
400 let ml_dsa = MlDsa65::new();
401 generator.set_signer(pub_key_bytes, move |msg| match ml_dsa.sign(&sk, msg) {
402 Ok(sig) => sig.as_bytes().to_vec(),
403 Err(e) => {
404 tracing::error!("ML-DSA-65 signing failed: {e}");
405 vec![]
406 }
407 });
408 generator.probe_signer()?;
409 Ok(())
410}
411
412#[cfg(test)]
413#[allow(clippy::expect_used)]
414mod tests {
415 use super::*;
416 use crate::payment::metrics::QuotingMetricsTracker;
417 use saorsa_pqc::pqc::types::MlDsaSecretKey;
418
419 fn create_test_generator() -> QuoteGenerator {
420 let rewards_address = RewardsAddress::new([1u8; 20]);
421 let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
422
423 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
424
425 generator.set_signer(vec![0u8; 64], |bytes| {
427 let mut sig = vec![0u8; 64];
429 for (i, b) in bytes.iter().take(64).enumerate() {
430 sig[i] = *b;
431 }
432 sig
433 });
434
435 generator
436 }
437
438 #[test]
439 fn test_create_quote() {
440 let generator = create_test_generator();
441 let content = [42u8; 32];
442
443 let quote = generator.create_quote(content, 1024, 0);
444 assert!(quote.is_ok());
445
446 let quote = quote.expect("valid quote");
447 assert_eq!(quote.content.0, content);
448 }
449
450 #[test]
451 fn test_verify_quote_content() {
452 let generator = create_test_generator();
453 let content = [42u8; 32];
454
455 let quote = generator
456 .create_quote(content, 1024, 0)
457 .expect("valid quote");
458 assert!(verify_quote_content("e, &content));
459
460 let wrong_content = [99u8; 32];
462 assert!(!verify_quote_content("e, &wrong_content));
463 }
464
465 #[test]
466 fn test_generator_without_signer() {
467 let rewards_address = RewardsAddress::new([1u8; 20]);
468 let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
469 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
470
471 assert!(!generator.can_sign());
472
473 let content = [42u8; 32];
474 let result = generator.create_quote(content, 1024, 0);
475 assert!(result.is_err());
476 }
477
478 #[test]
479 fn test_quote_signature_round_trip_real_keys() {
480 let ml_dsa = MlDsa65::new();
481 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
482
483 let rewards_address = RewardsAddress::new([2u8; 20]);
484 let metrics_tracker = QuotingMetricsTracker::new(1000, 100);
485 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
486
487 let pub_key_bytes = public_key.as_bytes().to_vec();
488 let sk_bytes = secret_key.as_bytes().to_vec();
489 generator.set_signer(pub_key_bytes, move |msg| {
490 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("secret key parse");
491 let ml_dsa = MlDsa65::new();
492 ml_dsa.sign(&sk, msg).expect("signing").as_bytes().to_vec()
493 });
494
495 let content = [7u8; 32];
496 let quote = generator
497 .create_quote(content, 2048, 0)
498 .expect("create quote");
499
500 assert!(verify_quote_signature("e));
502
503 let mut tampered_quote = quote;
505 if let Some(byte) = tampered_quote.signature.first_mut() {
506 *byte ^= 0xFF;
507 }
508 assert!(!verify_quote_signature(&tampered_quote));
509 }
510
511 #[test]
512 fn test_empty_signature_fails_verification() {
513 let generator = create_test_generator();
514 let content = [42u8; 32];
515
516 let quote = generator
517 .create_quote(content, 1024, 0)
518 .expect("create quote");
519
520 assert!(!verify_quote_signature("e));
523 }
524
525 #[test]
526 fn test_rewards_address_getter() {
527 let addr = RewardsAddress::new([42u8; 20]);
528 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
529 let generator = QuoteGenerator::new(addr, metrics_tracker);
530
531 assert_eq!(*generator.rewards_address(), addr);
532 }
533
534 #[test]
535 fn test_current_metrics() {
536 let rewards_address = RewardsAddress::new([1u8; 20]);
537 let metrics_tracker = QuotingMetricsTracker::new(500, 50);
538 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
539
540 let metrics = generator.current_metrics();
541 assert_eq!(metrics.max_records, 500);
542 assert_eq!(metrics.close_records_stored, 50);
543 assert_eq!(metrics.data_size, 0);
544 assert_eq!(metrics.data_type, 0);
545 }
546
547 #[test]
548 fn test_record_payment_delegation() {
549 let rewards_address = RewardsAddress::new([1u8; 20]);
550 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
551 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
552
553 generator.record_payment();
554 generator.record_payment();
555
556 let metrics = generator.current_metrics();
557 assert_eq!(metrics.received_payment_count, 2);
558 }
559
560 #[test]
561 fn test_record_store_delegation() {
562 let rewards_address = RewardsAddress::new([1u8; 20]);
563 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
564 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
565
566 generator.record_store(0);
567 generator.record_store(1);
568 generator.record_store(0);
569
570 let metrics = generator.current_metrics();
571 assert_eq!(metrics.close_records_stored, 3);
572 }
573
574 #[test]
575 fn test_create_quote_different_data_types() {
576 let generator = create_test_generator();
577 let content = [10u8; 32];
578
579 let q0 = generator.create_quote(content, 1024, 0).expect("type 0");
581 assert_eq!(q0.quoting_metrics.data_type, 0);
582
583 let q1 = generator.create_quote(content, 512, 1).expect("type 1");
585 assert_eq!(q1.quoting_metrics.data_type, 1);
586
587 let q2 = generator.create_quote(content, 256, 2).expect("type 2");
589 assert_eq!(q2.quoting_metrics.data_type, 2);
590 }
591
592 #[test]
593 fn test_create_quote_zero_size() {
594 let generator = create_test_generator();
595 let content = [11u8; 32];
596
597 let quote = generator.create_quote(content, 0, 0).expect("zero size");
598 assert_eq!(quote.quoting_metrics.data_size, 0);
599 }
600
601 #[test]
602 fn test_create_quote_large_size() {
603 let generator = create_test_generator();
604 let content = [12u8; 32];
605
606 let quote = generator
607 .create_quote(content, 10_000_000, 0)
608 .expect("large size");
609 assert_eq!(quote.quoting_metrics.data_size, 10_000_000);
610 }
611
612 #[test]
613 fn test_verify_quote_signature_empty_pub_key() {
614 let quote = PaymentQuote {
615 content: xor_name::XorName([0u8; 32]),
616 timestamp: SystemTime::now(),
617 quoting_metrics: QuotingMetrics {
618 data_size: 0,
619 data_type: 0,
620 close_records_stored: 0,
621 records_per_type: vec![],
622 max_records: 0,
623 received_payment_count: 0,
624 live_time: 0,
625 network_density: None,
626 network_size: None,
627 },
628 rewards_address: RewardsAddress::new([0u8; 20]),
629 pub_key: vec![],
630 signature: vec![],
631 };
632
633 assert!(!verify_quote_signature("e));
635 }
636
637 #[test]
638 fn test_can_sign_after_set_signer() {
639 let rewards_address = RewardsAddress::new([1u8; 20]);
640 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
641 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
642
643 assert!(!generator.can_sign());
644
645 generator.set_signer(vec![0u8; 32], |_| vec![0u8; 32]);
646
647 assert!(generator.can_sign());
648 }
649
650 #[test]
651 fn test_wire_ml_dsa_signer_returns_ok_with_valid_identity() {
652 let identity = saorsa_core::identity::NodeIdentity::generate().expect("keypair generation");
653 let rewards_address = RewardsAddress::new([3u8; 20]);
654 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
655 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
656
657 let result = wire_ml_dsa_signer(&mut generator, &identity);
658 assert!(
659 result.is_ok(),
660 "wire_ml_dsa_signer should succeed: {result:?}"
661 );
662 assert!(generator.can_sign());
663 }
664
665 #[test]
666 fn test_probe_signer_fails_without_signer() {
667 let rewards_address = RewardsAddress::new([1u8; 20]);
668 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
669 let generator = QuoteGenerator::new(rewards_address, metrics_tracker);
670
671 let result = generator.probe_signer();
672 assert!(result.is_err());
673 }
674
675 #[test]
676 fn test_probe_signer_fails_with_empty_signature() {
677 let rewards_address = RewardsAddress::new([1u8; 20]);
678 let metrics_tracker = QuotingMetricsTracker::new(1000, 0);
679 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
680
681 generator.set_signer(vec![0u8; 32], |_| vec![]);
682
683 let result = generator.probe_signer();
684 assert!(result.is_err());
685 }
686
687 #[test]
688 fn test_create_merkle_candidate_quote_with_ml_dsa() {
689 let ml_dsa = MlDsa65::new();
690 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keypair generation");
691
692 let rewards_address = RewardsAddress::new([0x42u8; 20]);
693 let metrics_tracker = QuotingMetricsTracker::new(800, 50);
694 let mut generator = QuoteGenerator::new(rewards_address, metrics_tracker);
695
696 let pub_key_bytes = public_key.as_bytes().to_vec();
698 let sk_bytes = secret_key.as_bytes().to_vec();
699 generator.set_signer(pub_key_bytes.clone(), move |msg| {
700 let sk = MlDsaSecretKey::from_bytes(&sk_bytes).expect("sk parse");
701 let ml_dsa = MlDsa65::new();
702 ml_dsa.sign(&sk, msg).expect("sign").as_bytes().to_vec()
703 });
704
705 let timestamp = std::time::SystemTime::now()
706 .duration_since(std::time::UNIX_EPOCH)
707 .expect("system time")
708 .as_secs();
709
710 let result = generator.create_merkle_candidate_quote(2048, 0, timestamp);
711
712 assert!(
713 result.is_ok(),
714 "create_merkle_candidate_quote should succeed: {result:?}"
715 );
716
717 let candidate = result.expect("valid candidate");
718
719 assert_eq!(candidate.reward_address, rewards_address);
721
722 assert_eq!(candidate.merkle_payment_timestamp, timestamp);
724
725 assert_eq!(candidate.quoting_metrics.data_size, 2048);
727 assert_eq!(candidate.quoting_metrics.data_type, 0);
728 assert_eq!(candidate.quoting_metrics.max_records, 800);
729 assert_eq!(candidate.quoting_metrics.close_records_stored, 50);
730
731 assert_eq!(
733 candidate.pub_key, pub_key_bytes,
734 "Public key should be raw ML-DSA-65 bytes"
735 );
736
737 assert!(
739 verify_merkle_candidate_signature(&candidate),
740 "ML-DSA-65 merkle candidate signature must be valid"
741 );
742
743 let mut tampered = candidate;
745 tampered.merkle_payment_timestamp = timestamp + 1;
746 assert!(
747 !verify_merkle_candidate_signature(&tampered),
748 "Tampered timestamp should invalidate the ML-DSA-65 signature"
749 );
750 }
751
752 fn make_valid_merkle_candidate() -> MerklePaymentCandidateNode {
758 let ml_dsa = MlDsa65::new();
759 let (public_key, secret_key) = ml_dsa.generate_keypair().expect("keygen");
760
761 let rewards_address = RewardsAddress::new([0xABu8; 20]);
762 let timestamp = std::time::SystemTime::now()
763 .duration_since(std::time::UNIX_EPOCH)
764 .expect("system time")
765 .as_secs();
766 let metrics = QuotingMetrics {
767 data_size: 4096,
768 data_type: 0,
769 close_records_stored: 10,
770 records_per_type: vec![],
771 max_records: 500,
772 received_payment_count: 3,
773 live_time: 600,
774 network_density: None,
775 network_size: None,
776 };
777
778 let msg = MerklePaymentCandidateNode::bytes_to_sign(&metrics, &rewards_address, timestamp);
779 let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
780 let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
781
782 MerklePaymentCandidateNode {
783 pub_key: public_key.as_bytes().to_vec(),
784 quoting_metrics: metrics,
785 reward_address: rewards_address,
786 merkle_payment_timestamp: timestamp,
787 signature,
788 }
789 }
790
791 #[test]
792 fn test_verify_merkle_candidate_valid_signature() {
793 let candidate = make_valid_merkle_candidate();
794 assert!(
795 verify_merkle_candidate_signature(&candidate),
796 "Freshly signed merkle candidate must verify"
797 );
798 }
799
800 #[test]
801 fn test_verify_merkle_candidate_tampered_pub_key() {
802 let mut candidate = make_valid_merkle_candidate();
803 if let Some(byte) = candidate.pub_key.first_mut() {
805 *byte ^= 0xFF;
806 }
807 assert!(
808 !verify_merkle_candidate_signature(&candidate),
809 "Tampered pub_key must invalidate the signature"
810 );
811 }
812
813 #[test]
814 fn test_verify_merkle_candidate_tampered_reward_address() {
815 let mut candidate = make_valid_merkle_candidate();
816 candidate.reward_address = RewardsAddress::new([0xFFu8; 20]);
817 assert!(
818 !verify_merkle_candidate_signature(&candidate),
819 "Tampered reward_address must invalidate the signature"
820 );
821 }
822
823 #[test]
824 fn test_verify_merkle_candidate_tampered_metrics() {
825 let mut candidate = make_valid_merkle_candidate();
826 candidate.quoting_metrics.data_size = 999_999;
827 assert!(
828 !verify_merkle_candidate_signature(&candidate),
829 "Tampered quoting_metrics must invalidate the signature"
830 );
831 }
832
833 #[test]
834 fn test_verify_merkle_candidate_tampered_signature_byte() {
835 let mut candidate = make_valid_merkle_candidate();
836 if let Some(byte) = candidate.signature.first_mut() {
837 *byte ^= 0xFF;
838 }
839 assert!(
840 !verify_merkle_candidate_signature(&candidate),
841 "Tampered signature byte must fail verification"
842 );
843 }
844
845 #[test]
846 fn test_verify_merkle_candidate_empty_pub_key() {
847 let mut candidate = make_valid_merkle_candidate();
848 candidate.pub_key = vec![];
849 assert!(
850 !verify_merkle_candidate_signature(&candidate),
851 "Empty pub_key must fail verification"
852 );
853 }
854
855 #[test]
856 fn test_verify_merkle_candidate_empty_signature() {
857 let mut candidate = make_valid_merkle_candidate();
858 candidate.signature = vec![];
859 assert!(
860 !verify_merkle_candidate_signature(&candidate),
861 "Empty signature must fail verification"
862 );
863 }
864
865 #[test]
866 fn test_verify_merkle_candidate_wrong_length_signature() {
867 let mut candidate = make_valid_merkle_candidate();
868 candidate.signature = vec![0xAA; 100];
870 assert!(
871 !verify_merkle_candidate_signature(&candidate),
872 "Wrong-length signature must fail verification"
873 );
874 }
875
876 #[test]
877 fn test_verify_merkle_candidate_wrong_length_pub_key() {
878 let mut candidate = make_valid_merkle_candidate();
879 candidate.pub_key = vec![0xBB; 100];
881 assert!(
882 !verify_merkle_candidate_signature(&candidate),
883 "Wrong-length pub_key must fail verification"
884 );
885 }
886
887 #[test]
888 fn test_verify_merkle_candidate_cross_key_rejection() {
889 let candidate = make_valid_merkle_candidate();
891 let ml_dsa = MlDsa65::new();
892 let (other_pk, _) = ml_dsa.generate_keypair().expect("keygen");
893
894 let mut swapped = candidate;
895 swapped.pub_key = other_pk.as_bytes().to_vec();
896 assert!(
897 !verify_merkle_candidate_signature(&swapped),
898 "Signature from key A must not verify under key B"
899 );
900 }
901}