//! Blind Diffie-Hellman Key Exchange (BDHKE) for Cashu ecash. //! //! Implements NUT-00 cryptographic operations: //! - hash_to_curve: deterministic point derivation from secret //! - blind: create blinded message for mint signing //! - unblind: remove blinding factor from mint signature //! - verify: verify unblinded signature against mint pubkey use anyhow::{Context, Result}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; use sha2::{Digest, Sha256}; /// Domain separator for hash_to_curve per NUT-00 spec. const DOMAIN_SEPARATOR: &[u8] = b"Secp256k1_HashToCurve_Cashu_"; /// Hash a message to a secp256k1 curve point (NUT-00). /// /// Iteratively hashes `sha256(sha256(domain_separator || msg) || counter)` until /// the result is a valid x-coordinate on secp256k1. Prepends 0x02 to try as /// a compressed public key. pub fn hash_to_curve(message: &[u8]) -> Result { let msg_hash = { let mut hasher = Sha256::new(); hasher.update(DOMAIN_SEPARATOR); hasher.update(message); hasher.finalize() }; for counter in 0u32..65536 { let mut hasher = Sha256::new(); hasher.update(msg_hash); hasher.update(counter.to_le_bytes()); let hash = hasher.finalize(); // Try to construct a point: 0x02 || hash (compressed even-y format) let mut point_bytes = [0u8; 33]; point_bytes[0] = 0x02; point_bytes[1..].copy_from_slice(&hash); if let Ok(pk) = PublicKey::from_slice(&point_bytes) { return Ok(pk); } } Err(anyhow::anyhow!( "hash_to_curve: no valid point found after 65536 iterations" )) } /// Blinded message output from the client. pub struct BlindedMessage { /// The blinded point B_ = Y + r*G pub b_prime: PublicKey, /// The blinding factor (kept secret by client) pub r: SecretKey, /// The original secret pub secret: Vec, } /// Create a blinded message for the mint to sign. /// /// Given a secret, computes Y = hash_to_curve(secret), picks random r, /// and returns B_ = Y + r*G along with the blinding factor r. pub fn blind_message(secret: &[u8], blinding_factor: &SecretKey) -> Result { let secp = Secp256k1::new(); // Y = hash_to_curve(secret) let y = hash_to_curve(secret)?; // r*G let r_pub = PublicKey::from_secret_key(&secp, blinding_factor); // B_ = Y + r*G let b_prime = PublicKey::combine_keys(&[&y, &r_pub]) .context("Failed to compute blinded message B_ = Y + r*G")?; Ok(BlindedMessage { b_prime, r: *blinding_factor, secret: secret.to_vec(), }) } /// Unblind a mint's blind signature to get the real signature. /// /// Given C_ (blind signature from mint), r (our blinding factor), and K (mint's pubkey): /// C = C_ - r*K pub fn unblind_signature( c_prime: &PublicKey, r: &SecretKey, mint_pubkey: &PublicKey, ) -> Result { let secp = Secp256k1::new(); // Compute r*K let r_scalar = Scalar::from_be_bytes(r.secret_bytes()).expect("valid secret key is valid scalar"); let r_times_k = mint_pubkey .mul_tweak(&secp, &r_scalar) .context("Failed to compute r*K")?; // Negate to get -(r*K) let neg_r_times_k = r_times_k.negate(&secp); // C = C_ + (-(r*K)) = C_ - r*K let c = PublicKey::combine_keys(&[c_prime, &neg_r_times_k]) .context("Failed to compute C = C_ - r*K")?; Ok(c) } /// Verify that a proof (secret, C) is valid against a mint's public key K. /// /// Checks: C == k * hash_to_curve(secret) — but since we don't have k (the mint's /// private key), we verify by checking that the DLEQ proof is valid, or by /// attempting to swap the token at the mint. This function provides a basic /// structural check that the proof components are well-formed. pub fn verify_proof_structure(secret: &[u8], c: &PublicKey) -> Result { // Verify that hash_to_curve(secret) produces a valid point let _y = hash_to_curve(secret)?; // Verify C is a valid public key (already guaranteed by type, but check non-identity) let c_bytes = c.serialize(); if c_bytes.iter().all(|&b| b == 0) { return Ok(false); } Ok(true) } /// Construct the secret string for a Cashu proof. /// NUT-10 defines secret as a JSON array: ["P2PK", {nonce, data, tags}] /// For basic (non-P2PK) proofs, the secret is just a random hex string. pub fn generate_secret() -> Vec { let random_bytes: [u8; 32] = rand::random(); hex::encode(random_bytes).into_bytes() } /// Generate a random blinding factor. pub fn random_blinding_factor() -> SecretKey { let mut rng = rand::thread_rng(); SecretKey::new(&mut rng) } #[cfg(test)] mod tests { use super::*; #[test] fn test_hash_to_curve_deterministic() { let msg = b"test_message"; let p1 = hash_to_curve(msg).unwrap(); let p2 = hash_to_curve(msg).unwrap(); assert_eq!(p1, p2); } #[test] fn test_hash_to_curve_different_messages() { let p1 = hash_to_curve(b"message_a").unwrap(); let p2 = hash_to_curve(b"message_b").unwrap(); assert_ne!(p1, p2); } #[test] fn test_blind_unblind_roundtrip() { let secp = Secp256k1::new(); let secret = b"test_secret"; let r = random_blinding_factor(); // Simulate mint: k is mint's private key, K = k*G is public key let k = SecretKey::new(&mut rand::thread_rng()); let k_pub = PublicKey::from_secret_key(&secp, &k); // Client blinds let blinded = blind_message(secret, &r).unwrap(); // Mint signs: C_ = k * B_ let k_scalar = Scalar::from_be_bytes(k.secret_bytes()).unwrap(); let c_prime = blinded.b_prime.mul_tweak(&secp, &k_scalar).unwrap(); // Client unblinds: C = C_ - r*K let c = unblind_signature(&c_prime, &r, &k_pub).unwrap(); // Verify: C should equal k * hash_to_curve(secret) let y = hash_to_curve(secret).unwrap(); let expected_c = y.mul_tweak(&secp, &k_scalar).unwrap(); assert_eq!(c, expected_c); } #[test] fn test_generate_secret_length() { let secret = generate_secret(); // 32 bytes hex-encoded = 64 chars assert_eq!(secret.len(), 64); } #[test] fn test_generate_secret_unique() { let s1 = generate_secret(); let s2 = generate_secret(); assert_ne!(s1, s2); } #[test] fn test_verify_proof_structure_valid() { let secret = generate_secret(); let secp = Secp256k1::new(); let k = SecretKey::new(&mut rand::thread_rng()); let y = hash_to_curve(&secret).unwrap(); let k_scalar = Scalar::from_be_bytes(k.secret_bytes()).unwrap(); let c = y.mul_tweak(&secp, &k_scalar).unwrap(); assert!(verify_proof_structure(&secret, &c).unwrap()); } }