//! Crate-level tests and fuzzing operations. //! //! # Test vectors //! //! The RFC 8032 Ed25519, RFC 7748 X25519, ZIP 215, and Project Wycheproof Ed25519/X25519 //! vectors in `cryptography/curve25519/src/test/vectors.rs` are generated by //! `cryptography/curve25519/generate_test_vectors.py`. From the repository root, regenerate //! them with: //! //! ```text //! python3 cryptography/curve25519/generate_test_vectors.py //! ``` //! //! The generator downloads pinned source documents, verifies their SHA-256 digests, validates //! the vectors, and rewrites the Rust fixture. Network access is required. To check that the //! fixture is current without modifying it, run: //! //! ```text //! python3 cryptography/curve25519/generate_test_vectors.py --check //! ``` #[cfg(test)] mod vectors; use crate::{ key_exchange::{PublicKey as ExchangePublicKey, SecretKey}, signing::{BatchVerifier, Signature, SigningKey, VerifyingKey}, }; use arbitrary::{Arbitrary, Unstructured}; use commonware_codec::DecodeExt as _; use commonware_formatting::hex; use commonware_math::algebra::Random as _; use commonware_parallel::Sequential; use commonware_utils::{FuzzRng, union_unique}; use ed25519_consensus::SigningKey as ConsensusSigningKey; const SCALAR_ORDER: [u8; 32] = [ 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, ]; // Bias signing key material and key-exchange RNG seeds toward low-entropy patterns. const INTERESTING_SECRET_BYTES: [[u8; 32]; 4] = [ [0; 32], [0xff; 32], hex!("0x0100000000000000000000000000000000000000000000000000000000000000"), hex!("0x0000000000000000000000000000000000000000000000000000000000000080"), ]; // Include every low-order coordinate on the curve and its twist, non-canonical aliases of 0 and // 1, the basepoint with both possible high bits, and the maximal encoding. const INTERESTING_X25519_PUBLIC_KEYS: [[u8; 32]; 10] = [ [0; 32], hex!("0x0100000000000000000000000000000000000000000000000000000000000000"), hex!("0xe0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800"), hex!("0x5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157"), hex!("0xecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), hex!("0xedffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), hex!("0xeeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), hex!("0x0900000000000000000000000000000000000000000000000000000000000000"), hex!("0x0900000000000000000000000000000000000000000000000000000000000080"), [0xff; 32], ]; // These are the canonical and non-canonical encodings of the two points with x = 0. They are // low order, so a signature with low-order A and R and s = 0 satisfies the cofactored ZIP215 // verification equation for every message. const LOW_ORDER_ENCODINGS: [[u8; 32]; 6] = [ hex!("0x0100000000000000000000000000000000000000000000000000000000000000"), hex!("0x0100000000000000000000000000000000000000000000000000000000000080"), hex!("0xecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), hex!("0xecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), hex!("0xeeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), hex!("0xeeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), ]; // Bias payloads around fixed Ed25519 widths, SHA-512 boundaries, and length-prefix transitions. const INTERESTING_LENGTHS: [usize; 18] = [ 0, 1, 31, 32, 47, 48, 55, 56, 63, 64, 65, 111, 112, 127, 128, 129, 255, 256, ]; // Cover both sides of the four-coefficient and eight-point SIMD blocks, plus larger compositions. const INTERESTING_BATCH_SIZES: [usize; 17] = [0, 1, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64]; // Debug verification is expensive. Keep the unit minifuzz bounded while the fuzz target retains // all of the larger structural boundaries above. const MAX_BATCH_SIZE: usize = if cfg!(test) { 17 } else { 64 }; const INTERESTING_BATCH_SIZE_COUNT: usize = if cfg!(test) { 12 } else { 17 }; fn arbitrary_length(u: &mut Unstructured<'_>) -> arbitrary::Result { if u.ratio(3, 4)? { Ok(*u.choose(&INTERESTING_LENGTHS)?) } else { u.int_in_range(0..=256) } } fn arbitrary_bytes(u: &mut Unstructured<'_>) -> arbitrary::Result> { let len = arbitrary_length(u)?; Ok(match u.int_in_range(0u8..=3)? { 0 => vec![0; len], 1 => vec![u.arbitrary()?; len], 2 => (0..len).map(|i| i as u8).collect(), _ => u.bytes(len)?.to_vec(), }) } fn arbitrary_batch_size(u: &mut Unstructured<'_>) -> arbitrary::Result { if u.ratio(3, 4)? { Ok(*u.choose(&INTERESTING_BATCH_SIZES[..INTERESTING_BATCH_SIZE_COUNT])?) } else { u.int_in_range(0..=MAX_BATCH_SIZE) } } fn arbitrary_item_index(u: &mut Unstructured<'_>, len: usize) -> arbitrary::Result { let last = len - 1; let interesting = [ 0, last, len / 2, 3.min(last), 4.min(last), 7.min(last), 8.min(last), ]; Ok(*u.choose(&interesting)?) } fn mutate_bytes(bytes: &mut Vec, u: &mut Unstructured<'_>) -> arbitrary::Result<()> { if bytes.is_empty() || u.ratio(1, 4)? { bytes.push(u.arbitrary()?); } else { let index = u.int_in_range(0..=bytes.len() - 1)?; let bit = u.int_in_range(0u8..=7)?; bytes[index] ^= 1u8 << bit; } Ok(()) } #[derive(Clone, Copy, Debug)] struct SecretBytes([u8; 32]); impl Arbitrary<'_> for SecretBytes { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { let bytes = if u.ratio(3, 4)? { *u.choose(&INTERESTING_SECRET_BYTES)? } else { u.arbitrary()? }; Ok(Self(bytes)) } } #[derive(Clone, Copy, Debug)] struct X25519PublicKey([u8; 32]); impl Arbitrary<'_> for X25519PublicKey { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { let bytes = if u.ratio(3, 4)? { *u.choose(&INTERESTING_X25519_PUBLIC_KEYS)? } else { u.arbitrary()? }; Ok(Self(bytes)) } } #[derive(Clone, Copy, Debug)] struct EncodedPoint([u8; 32]); impl Arbitrary<'_> for EncodedPoint { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { let bytes = match u.int_in_range(0u8..=9)? { 0..=3 => *u.choose(&LOW_ORDER_ENCODINGS)?, 4 => { let mut bytes = [0; 32]; bytes[0] = 2; bytes } 5 => [0; 32], 6 => [0xff; 32], 7 => { let SecretBytes(seed) = u.arbitrary()?; SigningKey::decode(seed.as_slice()) .unwrap() .verifying_key() .as_ref() .try_into() .unwrap() } _ => u.arbitrary()?, }; Ok(Self(bytes)) } } #[derive(Clone, Copy, Debug)] struct EncodedScalar([u8; 32]); impl Arbitrary<'_> for EncodedScalar { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { let bytes = match u.int_in_range(0u8..=7)? { 0 => [0; 32], 1 => { let mut bytes = [0; 32]; bytes[0] = 1; bytes } 2 => { let mut bytes = SCALAR_ORDER; bytes[0] -= 1; bytes } 3 => SCALAR_ORDER, 4 => { let mut bytes = SCALAR_ORDER; bytes[0] += 1; bytes } 5 => [0xff; 32], _ => u.arbitrary()?, }; Ok(Self(bytes)) } } #[derive(Clone, Debug)] struct Payload { namespace: Vec, message: Vec, } impl Arbitrary<'_> for Payload { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { Ok(Self { namespace: arbitrary_bytes(u)?, message: arbitrary_bytes(u)?, }) } } #[derive(Debug, Arbitrary)] struct Signing { seed: SecretBytes, payload: Payload, } impl Signing { fn run(self) { let SecretBytes(seed) = self.seed; let signing_key = SigningKey::decode(seed.as_slice()).unwrap(); let consensus_key = ConsensusSigningKey::from(seed); assert_eq!( signing_key.verifying_key().as_ref(), consensus_key.verification_key().to_bytes(), "signing: {self:#?}", ); let signature = signing_key.sign(&self.payload.namespace, &self.payload.message); let consensus_signature = consensus_key.sign(&union_unique( &self.payload.namespace, &self.payload.message, )); assert_eq!( signature.as_ref(), consensus_signature.to_bytes(), "signing: {self:#?}", ); } } #[derive(Debug, Arbitrary)] struct KeyExchange { secret: SecretBytes, public_key: X25519PublicKey, } impl KeyExchange { fn run(self) { let SecretBytes(secret_bytes) = self.secret; let X25519PublicKey(public_key_bytes) = self.public_key; let mut rng = FuzzRng::new(secret_bytes.to_vec()); let secret_key = SecretKey::random(&mut rng); let mut dalek_rng = FuzzRng::new(secret_bytes.to_vec()); let dalek_secret = x25519_dalek::EphemeralSecret::random_from_rng(&mut dalek_rng); assert_eq!( secret_key.public_key().as_ref(), x25519_dalek::PublicKey::from(&dalek_secret).as_bytes(), "key exchange: {self:#?}", ); let public_key = ExchangePublicKey::decode(public_key_bytes.as_slice()).unwrap(); let shared = secret_key.exchange(&public_key); let dalek_shared = dalek_secret .diffie_hellman(&x25519_dalek::PublicKey::from(public_key_bytes)) .to_bytes(); let shared = shared.as_ref().map(|shared| *shared.as_bytes()); let dalek_shared = (dalek_shared != [0; 32]).then_some(dalek_shared); assert_eq!(shared, dalek_shared, "key exchange: {self:#?}"); } } #[derive(Clone, Debug)] struct Item { namespace: Vec, message: Vec, verifying_key: VerifyingKey, signature: Signature, } impl Item { fn signed(u: &mut Unstructured<'_>, seed: Option) -> arbitrary::Result { let Payload { namespace, message } = u.arbitrary()?; let SecretBytes(seed) = match seed { Some(seed) => seed, None => u.arbitrary()?, }; let signing_key = SigningKey::decode(seed.as_slice()).unwrap(); let verifying_key = signing_key.verifying_key(); let signature = signing_key.sign(&namespace, &message); Ok(Self { namespace, message, verifying_key, signature, }) } fn low_order(u: &mut Unstructured<'_>) -> arbitrary::Result { let Payload { namespace, message } = u.arbitrary()?; let bytes = u.choose(&LOW_ORDER_ENCODINGS)?; let verifying_key = VerifyingKey::decode(bytes.as_slice()).unwrap(); let mut signature = [0; 64]; signature[..32].copy_from_slice(u.choose(&LOW_ORDER_ENCODINGS)?); Ok(Self { namespace, message, verifying_key, signature: Signature::decode(signature.as_slice()).unwrap(), }) } fn valid(u: &mut Unstructured<'_>, seed: Option) -> arbitrary::Result { if seed.is_none() && u.ratio(1, 8)? { Self::low_order(u) } else { Self::signed(u, seed) } } fn raw(u: &mut Unstructured<'_>) -> arbitrary::Result { let Payload { namespace, message } = u.arbitrary()?; let EncodedPoint(verifying_key) = u.arbitrary()?; let EncodedPoint(r) = u.arbitrary()?; let EncodedScalar(s) = u.arbitrary()?; let mut signature = [0; 64]; signature[..32].copy_from_slice(&r); signature[32..].copy_from_slice(&s); Ok(Self { namespace, message, verifying_key: VerifyingKey::decode(verifying_key.as_slice()).unwrap(), signature: Signature::decode(signature.as_slice()).unwrap(), }) } fn invalidate(mut self, u: &mut Unstructured<'_>) -> arbitrary::Result { match u.int_in_range(0u8..=5)? { 0 => mutate_bytes(&mut self.namespace, u)?, 1 => mutate_bytes(&mut self.message, u)?, 2 => { let EncodedPoint(bytes) = u.arbitrary()?; self.verifying_key = VerifyingKey::decode(bytes.as_slice()).unwrap(); } 3 => { let EncodedPoint(r) = u.arbitrary()?; let mut signature: [u8; 64] = self.signature.as_ref().try_into().unwrap(); signature[..32].copy_from_slice(&r); self.signature = Signature::decode(signature.as_slice()).unwrap(); } 4 => { let EncodedScalar(s) = u.arbitrary()?; let mut signature: [u8; 64] = self.signature.as_ref().try_into().unwrap(); signature[32..].copy_from_slice(&s); self.signature = Signature::decode(signature.as_slice()).unwrap(); } _ => self.make_scalar_noncanonical(), } // A targeted mutation can occasionally preserve a signature (for example, by selecting // its original key). Fall back to the first non-canonical scalar so this path always // exercises rejection. if self.verify() { self.make_scalar_noncanonical(); } Ok(self) } fn make_scalar_noncanonical(&mut self) { let mut signature: [u8; 64] = self.signature.as_ref().try_into().unwrap(); signature[32..].copy_from_slice(&SCALAR_ORDER); self.signature = Signature::decode(signature.as_slice()).unwrap(); } fn verify(&self) -> bool { self.verifying_key .verify(&self.namespace, &self.message, &self.signature) } } impl Arbitrary<'_> for Item { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { match u.int_in_range(0u8..=9)? { 0..=3 => Self::signed(u, None), 4 => Self::low_order(u), 5..=7 => Self::signed(u, None)?.invalidate(u), _ => Self::raw(u), } } } #[derive(Debug)] struct Batch { rng_seed: SecretBytes, items: Vec, } impl Arbitrary<'_> for Batch { fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result { let len = arbitrary_batch_size(u)?; let rng_seed = u.arbitrary()?; let shared_seed = u.arbitrary()?; let items = match u.int_in_range(0u8..=5)? { 0 => (0..len) .map(|_| Item::valid(u, None)) .collect::>()?, 1 => (0..len) .map(|_| Item::signed(u, Some(shared_seed))) .collect::>()?, 2 if len != 0 => vec![Item::valid(u, None)?; len], 2 => Vec::new(), 3 => { let mut items = (0..len) .map(|_| Item::valid(u, None)) .collect::>>()?; if len != 0 { let index = arbitrary_item_index(u, len)?; items[index] = items[index].clone().invalidate(u)?; } items } 4 => { let mut items = (0..len) .map(|_| Item::signed(u, Some(shared_seed))) .collect::>>()?; if len != 0 { let index = arbitrary_item_index(u, len)?; items[index] = items[index].clone().invalidate(u)?; } items } _ => (0..len) .map(|_| u.arbitrary()) .collect::>()?, }; Ok(Self { rng_seed, items }) } } impl Batch { fn run(self) { let expected = !self.items.is_empty() && self.items.iter().all(Item::verify); let mut batch = BatchVerifier::new(self.items.len()); for item in &self.items { batch.add( &item.namespace, &item.message, &item.verifying_key, &item.signature, ); } let SecretBytes(rng_seed) = self.rng_seed; let actual = batch.verify(&mut FuzzRng::new(rng_seed.to_vec()), &Sequential); assert_eq!(actual, expected, "batch: {self:#?}"); } } /// Fuzzing operations for public API invariants. pub mod fuzz { use super::{Batch, KeyExchange, Signing}; use arbitrary::{Arbitrary, Unstructured}; /// A public API fuzzing operation. #[derive(Debug, Arbitrary)] pub enum Plan { /// Check that batch verification agrees with verifying every item individually. BatchMatchesIndividual, /// Check that signing agrees with `ed25519-consensus`. SigningMatchesConsensus, /// Check that key exchange agrees with `x25519-dalek`. KeyExchangeMatchesDalek, } impl Plan { /// Runs the fuzzing operation using the remaining input. pub fn run(self, u: &mut Unstructured<'_>) -> arbitrary::Result<()> { match self { Self::BatchMatchesIndividual => u.arbitrary::()?.run(), Self::SigningMatchesConsensus => u.arbitrary::()?.run(), Self::KeyExchangeMatchesDalek => u.arbitrary::()?.run(), } Ok(()) } } #[cfg(test)] #[test] fn minifuzz_batch_matches_individual() { commonware_invariants::minifuzz::Builder::default() .with_seed(0) .with_search_limit(32) .test(|u| Plan::BatchMatchesIndividual.run(u)); } #[cfg(test)] #[test] fn minifuzz_signing_matches_consensus() { commonware_invariants::minifuzz::Builder::default() .with_seed(0) .with_search_limit(100) .test(|u| Plan::SigningMatchesConsensus.run(u)); } #[cfg(test)] #[test] fn minifuzz_key_exchange_matches_dalek() { commonware_invariants::minifuzz::Builder::default() .with_seed(0) .with_search_limit(100) .test(|u| Plan::KeyExchangeMatchesDalek.run(u)); } } #[cfg(test)] mod tests { use super::{ ExchangePublicKey, Signature, VerifyingKey, vectors::{ RFC7748_X25519, RFC7748_X25519_DIFFIE_HELLMAN, RFC8032_ED25519, WYCHEPROOF_ED25519, WYCHEPROOF_X25519, ZIP215_POINTS, }, }; use crate::{key_exchange::SecretKey, signing::SigningKey}; use commonware_codec::DecodeExt as _; #[test] fn rfc8032_ed25519_vectors() { for vector in RFC8032_ED25519 { let signing_key = SigningKey::decode(vector.secret_key.as_slice()).unwrap(); assert_eq!( signing_key.verifying_key().as_ref(), vector.public_key, "RFC 8032 test {} public key", vector.name, ); assert_eq!( signing_key.sign_raw(vector.message).as_ref(), vector.signature, "RFC 8032 test {} signature", vector.name, ); } } #[test] fn wycheproof_ed25519_vectors() { for vector in WYCHEPROOF_ED25519 { let verifying_key = VerifyingKey::decode(vector.public_key.as_slice()).unwrap(); let valid = Signature::decode(vector.signature) .is_ok_and(|signature| verifying_key.verify_raw(vector.message, &signature)); assert_eq!( valid, vector.valid_zip215, "Wycheproof Ed25519 test {}", vector.tc_id, ); } } #[test] fn rfc7748_x25519_vectors() { for vector in RFC7748_X25519 { let public_key = ExchangePublicKey::decode(vector.u_coordinate.as_slice()).unwrap(); let shared_secret = SecretKey::from_raw(vector.scalar) .exchange(&public_key) .expect("RFC 7748 output is contributory"); assert_eq!(shared_secret.as_bytes(), &vector.output); } } #[test] fn rfc7748_x25519_diffie_hellman_vector() { let vector = &RFC7748_X25519_DIFFIE_HELLMAN; let alice = SecretKey::from_raw(vector.alice_secret); let bob = SecretKey::from_raw(vector.bob_secret); assert_eq!(alice.public_key().as_ref(), vector.alice_public); assert_eq!(bob.public_key().as_ref(), vector.bob_public); let bob_public = ExchangePublicKey::decode(vector.bob_public.as_slice()).unwrap(); let alice_public = ExchangePublicKey::decode(vector.alice_public.as_slice()).unwrap(); let alice_shared = alice .exchange(&bob_public) .expect("RFC 7748 Bob public key is contributory"); let bob_shared = bob .exchange(&alice_public) .expect("RFC 7748 Alice public key is contributory"); assert_eq!(alice_shared.as_bytes(), &vector.shared_secret); assert_eq!(bob_shared.as_bytes(), &vector.shared_secret); } #[test] fn wycheproof_x25519_vectors() { for vector in WYCHEPROOF_X25519 { let public_key = ExchangePublicKey::decode(vector.public_key.as_slice()).unwrap(); let shared_secret = SecretKey::from_raw(vector.private_key) .exchange(&public_key) .map(|shared_secret| *shared_secret.as_bytes()); assert_eq!( shared_secret, vector.shared_secret, "Wycheproof X25519 test {}", vector.tc_id, ); } } #[test] fn zip215_verification_vectors() { const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_CURVE25519_ZIP215_VECTORS"; let message = b"Zcash"; // These are the 196 ZIP215 test vectors: every pairing of the eight canonical // low-order encodings and their six non-canonical aliases. With s = 0, each pair // satisfies the cofactored verification equation for every message. for public_key_bytes in ZIP215_POINTS { for r_bytes in ZIP215_POINTS { let verifying_key = VerifyingKey::decode(public_key_bytes.as_slice()).unwrap(); let mut signature_bytes = [0u8; 64]; signature_bytes[..32].copy_from_slice(&r_bytes); let signature = Signature::decode(signature_bytes.as_slice()).unwrap(); assert!( verifying_key.verify(NAMESPACE, message, &signature), "ZIP215 vector failed for A={public_key_bytes:?}, R={r_bytes:?}", ); } } } }