aboutsummaryrefslogtreecommitdiffstats
path: root/src/crypto.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/crypto.rs')
-rw-r--r--src/crypto.rs172
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);
+ }
+}