Skip to main content

ant_protocol/payment/
verify.rs

1//! Pure verification helpers for payment quotes and merkle candidates.
2//!
3//! These functions inspect signed wire artifacts (`PaymentQuote` and
4//! `MerklePaymentCandidateNode`) and confirm their ML-DSA-65 signatures
5//! are valid. They do no I/O and do not touch on-chain state.
6//!
7//! Both the client (when building proofs) and the node (when validating
8//! incoming PUTs) need identical verification semantics — keeping these
9//! functions in `ant-protocol` ensures the client and node cannot drift.
10
11use 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/// Verify that a payment quote's content address matches the expected address.
20///
21/// This is a pure field-equality check — the signature is verified
22/// separately via [`verify_quote_signature`].
23///
24/// # Arguments
25///
26/// * `quote` - The quote to check.
27/// * `expected_content` - The address the caller expected.
28///
29/// # Returns
30///
31/// `true` if `quote.content` equals `expected_content`.
32#[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/// Verify a payment quote's ML-DSA-65 signature.
48///
49/// Autonomi uses ML-DSA-65 post-quantum signatures for quote signing
50/// (not the Ed25519/libp2p signatures that the upstream `ant-evm`
51/// library assumes). The `pub_key` field holds the raw ML-DSA-65 public
52/// key bytes; `signature` is the ML-DSA-65 signature over the quote's
53/// canonical signing payload (`PaymentQuote::bytes_for_sig`).
54///
55/// # Returns
56///
57/// `true` if the signature is valid for `quote.bytes_for_sig()`.
58#[must_use]
59pub fn verify_quote_signature(quote: &PaymentQuote) -> bool {
60    let pub_key = match MlDsaPublicKey::from_bytes(&quote.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(&quote.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/// Verify a `MerklePaymentCandidateNode`'s ML-DSA-65 signature.
94///
95/// Autonomi uses ML-DSA-65 for merkle candidate signing; the upstream
96/// `MerklePaymentCandidateNode::verify_signature()` method expects libp2p
97/// Ed25519 keys and cannot be used.
98///
99/// `pub_key` holds the raw ML-DSA-65 public key bytes; `signature` is
100/// the ML-DSA-65 signature over `MerklePaymentCandidateNode::bytes_to_sign()`.
101#[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    // ADR-0004: the per-node signature now covers the commitment binding
120    // (`committed_key_count`, `commitment_pin`) too, so a count/pin mismatch is
121    // genuine same-key-signed evidence. Must match `bytes_to_sign` in evmlib.
122    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        // Build a quote with all fields except signature, then sign bytes_for_sig().
161        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(&quote, &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(&quote, &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(&quote));
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(&quote));
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(&quote));
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(&quote));
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}