use crate::chunk::XorName;
use crate::logging::debug;
use evmlib::merkle_payments::MerklePaymentCandidateNode;
use evmlib::PaymentQuote;
use saorsa_core::MlDsa65;
use saorsa_pqc::pqc::types::{MlDsaPublicKey, MlDsaSignature};
use saorsa_pqc::pqc::MlDsaOperations;
#[must_use]
pub fn verify_quote_content(quote: &PaymentQuote, expected_content: &XorName) -> bool {
if quote.content.0 != *expected_content {
if crate::logging::enabled!(crate::logging::Level::DEBUG) {
debug!(
"Quote content mismatch: expected {}, got {}",
hex::encode(expected_content),
hex::encode(quote.content.0)
);
}
return false;
}
true
}
#[must_use]
pub fn verify_quote_signature(quote: &PaymentQuote) -> bool {
let pub_key = match MlDsaPublicKey::from_bytes("e.pub_key) {
Ok(pk) => pk,
Err(e) => {
debug!("Failed to parse ML-DSA-65 public key from quote: {e}");
return false;
}
};
let signature = match MlDsaSignature::from_bytes("e.signature) {
Ok(sig) => sig,
Err(e) => {
debug!("Failed to parse ML-DSA-65 signature from quote: {e}");
return false;
}
};
let bytes = quote.bytes_for_sig();
let ml_dsa = MlDsa65::new();
match ml_dsa.verify(&pub_key, &bytes, &signature) {
Ok(valid) => {
if !valid {
debug!("ML-DSA-65 quote signature verification failed");
}
valid
}
Err(e) => {
debug!("ML-DSA-65 verification error: {e}");
false
}
}
}
#[must_use]
pub fn verify_merkle_candidate_signature(candidate: &MerklePaymentCandidateNode) -> bool {
let pub_key = match MlDsaPublicKey::from_bytes(&candidate.pub_key) {
Ok(pk) => pk,
Err(e) => {
debug!("Failed to parse ML-DSA-65 public key from merkle candidate: {e}");
return false;
}
};
let signature = match MlDsaSignature::from_bytes(&candidate.signature) {
Ok(sig) => sig,
Err(e) => {
debug!("Failed to parse ML-DSA-65 signature from merkle candidate: {e}");
return false;
}
};
let msg = MerklePaymentCandidateNode::bytes_to_sign(
&candidate.price,
&candidate.reward_address,
candidate.merkle_payment_timestamp,
);
let ml_dsa = MlDsa65::new();
match ml_dsa.verify(&pub_key, &msg, &signature) {
Ok(valid) => {
if !valid {
debug!("ML-DSA-65 merkle candidate signature verification failed");
}
valid
}
Err(e) => {
debug!("ML-DSA-65 merkle candidate verification error: {e}");
false
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use evmlib::common::Amount;
use evmlib::RewardsAddress;
use saorsa_pqc::pqc::types::MlDsaSecretKey;
use std::time::SystemTime;
fn real_ml_dsa_quote() -> (PaymentQuote, [u8; 32]) {
let content = [7u8; 32];
let ml_dsa = MlDsa65::new();
let (pub_key, secret_key) = ml_dsa.generate_keypair().expect("keypair");
let pub_key_bytes = pub_key.as_bytes().to_vec();
let mut quote = PaymentQuote {
content: xor_name::XorName(content),
timestamp: SystemTime::now(),
price: Amount::from(42u64),
rewards_address: RewardsAddress::new([2u8; 20]),
pub_key: pub_key_bytes,
signature: vec![],
};
let msg = quote.bytes_for_sig();
let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
let sig = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
quote.signature = sig;
(quote, content)
}
#[test]
fn verify_quote_content_matches() {
let (quote, content) = real_ml_dsa_quote();
assert!(verify_quote_content("e, &content));
}
#[test]
fn verify_quote_content_mismatch() {
let (quote, _) = real_ml_dsa_quote();
let wrong = [0xFFu8; 32];
assert!(!verify_quote_content("e, &wrong));
}
#[test]
fn verify_quote_signature_real_keys_roundtrip() {
let (quote, _) = real_ml_dsa_quote();
assert!(verify_quote_signature("e));
}
#[test]
fn verify_quote_signature_tampered_signature_fails() {
let (mut quote, _) = real_ml_dsa_quote();
if let Some(byte) = quote.signature.first_mut() {
*byte ^= 0xFF;
}
assert!(!verify_quote_signature("e));
}
#[test]
fn verify_quote_signature_empty_pub_key_fails() {
let quote = PaymentQuote {
content: xor_name::XorName([0u8; 32]),
timestamp: SystemTime::now(),
price: Amount::from(1u64),
rewards_address: RewardsAddress::new([0u8; 20]),
pub_key: vec![],
signature: vec![],
};
assert!(!verify_quote_signature("e));
}
#[test]
fn verify_quote_signature_empty_signature_fails() {
let ml_dsa = MlDsa65::new();
let (pub_key, _sk) = ml_dsa.generate_keypair().expect("keypair");
let quote = PaymentQuote {
content: xor_name::XorName([0u8; 32]),
timestamp: SystemTime::now(),
price: Amount::from(1u64),
rewards_address: RewardsAddress::new([0u8; 20]),
pub_key: pub_key.as_bytes().to_vec(),
signature: vec![],
};
assert!(!verify_quote_signature("e));
}
fn real_ml_dsa_merkle_candidate() -> MerklePaymentCandidateNode {
let ml_dsa = MlDsa65::new();
let (pub_key, secret_key) = ml_dsa.generate_keypair().expect("keypair");
let price = Amount::from(1024u64);
let reward_address = RewardsAddress::new([3u8; 20]);
let merkle_payment_timestamp = 1_700_000_000u64;
let msg = MerklePaymentCandidateNode::bytes_to_sign(
&price,
&reward_address,
merkle_payment_timestamp,
);
let sk = MlDsaSecretKey::from_bytes(secret_key.as_bytes()).expect("sk");
let signature = ml_dsa.sign(&sk, &msg).expect("sign").as_bytes().to_vec();
MerklePaymentCandidateNode {
pub_key: pub_key.as_bytes().to_vec(),
price,
reward_address,
merkle_payment_timestamp,
signature,
}
}
#[test]
fn verify_merkle_candidate_real_keys_roundtrip() {
let candidate = real_ml_dsa_merkle_candidate();
assert!(verify_merkle_candidate_signature(&candidate));
}
#[test]
fn verify_merkle_candidate_tampered_fails() {
let mut candidate = real_ml_dsa_merkle_candidate();
if let Some(byte) = candidate.signature.first_mut() {
*byte ^= 0x55;
}
assert!(!verify_merkle_candidate_signature(&candidate));
}
#[test]
fn verify_merkle_candidate_empty_pub_key_fails() {
let mut candidate = real_ml_dsa_merkle_candidate();
candidate.pub_key = vec![];
assert!(!verify_merkle_candidate_signature(&candidate));
}
#[test]
fn verify_merkle_candidate_wrong_timestamp_fails() {
let mut candidate = real_ml_dsa_merkle_candidate();
candidate.merkle_payment_timestamp = candidate.merkle_payment_timestamp.wrapping_add(1);
assert!(!verify_merkle_candidate_signature(&candidate));
}
}