#![allow( clippy::unwrap_used, clippy::indexing_slicing, clippy::arithmetic_side_effects )] use super::*; use alloc::{vec, vec::Vec}; use serde_json::Value; fn header(seed: &[u8; 32]) -> Header { let public_key = public_key_from_seed(seed).unwrap(); let descriptor = Descriptor { version: VERSION, algorithm: ALGORITHM, initial_commitment: commitment(&public_key), }; Header { account: account_id(&descriptor), version: VERSION, algorithm: ALGORITHM, generation: 0, public_key, next_commitment: commitment(&public_key_from_seed(&[22; 32]).unwrap()), } } #[test] fn exact_wire_layout_and_strict_version_dispatch() { let header = header(&[11; 32]); let signed = header.sign(&[11; 32], b"payload", &[0; 32]).unwrap(); let encoded = signed.encode(); assert_eq!(Header::ENCODED_LEN, header.encode().len()); assert_eq!(SignerAddress::ENCODED_LEN, encoded.len()); assert_eq!(&encoded[..32], &header.account); assert_eq!(&encoded[32..34], &[VERSION, ALGORITHM]); assert_eq!(&encoded[34..42], &header.generation.to_le_bytes()); assert_eq!(&encoded[42..1994], &header.public_key); assert_eq!(&encoded[1994..2058], &header.next_commitment); assert_eq!(&encoded[2058..], &signed.signature); assert_eq!(SignerAddress::from_bytes(&encoded).unwrap(), signed); assert!(SignerAddress::from_bytes(&encoded[..5366]).is_err()); let mut overlong = encoded.clone(); overlong.push(0); assert!(SignerAddress::from_bytes(&overlong).is_err()); for (offset, value) in [(32, 1), (32, 3), (33, 1), (33, 3), (33, 6), (33, 255)] { let mut bad = encoded.clone(); bad[offset] = value; assert!(SignerAddress::from_bytes(&bad).is_err()); } assert_eq!(codec::Compact(5367u32).encode(), [0xdd, 0x53]); assert!(CARRIER_SIGNATURE.iter().all(|byte| *byte == 0x4d)); assert_eq!(SignerAddress::max_encoded_len(), 5367); assert_eq!(Header::max_encoded_len(), 2058); assert_eq!(Descriptor::max_encoded_len(), 66); } #[test] fn descriptors_have_separate_domains_and_no_implicit_version_upgrade() { let descriptor = Descriptor { version: VERSION, algorithm: ALGORITHM, initial_commitment: [7; 64], }; assert_eq!(descriptor.encode().len(), Descriptor::ENCODED_LEN); assert_eq!( Descriptor::from_bytes(&descriptor.encode()).unwrap(), descriptor ); let v1 = crate::Descriptor { version: 1, scheme: crate::Scheme::Ed25519, initial_commitment: [7; 32], }; assert!(Descriptor::from_bytes(&v1.encode()).is_err()); for (version, algorithm, commitment) in [(1, 4, [7; 64]), (2, 3, [7; 64]), (2, 4, [0; 64])] { let invalid = Descriptor { version, algorithm, initial_commitment: commitment, }; assert!(!invalid.is_supported()); assert!(Descriptor::from_bytes(&invalid.encode()).is_err()); } let mut trailing = descriptor.encode(); trailing.push(0); assert!(Descriptor::from_bytes(&trailing).is_err()); assert_ne!(account_id(&descriptor), crate::account_id(&v1)); } fn stateless_header() -> ([u8; 32], Header) { for value in 0u8..=255 { let seed = [value; 32]; let public_key = public_key_from_seed(&seed).unwrap(); let current = commitment(&public_key); let descriptor = Descriptor { version: VERSION, algorithm: FIXED_ALGORITHM, initial_commitment: current, }; let account = account_id(&descriptor); if rejects_legacy_public_keys(&account) { return ( seed, Header { account, version: VERSION, algorithm: FIXED_ALGORITHM, generation: 0, public_key, next_commitment: current, }, ); } } panic!("test seed search exhausted"); } #[test] fn every_encoded_proof_byte_is_authenticated_for_both_mldsa_profiles() { let payload = b"release validation: canonical proof mutation sweep"; for (seed, header) in [([11; 32], header(&[11; 32])), stateless_header()] { let proof = header.sign(&seed, payload, &[0x63; 32]).unwrap(); assert!(proof.verify(payload)); let mut encoded = proof.encode(); for offset in 0..encoded.len() { encoded[offset] ^= 1; if let Ok(mutated) = SignerAddress::from_bytes(&encoded) { assert!( !mutated.verify(payload), "profile {} accepted mutated proof byte {offset}", header.algorithm ); } encoded[offset] ^= 1; } assert_eq!(SignerAddress::from_bytes(&encoded).unwrap(), proof); } } #[test] fn stateless_authority_is_bound_to_the_key_profile_and_complete_payload() { let (seed, header) = stateless_header(); assert!(header.is_valid_stateless()); let descriptor = Descriptor { version: VERSION, algorithm: FIXED_ALGORITHM, initial_commitment: commitment(&header.public_key), }; assert!(descriptor.is_stateless()); assert!(!descriptor.is_rotating()); assert_eq!( Descriptor::from_bytes(&descriptor.encode()).unwrap(), descriptor ); let payload = (vec![17u8; 1024], 19u64, [23u8; 32]); let signed = header.sign(&seed, &payload.encode(), &[0; 32]).unwrap(); assert!(signed.verify_encoded(&payload)); assert!(signed.verify(&payload.encode())); assert_eq!(SignerAddress::from_bytes(&signed.encode()).unwrap(), signed); assert_eq!(signed.encode().len(), SignerAddress::ENCODED_LEN); // Actual signatures on invalid authority must fail, not merely corrupted // signatures whose cryptographic check would have failed independently. let mut invalid = Vec::new(); for generation in [1, u64::MAX] { let mut changed = header.clone(); changed.generation = generation; invalid.push(changed); } for next in [[0; COMMITMENT_LEN], [42; COMMITMENT_LEN]] { let mut changed = header.clone(); changed.next_commitment = next; invalid.push(changed); } for algorithm in [ALGORITHM, 6] { let mut changed = header.clone(); changed.account = account_id(&Descriptor { algorithm, ..descriptor }); invalid.push(changed); } let mut changed = header.clone(); changed.account = [0; 32]; invalid.push(changed); for changed in invalid { assert!(!changed.is_valid_stateless()); assert!(changed.sign(&seed, &payload.encode(), &[0; 32]).is_err()); let mut transcript = TRANSACTION_DOMAIN.to_vec(); changed.encode_to(&mut transcript); transcript.extend_from_slice(&payload.encode()); let mut malformed = signed.clone(); malformed.account = changed.account; malformed.generation = changed.generation; malformed.next_commitment = changed.next_commitment; malformed.signature = sign_message(&seed, &transcript, TRANSACTION_CONTEXT, &[0; 32]).unwrap(); assert!(!malformed.verify_encoded(&payload)); } for changed_payload in [ (vec![18u8; 1024], 19u64, [23u8; 32]), (vec![17u8; 1024], 20u64, [23u8; 32]), (vec![17u8; 1024], 19u64, [24u8; 32]), ] { assert!(!signed.verify_encoded(&changed_payload)); } for algorithm in [ALGORITHM, 6] { let mut changed = signed.clone(); changed.algorithm = algorithm; assert!(!changed.verify_encoded(&payload)); } } #[test] fn stateless_matching_key_cannot_authorize_an_account_with_a_legacy_fallback() { for value in 0u8..=255 { let seed = [value; 32]; let public_key = public_key_from_seed(&seed).unwrap(); let current = commitment(&public_key); let descriptor = Descriptor { version: VERSION, algorithm: FIXED_ALGORITHM, initial_commitment: current, }; let account = account_id(&descriptor); if rejects_legacy_public_keys(&account) { continue; } let header = Header { account, version: VERSION, algorithm: FIXED_ALGORITHM, generation: 0, public_key, next_commitment: current, }; assert!(!header.is_valid_stateless()); assert!(Descriptor::from_bytes(&descriptor.encode()).is_err()); assert!(header.sign(&seed, b"payload", &[0; 32]).is_err()); let mut transcript = TRANSACTION_DOMAIN.to_vec(); header.encode_to(&mut transcript); transcript.extend_from_slice(b"payload"); let signature = sign_message(&seed, &transcript, TRANSACTION_CONTEXT, &[0; 32]).unwrap(); assert!(verify_message( &public_key, &transcript, TRANSACTION_CONTEXT, &signature )); let proof = SignerAddress { account, version: VERSION, algorithm: FIXED_ALGORITHM, generation: 0, public_key, next_commitment: current, signature, }; assert!(!proof.verify(b"payload")); return; } panic!("test seed search exhausted"); } #[cfg(all(feature = "std", feature = "verify"))] #[test] fn stateless_accounts_fail_all_pinned_legacy_curve_host_verifiers() { use sp_core::{Pair, ed25519, sr25519}; let message = b"legacy signature fallback"; let ed = ed25519::Pair::from_seed(&[17; 32]); let sr = sr25519::Pair::from_seed(&[29; 32]); let ed_signature = ed.sign(message); let sr_signature = sr.sign(message); assert!(!rejects_legacy_public_keys(&ed.public().0)); assert!(!rejects_legacy_public_keys(&sr.public().0)); assert!(sp_io::crypto::ed25519_verify( &ed_signature, message, &ed.public() )); assert!(sp_io::crypto::sr25519_verify( &sr_signature, message, &sr.public() )); let (_, header) = stateless_header(); let mut accounts = vec![header.account]; for value in 0u8..128 { let account = sp_core::hashing::blake2_256(&[value]); if rejects_legacy_public_keys(&account) { accounts.push(account); } } assert!(accounts.len() > 32); for account in accounts { let ed_public = ed25519::Public::from_raw(account); let sr_public = sr25519::Public::from_raw(account); assert!(!ed25519::Pair::verify(&ed_signature, message, &ed_public)); assert!(!sr25519::Pair::verify(&sr_signature, message, &sr_public)); assert!(!sr25519::Pair::verify_deprecated( &sr_signature, message, &sr_public )); let mut externalities = sp_io::TestExternalities::default(); externalities.execute_with(|| { assert!(!sp_io::crypto::ed25519_verify( &ed_signature, message, &ed_public )); assert!(!sp_io::crypto::sr25519_verify( &sr_signature, message, &sr_public )); }); externalities.register_extension(sp_io::UseDalekExt); externalities.execute_with(|| { assert!(!sp_io::crypto::ed25519_verify( &ed_signature, message, &ed_public )); }); } // ZIP-215 accepts unreduced Edwards encodings and small-order keys. They // must never be mistaken for addresses that lack a legacy-key fallback. for first in [0xed, 0xee] { for sign in [0x7f, 0xff] { let mut noncanonical = [0xff; 32]; noncanonical[0] = first; noncanonical[31] = sign; assert!(!rejects_legacy_public_keys(&noncanonical)); let mut signature = [0; 64]; signature[0] = 1; // R = identity, s = zero. assert!(sp_io::crypto::ed25519_verify( &ed25519::Signature::from_raw(signature), message, &ed25519::Public::from_raw(noncanonical), )); } } } #[test] fn pure_external_and_streamed_full_transcripts_agree() { let seed = [11; 32]; let header = header(&seed); let payload = (vec![23u8; 1024], 17u64, [33u8; 32]); let payload_bytes = payload.encode(); let signed = header.sign(&seed, &payload_bytes, &[44; 32]).unwrap(); assert!(signed.verify(&payload_bytes)); assert!(signed.verify_encoded(&payload)); let mut transcript = TRANSACTION_DOMAIN.to_vec(); header.encode_to(&mut transcript); transcript.extend_from_slice(&payload_bytes); let public = ml_dsa_65::PublicKey::try_from_bytes(header.public_key).unwrap(); assert!(public.verify(&transcript, &signed.signature, TRANSACTION_CONTEXT)); assert!(!public.verify(&transcript, &signed.signature, b"bittensor/ms/v2/order")); assert!(!signed.verify(&sp_core::hashing::blake2_256(&payload_bytes))); // Hedged and deterministic signatures share exactly the same verifier. let deterministic = header.sign(&seed, &payload_bytes, &[0; 32]).unwrap(); assert!(deterministic.verify(&payload_bytes)); assert_ne!(signed.signature, deterministic.signature); assert!(header.sign(&[99; 32], &payload_bytes, &[0; 32]).is_err()); } #[test] fn every_header_field_and_full_payload_are_authenticated() { let signed = header(&[11; 32]) .sign(&[11; 32], &[42; 300], &[0; 32]) .unwrap(); for offset in [0, 31, 32, 33, 34, 41, 42, 1993, 1994, 2057, 2058, 5366] { let mut bytes = signed.encode(); bytes[offset] ^= 1; if let Ok(mutated) = SignerAddress::from_bytes(&bytes) { assert!(!mutated.verify(&[42; 300]), "mutation at {offset}"); } } let mut changed = [42u8; 300]; changed[299] ^= 1; assert!(!signed.verify(&changed)); assert!(!signed.verify(&[42; 299])); } #[test] fn message_contexts_and_lengths_are_checked() { let seed = [11; 32]; let public_key = public_key_from_seed(&seed).unwrap(); let signature = sign_message(&seed, b"message", b"order", &[0; 32]).unwrap(); assert!(verify_message( &public_key, b"message", b"order", &signature )); assert!(!verify_message( &public_key, b"message", b"message", &signature )); assert!(!verify_message( &public_key, b"changed", b"order", &signature )); assert!(!verify_message( &public_key, b"message", &[0; 256], &signature )); assert!(sign_message(&seed, b"message", &[0; 256], &[0; 32]).is_err()); } #[test] fn streaming_size_limit_fails_closed_without_payload_allocation() { struct Oversized; impl Encode for Oversized { fn encode_to(&self, dest: &mut T) { for _ in 0..=(MAX_PAYLOAD_LEN / 4096) { dest.write(&[0; 4096]); } } } assert!(header(&[11; 32]).representative(&Oversized).is_none()); } fn decode_hex(value: &Value) -> [u8; N] { hex::decode(value.as_str().unwrap()) .unwrap() .try_into() .unwrap() } #[test] #[allow(deprecated)] fn pinned_nist_keygen_sign_and_positive_negative_verification_vectors() { let vectors: Value = serde_json::from_str(include_str!("../tests/fixtures/nist-mldsa65.json")).unwrap(); for group in vectors["keygen"].as_array().unwrap() { for test in group["tests"].as_array().unwrap() { let (public, secret) = ml_dsa_65::KG::keygen_from_seed(&decode_hex(&test["seed"])); assert_eq!(public.into_bytes(), decode_hex::<1952>(&test["pk"])); assert_eq!(secret.into_bytes(), decode_hex::<4032>(&test["sk"])); } } for group in vectors["siggen"].as_array().unwrap() { for test in group["tests"].as_array().unwrap() { let secret = ml_dsa_65::PrivateKey::try_from_bytes(decode_hex(&test["sk"])).unwrap(); let message = hex::decode(test["message"].as_str().unwrap()).unwrap(); let randomness = if group["deterministic"] == true { [0; 32] } else { decode_hex(&test["rnd"]) }; let signature = ml_dsa_65::_internal_sign(&secret, &message, &[], randomness).unwrap(); assert_eq!(signature, decode_hex::<3309>(&test["signature"])); } } let mut cases = 0; let mut positive = 0; for group in vectors["sigver"].as_array().unwrap() { let public = ml_dsa_65::PublicKey::try_from_bytes(decode_hex(&group["pk"])).unwrap(); for test in group["tests"].as_array().unwrap() { let message: Vec<_> = hex::decode(test["message"].as_str().unwrap()).unwrap(); let expected = test["testPassed"].as_bool().unwrap(); assert_eq!( ml_dsa_65::_internal_verify( &public, &message, &decode_hex(&test["signature"]), &[] ), expected, "NIST case {}", test["tcId"] ); cases += 1; positive += usize::from(expected); } } assert_eq!(cases, 15); assert!(positive > 0 && positive < cases); }