identity.rs (4243B)
1 use std::{ 2 collections::BTreeMap, 3 sync::{Mutex as StdMutex, OnceLock}, 4 }; 5 6 use anyhow::Result; 7 use secrecy::{ExposeSecret, SecretBox}; 8 9 use crate::{ 10 app::{GossipEnvelope, NETWORK_ID}, 11 domain::{ed25519_public_key, sign_ed25519, verify_ed25519}, 12 }; 13 14 use super::GossipNetwork; 15 16 static NODE_SIGNING_KEYS: OnceLock<StdMutex<BTreeMap<String, SecretBox<[u8; 32]>>>> = 17 OnceLock::new(); 18 19 pub(super) fn new_node_id() -> String { 20 let signing_seed = SecretBox::init_with_mut(|bytes: &mut [u8; 32]| { 21 getrandom::getrandom(bytes).expect("secure randomness unavailable for p2p node id"); 22 }); 23 let node_id = hex_encode(&ed25519_public_key(signing_seed.expose_secret())); 24 node_signing_keys() 25 .lock() 26 .expect("node signing key registry mutex poisoned") 27 .insert(node_id.clone(), signing_seed); 28 node_id 29 } 30 31 fn node_signing_keys() -> &'static StdMutex<BTreeMap<String, SecretBox<[u8; 32]>>> { 32 NODE_SIGNING_KEYS.get_or_init(|| StdMutex::new(BTreeMap::new())) 33 } 34 35 pub(super) fn hex_encode(bytes: &[u8]) -> String { 36 const HEX: &[u8; 16] = b"0123456789abcdef"; 37 let mut encoded = String::with_capacity(bytes.len() * 2); 38 for byte in bytes { 39 encoded.push(HEX[(byte >> 4) as usize] as char); 40 encoded.push(HEX[(byte & 0x0f) as usize] as char); 41 } 42 encoded 43 } 44 45 pub(super) fn decode_hex_array<const N: usize>(value: &str) -> Result<[u8; N]> { 46 if value.len() != N * 2 { 47 anyhow::bail!("hex value has {} chars, expected {}", value.len(), N * 2); 48 } 49 let mut bytes = [0_u8; N]; 50 for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() { 51 let high = hex_nibble(chunk[0])?; 52 let low = hex_nibble(chunk[1])?; 53 bytes[index] = (high << 4) | low; 54 } 55 Ok(bytes) 56 } 57 58 pub(super) fn hex_nibble(byte: u8) -> Result<u8> { 59 match byte { 60 b'0'..=b'9' => Ok(byte - b'0'), 61 b'a'..=b'f' => Ok(byte - b'a' + 10), 62 b'A'..=b'F' => Ok(byte - b'A' + 10), 63 _ => anyhow::bail!("invalid hex digit"), 64 } 65 } 66 67 pub(super) fn new_verification_nonce() -> String { 68 let mut bytes = [0_u8; 32]; 69 getrandom::getrandom(&mut bytes) 70 .expect("secure randomness unavailable for p2p verification nonce"); 71 hex_encode(&bytes) 72 } 73 74 pub(super) fn peer_verification_payload(address: &str, nonce: &str, node_id: &str) -> String { 75 format!("iuna-peer-verification:v1:{NETWORK_ID}:{node_id}:{address}:{nonce}") 76 } 77 78 pub(super) fn peer_verification_response( 79 network: &GossipNetwork, 80 address: &str, 81 nonce: &str, 82 ) -> Option<GossipEnvelope> { 83 peer_verification_response_for_node_id(&network.inner.node_id, address, nonce) 84 } 85 86 pub(super) fn peer_verification_response_for_node_id( 87 node_id: &str, 88 address: &str, 89 nonce: &str, 90 ) -> Option<GossipEnvelope> { 91 let keys = node_signing_keys() 92 .lock() 93 .expect("node signing key registry mutex poisoned"); 94 let signing_seed = keys.get(node_id)?; 95 let payload = peer_verification_payload(address, nonce, node_id); 96 let signature = sign_ed25519(signing_seed.expose_secret(), payload.as_bytes()); 97 Some(GossipEnvelope::PeerVerificationResponse { 98 address: address.to_string(), 99 nonce: nonce.to_string(), 100 node_id: node_id.to_string(), 101 signature: hex_encode(&signature), 102 }) 103 } 104 105 pub(super) fn peer_verification_response_is_valid( 106 response_address: &str, 107 response_nonce: &str, 108 response_node_id: &str, 109 signature: &str, 110 expected_address: &str, 111 expected_nonce: &str, 112 expected_node_id: &str, 113 ) -> bool { 114 if response_address != expected_address 115 || response_nonce != expected_nonce 116 || response_node_id != expected_node_id 117 { 118 return false; 119 } 120 let public_key = match decode_hex_array::<32>(response_node_id) { 121 Ok(public_key) => public_key, 122 Err(_) => return false, 123 }; 124 let signature = match decode_hex_array::<64>(signature) { 125 Ok(signature) => signature, 126 Err(_) => return false, 127 }; 128 verify_ed25519( 129 &public_key, 130 peer_verification_payload(expected_address, expected_nonce, expected_node_id).as_bytes(), 131 &signature, 132 "peer verification", 133 ) 134 .is_ok() 135 }