diff options
Diffstat (limited to 'src/crypto.rs')
| -rw-r--r-- | src/crypto.rs | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..10587ec --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,172 @@ +//! Ed25519 identity and packet signing. +//! +//! Each node has an Ed25519 keypair: +//! - **Private key** (32 bytes): never leaves the node. +//! Used to sign every outgoing packet. +//! - **Public key** (32 bytes): shared with peers. +//! Used to verify incoming packets. +//! - **NodeId** = public key (32 bytes). Direct 1:1 +//! binding, no hashing. + +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; + +use crate::id::NodeId; + +/// Ed25519 signature size (64 bytes). +pub const SIGNATURE_SIZE: usize = 64; + +/// Ed25519 public key size (32 bytes). +pub const PUBLIC_KEY_SIZE: usize = 32; + +/// A node's cryptographic identity. +/// +/// Contains the Ed25519 keypair and the derived NodeId. +/// The NodeId is the public key directly (32 bytes) +/// deterministically bound to the keypair. +pub struct Identity { + signing_key: SigningKey, + verifying_key: VerifyingKey, + node_id: NodeId, +} + +impl Identity { + /// Generate a new random identity. + pub fn generate() -> Self { + let mut seed = [0u8; 32]; + crate::sys::random_bytes(&mut seed); + Self::from_seed(seed) + } + + /// Create an identity from a 32-byte seed. + /// + /// Deterministic: same seed → same keypair → same + /// NodeId. + pub fn from_seed(seed: [u8; 32]) -> Self { + let signing_key = SigningKey::from_bytes(&seed); + let verifying_key = signing_key.verifying_key(); + let node_id = NodeId::from_bytes(*verifying_key.as_bytes()); + Self { + signing_key, + verifying_key, + node_id, + } + } + + /// The node's 256-bit ID (= public key). + pub fn node_id(&self) -> &NodeId { + &self.node_id + } + + /// The Ed25519 public key (32 bytes). + pub fn public_key(&self) -> &[u8; PUBLIC_KEY_SIZE] { + self.verifying_key.as_bytes() + } + + /// Sign data with the private key. + /// + /// Returns a 64-byte Ed25519 signature. + pub fn sign(&self, data: &[u8]) -> [u8; SIGNATURE_SIZE] { + let sig = self.signing_key.sign(data); + sig.to_bytes() + } + + /// Verify a signature against a public key. + /// + /// This is a static method — used to verify packets + /// from other nodes using their public key. + pub fn verify( + public_key: &[u8; PUBLIC_KEY_SIZE], + data: &[u8], + signature: &[u8], + ) -> bool { + // Length check is not timing-sensitive (length is + // public). ed25519_dalek::verify() is constant-time. + if signature.len() != SIGNATURE_SIZE { + return false; + } + let Ok(vk) = VerifyingKey::from_bytes(public_key) else { + return false; + }; + let mut sig_bytes = [0u8; SIGNATURE_SIZE]; + sig_bytes.copy_from_slice(signature); + let sig = Signature::from_bytes(&sig_bytes); + vk.verify(data, &sig).is_ok() + } +} + +impl std::fmt::Debug for Identity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Identity({})", self.node_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_unique() { + let a = Identity::generate(); + let b = Identity::generate(); + assert_ne!(a.node_id(), b.node_id()); + } + + #[test] + fn from_seed_deterministic() { + let seed = [0x42u8; 32]; + let a = Identity::from_seed(seed); + let b = Identity::from_seed(seed); + assert_eq!(a.node_id(), b.node_id()); + assert_eq!(a.public_key(), b.public_key()); + } + + #[test] + fn node_id_is_pubkey() { + let id = Identity::generate(); + let expected = NodeId::from_bytes(*id.public_key()); + assert_eq!(*id.node_id(), expected); + } + + #[test] + fn sign_verify() { + let id = Identity::generate(); + let data = b"hello world"; + let sig = id.sign(data); + + assert!(Identity::verify(id.public_key(), data, &sig)); + } + + #[test] + fn verify_wrong_data() { + let id = Identity::generate(); + let sig = id.sign(b"correct"); + + assert!(!Identity::verify(id.public_key(), b"wrong", &sig)); + } + + #[test] + fn verify_wrong_key() { + let id1 = Identity::generate(); + let id2 = Identity::generate(); + let sig = id1.sign(b"data"); + + assert!(!Identity::verify(id2.public_key(), b"data", &sig)); + } + + #[test] + fn verify_truncated_sig() { + let id = Identity::generate(); + assert!(!Identity::verify( + id.public_key(), + b"data", + &[0u8; 10] // too short + )); + } + + #[test] + fn signature_size() { + let id = Identity::generate(); + let sig = id.sign(b"test"); + assert_eq!(sig.len(), SIGNATURE_SIZE); + } +} |