ant_protocol/payment/
verify.rs1use crate::chunk::XorName;
12use crate::logging::debug;
13use evmlib::merkle_payments::MerklePaymentCandidateNode;
14use evmlib::PaymentQuote;
15use saorsa_core::MlDsa65;
16use saorsa_pqc::pqc::types::{MlDsaPublicKey, MlDsaSignature};
17use saorsa_pqc::pqc::MlDsaOperations;
18
19#[must_use]
33pub fn verify_quote_content(quote: &PaymentQuote, expected_content: &XorName) -> bool {
34 if quote.content.0 != *expected_content {
35 if crate::logging::enabled!(crate::logging::Level::DEBUG) {
36 debug!(
37 "Quote content mismatch: expected {}, got {}",
38 hex::encode(expected_content),
39 hex::encode(quote.content.0)
40 );
41 }
42 return false;
43 }
44 true
45}
46
47#[must_use]
59pub fn verify_quote_signature(quote: &PaymentQuote) -> bool {
60 let pub_key = match MlDsaPublicKey::from_bytes("e.pub_key) {
61 Ok(pk) => pk,
62 Err(e) => {
63 debug!("Failed to parse ML-DSA-65 public key from quote: {e}");
64 return false;
65 }
66 };
67
68 let signature = match MlDsaSignature::from_bytes("e.signature) {
69 Ok(sig) => sig,
70 Err(e) => {
71 debug!("Failed to parse ML-DSA-65 signature from quote: {e}");
72 return false;
73 }
74 };
75
76 let bytes = quote.bytes_for_sig();
77
78 let ml_dsa = MlDsa65::new();
79 match ml_dsa.verify(&pub_key, &bytes, &signature) {
80 Ok(valid) => {
81 if !valid {
82 debug!("ML-DSA-65 quote signature verification failed");
83 }
84 valid
85 }
86 Err(e) => {
87 debug!("ML-DSA-65 verification error: {e}");
88 false
89 }
90 }
91}
92
93#[must_use]
102pub fn verify_merkle_candidate_signature(candidate: &MerklePaymentCandidateNode) -> bool {
103 let pub_key = match MlDsaPublicKey::from_bytes(&candidate.pub_key) {
104 Ok(pk) => pk,
105 Err(e) => {
106 debug!("Failed to parse ML-DSA-65 public key from merkle candidate: {e}");
107 return false;
108 }
109 };
110
111 let signature = match MlDsaSignature::from_bytes(&candidate.signature) {
112 Ok(sig) => sig,
113 Err(e) => {
114 debug!("Failed to parse ML-DSA-65 signature from merkle candidate: {e}");
115 return false;
116 }
117 };
118
119 let msg = MerklePaymentCandidateNode::bytes_to_sign(
123 &candidate.price,
124 &candidate.reward_address,
125 candidate.merkle_payment_timestamp,
126 candidate.committed_key_count,
127 &candidate.commitment_pin,
128 );
129
130 let ml_dsa = MlDsa65::new();
131 match ml_dsa.verify(&pub_key, &msg, &signature) {
132 Ok(valid) => {
133 if !valid {
134 debug!("ML-DSA-65 merkle candidate signature verification failed");
135 }
136 valid
137 }
138 Err(e) => {
139 debug!("ML-DSA-65 merkle candidate verification error: {e}");
140 false
141 }
142 }
143}
144
145#[cfg(test)]
146#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
147mod tests {
148 use super::*;
149 use evmlib::common::Amount;
150 use evmlib::RewardsAddress;
151 use saorsa_pqc::pqc::types::MlDsaSecretKey;
152 use std::time::SystemTime;
153
154 fn real_ml_dsa_quote() -> (PaymentQuote, [u8; 32]) {
155 let content = [7u8; 32];
156 let ml_dsa = MlDsa65::new();
157 let (pub_key, secret_key) = ml_dsa.generate_keypair().expect("keypair");
158 let pub_key_bytes = pub_key.as_bytes().to_vec();
159
160 let mut quote = PaymentQuote {
162 content: xor_name::XorName(content),
163 timestamp: SystemTime::now(),
164 price: Amount::from(42u64),
165 rewards_address: RewardsAddress::new([2u8; 20]),
166 pub_key: pub_key_bytes,
167 signature: vec![],
168 committed_key_count: 0,
169 commitment_pin: None,
170 };
171 let msg = quote.bytes_for_sig();
172 let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
173 let sig = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
174 quote.signature = sig;
175
176 (quote, content)
177 }
178
179 #[test]
180 fn verify_quote_content_matches() {
181 let (quote, content) = real_ml_dsa_quote();
182 assert!(verify_quote_content("e, &content));
183 }
184
185 #[test]
186 fn verify_quote_content_mismatch() {
187 let (quote, _) = real_ml_dsa_quote();
188 let wrong = [0xFFu8; 32];
189 assert!(!verify_quote_content("e, &wrong));
190 }
191
192 #[test]
193 fn verify_quote_signature_real_keys_roundtrip() {
194 let (quote, _) = real_ml_dsa_quote();
195 assert!(verify_quote_signature("e));
196 }
197
198 #[test]
199 fn verify_quote_signature_tampered_signature_fails() {
200 let (mut quote, _) = real_ml_dsa_quote();
201 if let Some(byte) = quote.signature.first_mut() {
202 *byte ^= 0xFF;
203 }
204 assert!(!verify_quote_signature("e));
205 }
206
207 #[test]
208 fn verify_quote_signature_empty_pub_key_fails() {
209 let quote = PaymentQuote {
210 content: xor_name::XorName([0u8; 32]),
211 timestamp: SystemTime::now(),
212 price: Amount::from(1u64),
213 rewards_address: RewardsAddress::new([0u8; 20]),
214 pub_key: vec![],
215 signature: vec![],
216 committed_key_count: 0,
217 commitment_pin: None,
218 };
219 assert!(!verify_quote_signature("e));
220 }
221
222 #[test]
223 fn verify_quote_signature_empty_signature_fails() {
224 let ml_dsa = MlDsa65::new();
225 let (pub_key, _sk) = ml_dsa.generate_keypair().expect("keypair");
226 let quote = PaymentQuote {
227 content: xor_name::XorName([0u8; 32]),
228 timestamp: SystemTime::now(),
229 price: Amount::from(1u64),
230 rewards_address: RewardsAddress::new([0u8; 20]),
231 pub_key: pub_key.as_bytes().to_vec(),
232 signature: vec![],
233 committed_key_count: 0,
234 commitment_pin: None,
235 };
236 assert!(!verify_quote_signature("e));
237 }
238
239 fn real_ml_dsa_merkle_candidate() -> MerklePaymentCandidateNode {
240 let ml_dsa = MlDsa65::new();
241 let (pub_key, secret_key) = ml_dsa.generate_keypair().expect("keypair");
242 let price = Amount::from(1024u64);
243 let reward_address = RewardsAddress::new([3u8; 20]);
244 let merkle_payment_timestamp = 1_700_000_000u64;
245 let committed_key_count = 7u32;
246 let commitment_pin = Some([5u8; 32]);
247 let msg = MerklePaymentCandidateNode::bytes_to_sign(
248 &price,
249 &reward_address,
250 merkle_payment_timestamp,
251 committed_key_count,
252 &commitment_pin,
253 );
254 let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
255 let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
256 MerklePaymentCandidateNode {
257 pub_key: pub_key.as_bytes().to_vec(),
258 price,
259 reward_address,
260 merkle_payment_timestamp,
261 committed_key_count,
262 commitment_pin,
263 signature,
264 }
265 }
266
267 #[test]
268 fn verify_merkle_candidate_real_keys_roundtrip() {
269 let candidate = real_ml_dsa_merkle_candidate();
270 assert!(verify_merkle_candidate_signature(&candidate));
271 }
272
273 #[test]
274 fn verify_merkle_candidate_tampered_fails() {
275 let mut candidate = real_ml_dsa_merkle_candidate();
276 if let Some(byte) = candidate.signature.first_mut() {
277 *byte ^= 0x55;
278 }
279 assert!(!verify_merkle_candidate_signature(&candidate));
280 }
281
282 #[test]
283 fn verify_merkle_candidate_empty_pub_key_fails() {
284 let mut candidate = real_ml_dsa_merkle_candidate();
285 candidate.pub_key = vec![];
286 assert!(!verify_merkle_candidate_signature(&candidate));
287 }
288
289 #[test]
290 fn verify_merkle_candidate_wrong_timestamp_fails() {
291 let mut candidate = real_ml_dsa_merkle_candidate();
292 candidate.merkle_payment_timestamp = candidate.merkle_payment_timestamp.wrapping_add(1);
293 assert!(!verify_merkle_candidate_signature(&candidate));
294 }
295}