diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..56bc752 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,307 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cc" +version = "1.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "fips" +version = "0.1.0" +dependencies = [ + "hex", + "rand", + "secp256k1", + "sha2", + "thiserror", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 58ee9ba..3d78b0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] +secp256k1 = { version = "0.30", features = ["rand", "global-context"] } +sha2 = "0.10" +rand = "0.8" +thiserror = "2.0" + +[dev-dependencies] +hex = "0.4" diff --git a/src/identity.rs b/src/identity.rs new file mode 100644 index 0000000..0ed91d2 --- /dev/null +++ b/src/identity.rs @@ -0,0 +1,497 @@ +//! FIPS Identity System +//! +//! Node identity based on Nostr keypairs (secp256k1). The node_id is derived +//! from the public key via SHA-256, and the FIPS address uses an IPv6-compatible +//! format with the 0xfd prefix. + +use rand::Rng; +use secp256k1::{Keypair, Secp256k1, SecretKey, XOnlyPublicKey}; +use sha2::{Digest, Sha256}; +use std::fmt; +use thiserror::Error; + +/// Domain separation string for authentication challenges. +const AUTH_DOMAIN: &[u8] = b"fips-auth-v1"; + +/// FIPS address prefix (IPv6 ULA range). +const FIPS_ADDRESS_PREFIX: u8 = 0xfd; + +/// Errors that can occur in identity operations. +#[derive(Debug, Error)] +pub enum IdentityError { + #[error("invalid secret key: {0}")] + InvalidSecretKey(#[from] secp256k1::Error), + + #[error("signature verification failed")] + SignatureVerificationFailed, + + #[error("invalid node_id length: expected 32, got {0}")] + InvalidNodeIdLength(usize), + + #[error("invalid address length: expected 16, got {0}")] + InvalidAddressLength(usize), + + #[error("invalid address prefix: expected 0xfd, got 0x{0:02x}")] + InvalidAddressPrefix(u8), +} + +/// 32-byte node identifier derived from SHA-256(npub). +/// +/// The node_id is used in protocol messages and bloom filters. Hashing the +/// public key prevents grinding attacks that exploit secp256k1's algebraic +/// structure. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NodeId([u8; 32]); + +impl NodeId { + /// Create a NodeId from a 32-byte array. + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Create a NodeId from a slice. + pub fn from_slice(slice: &[u8]) -> Result { + if slice.len() != 32 { + return Err(IdentityError::InvalidNodeIdLength(slice.len())); + } + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(slice); + Ok(Self(bytes)) + } + + /// Derive a NodeId from an x-only public key (npub). + pub fn from_pubkey(pubkey: &XOnlyPublicKey) -> Self { + let mut hasher = Sha256::new(); + hasher.update(pubkey.serialize()); + let hash = hasher.finalize(); + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(&hash); + Self(bytes) + } + + /// Return the raw bytes. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Return the bytes as a slice. + pub fn as_slice(&self) -> &[u8] { + &self.0 + } +} + +impl fmt::Debug for NodeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "NodeId({})", hex_encode(&self.0[..8])) + } +} + +impl fmt::Display for NodeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex_encode(&self.0)) + } +} + +impl AsRef<[u8]> for NodeId { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// 128-bit FIPS address with IPv6-compatible format. +/// +/// The address uses the IPv6 Unique Local Address (ULA) prefix `fd00::/8`, +/// providing 120 bits for the node_id hash. This format allows applications +/// designed for IP transports to bind to FIPS addresses via a TUN interface. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct FipsAddress([u8; 16]); + +impl FipsAddress { + /// Create a FipsAddress from a 16-byte array. + pub fn from_bytes(bytes: [u8; 16]) -> Result { + if bytes[0] != FIPS_ADDRESS_PREFIX { + return Err(IdentityError::InvalidAddressPrefix(bytes[0])); + } + Ok(Self(bytes)) + } + + /// Create a FipsAddress from a slice. + pub fn from_slice(slice: &[u8]) -> Result { + if slice.len() != 16 { + return Err(IdentityError::InvalidAddressLength(slice.len())); + } + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(slice); + Self::from_bytes(bytes) + } + + /// Derive a FipsAddress from a NodeId. + /// + /// Takes the first 15 bytes of the node_id and prepends the 0xfd prefix. + pub fn from_node_id(node_id: &NodeId) -> Self { + let mut bytes = [0u8; 16]; + bytes[0] = FIPS_ADDRESS_PREFIX; + bytes[1..16].copy_from_slice(&node_id.0[0..15]); + Self(bytes) + } + + /// Return the raw bytes. + pub fn as_bytes(&self) -> &[u8; 16] { + &self.0 + } + + /// Format as an IPv6 address string. + pub fn to_ipv6_string(&self) -> String { + let segments: Vec = (0..8) + .map(|i| { + let high = self.0[i * 2]; + let low = self.0[i * 2 + 1]; + format!("{:02x}{:02x}", high, low) + }) + .collect(); + segments.join(":") + } +} + +impl fmt::Debug for FipsAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "FipsAddress({})", self.to_ipv6_string()) + } +} + +impl fmt::Display for FipsAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_ipv6_string()) + } +} + +/// A FIPS node identity consisting of a keypair and derived identifiers. +/// +/// The identity holds the secp256k1 keypair and provides methods for signing +/// and verifying protocol messages. +pub struct Identity { + keypair: Keypair, + node_id: NodeId, + address: FipsAddress, +} + +impl Identity { + /// Create a new random identity. + pub fn generate() -> Self { + let secp = Secp256k1::new(); + let keypair = Keypair::new(&secp, &mut rand::thread_rng()); + Self::from_keypair(keypair) + } + + /// Create an identity from an existing keypair. + pub fn from_keypair(keypair: Keypair) -> Self { + let (pubkey, _parity) = keypair.x_only_public_key(); + let node_id = NodeId::from_pubkey(&pubkey); + let address = FipsAddress::from_node_id(&node_id); + Self { + keypair, + node_id, + address, + } + } + + /// Create an identity from a secret key. + pub fn from_secret_key(secret_key: SecretKey) -> Self { + let secp = Secp256k1::new(); + let keypair = Keypair::from_secret_key(&secp, &secret_key); + Self::from_keypair(keypair) + } + + /// Create an identity from secret key bytes. + pub fn from_secret_bytes(bytes: &[u8; 32]) -> Result { + let secret_key = SecretKey::from_slice(bytes)?; + Ok(Self::from_secret_key(secret_key)) + } + + /// Return the x-only public key (npub). + pub fn pubkey(&self) -> XOnlyPublicKey { + self.keypair.x_only_public_key().0 + } + + /// Return the node ID. + pub fn node_id(&self) -> &NodeId { + &self.node_id + } + + /// Return the FIPS address. + pub fn address(&self) -> &FipsAddress { + &self.address + } + + /// Sign arbitrary data with this identity's secret key. + pub fn sign(&self, data: &[u8]) -> secp256k1::schnorr::Signature { + let secp = Secp256k1::new(); + let digest = sha256(data); + secp.sign_schnorr(&digest, &self.keypair) + } + + /// Create an authentication response for a challenge. + /// + /// The response signs: SHA256("fips-auth-v1" || challenge || timestamp) + pub fn sign_challenge(&self, challenge: &[u8; 32], timestamp: u64) -> AuthResponse { + let digest = auth_challenge_digest(challenge, timestamp); + let secp = Secp256k1::new(); + let signature = secp.sign_schnorr(&digest, &self.keypair); + AuthResponse { + pubkey: self.pubkey(), + timestamp, + signature, + } + } +} + +impl fmt::Debug for Identity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Identity") + .field("node_id", &self.node_id) + .field("address", &self.address) + .finish_non_exhaustive() + } +} + +/// A 32-byte random authentication challenge. +#[derive(Clone, Copy, Debug)] +pub struct AuthChallenge([u8; 32]); + +impl AuthChallenge { + /// Generate a new random challenge. + pub fn generate() -> Self { + let mut bytes = [0u8; 32]; + rand::thread_rng().fill(&mut bytes); + Self(bytes) + } + + /// Create a challenge from bytes. + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Return the challenge bytes. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Verify a response to this challenge. + pub fn verify(&self, response: &AuthResponse) -> Result { + let digest = auth_challenge_digest(&self.0, response.timestamp); + let secp = Secp256k1::new(); + + secp.verify_schnorr(&response.signature, &digest, &response.pubkey) + .map_err(|_| IdentityError::SignatureVerificationFailed)?; + + Ok(NodeId::from_pubkey(&response.pubkey)) + } +} + +/// Response to an authentication challenge. +#[derive(Clone, Debug)] +pub struct AuthResponse { + /// The responder's public key. + pub pubkey: XOnlyPublicKey, + /// Timestamp included in the signed message. + pub timestamp: u64, + /// Schnorr signature over the challenge digest. + pub signature: secp256k1::schnorr::Signature, +} + +/// Compute the digest for an authentication challenge. +fn auth_challenge_digest(challenge: &[u8; 32], timestamp: u64) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(AUTH_DOMAIN); + hasher.update(challenge); + hasher.update(timestamp.to_be_bytes()); + let result = hasher.finalize(); + let mut digest = [0u8; 32]; + digest.copy_from_slice(&result); + digest +} + +/// Compute SHA-256 hash of data. +fn sha256(data: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(data); + let result = hasher.finalize(); + let mut hash = [0u8; 32]; + hash.copy_from_slice(&result); + hash +} + +/// Encode bytes as lowercase hex string. +fn hex_encode(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_identity_generation() { + let identity = Identity::generate(); + + // NodeId should be 32 bytes + assert_eq!(identity.node_id().as_bytes().len(), 32); + + // Address should start with 0xfd + assert_eq!(identity.address().as_bytes()[0], 0xfd); + + // Address bytes 1-15 should match node_id bytes 0-14 + assert_eq!( + &identity.address().as_bytes()[1..16], + &identity.node_id().as_bytes()[0..15] + ); + } + + #[test] + fn test_node_id_from_pubkey_deterministic() { + let identity = Identity::generate(); + let pubkey = identity.pubkey(); + + let node_id1 = NodeId::from_pubkey(&pubkey); + let node_id2 = NodeId::from_pubkey(&pubkey); + + assert_eq!(node_id1, node_id2); + } + + #[test] + fn test_fips_address_ipv6_format() { + let identity = Identity::generate(); + let addr_str = identity.address().to_ipv6_string(); + + // Should be 8 groups of 4 hex chars separated by colons + let parts: Vec<&str> = addr_str.split(':').collect(); + assert_eq!(parts.len(), 8); + for part in parts { + assert_eq!(part.len(), 4); + } + + // First byte should be fd + assert!(addr_str.starts_with("fd")); + } + + #[test] + fn test_auth_challenge_verify_success() { + let identity = Identity::generate(); + let challenge = AuthChallenge::generate(); + let timestamp = 1234567890u64; + + let response = identity.sign_challenge(challenge.as_bytes(), timestamp); + let result = challenge.verify(&response); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), *identity.node_id()); + } + + #[test] + fn test_auth_challenge_verify_wrong_challenge() { + let identity = Identity::generate(); + let challenge1 = AuthChallenge::generate(); + let challenge2 = AuthChallenge::generate(); + let timestamp = 1234567890u64; + + let response = identity.sign_challenge(challenge1.as_bytes(), timestamp); + let result = challenge2.verify(&response); + + assert!(matches!( + result, + Err(IdentityError::SignatureVerificationFailed) + )); + } + + #[test] + fn test_auth_challenge_verify_wrong_timestamp() { + let identity = Identity::generate(); + let challenge = AuthChallenge::generate(); + + let response = identity.sign_challenge(challenge.as_bytes(), 1234567890); + + // Modify the timestamp in the response + let bad_response = AuthResponse { + pubkey: response.pubkey, + timestamp: 9999999999, + signature: response.signature, + }; + + let result = challenge.verify(&bad_response); + assert!(matches!( + result, + Err(IdentityError::SignatureVerificationFailed) + )); + } + + #[test] + fn test_node_id_ordering() { + let id1 = Identity::generate(); + let id2 = Identity::generate(); + + // NodeIds should be comparable for root election + let _cmp = id1.node_id().cmp(id2.node_id()); + } + + #[test] + fn test_identity_from_secret_bytes() { + // A known secret key (32 bytes) + let secret_bytes: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x1d, 0x1e, 0x1f, 0x20, + ]; + + let identity1 = Identity::from_secret_bytes(&secret_bytes).unwrap(); + let identity2 = Identity::from_secret_bytes(&secret_bytes).unwrap(); + + // Same secret key should produce same node_id + assert_eq!(identity1.node_id(), identity2.node_id()); + assert_eq!(identity1.address(), identity2.address()); + } + + #[test] + fn test_node_id_from_slice() { + let bytes = [0u8; 32]; + let node_id = NodeId::from_slice(&bytes).unwrap(); + assert_eq!(node_id.as_bytes(), &bytes); + + // Wrong length should fail + let short = [0u8; 16]; + assert!(matches!( + NodeId::from_slice(&short), + Err(IdentityError::InvalidNodeIdLength(16)) + )); + } + + #[test] + fn test_fips_address_validation() { + // Valid address with fd prefix + let mut valid = [0u8; 16]; + valid[0] = 0xfd; + assert!(FipsAddress::from_bytes(valid).is_ok()); + + // Invalid prefix + let mut invalid = [0u8; 16]; + invalid[0] = 0xfe; + assert!(matches!( + FipsAddress::from_bytes(invalid), + Err(IdentityError::InvalidAddressPrefix(0xfe)) + )); + } + + #[test] + fn test_identity_sign() { + let identity = Identity::generate(); + let data = b"test message"; + + let sig = identity.sign(data); + + // Verify the signature manually + let secp = Secp256k1::new(); + let digest = sha256(data); + assert!(secp + .verify_schnorr(&sig, &digest, &identity.pubkey()) + .is_ok()); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b570ee5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +//! FIPS: Federated Interoperable Peering System +//! +//! A distributed, decentralized network routing protocol for mesh nodes +//! connecting over arbitrary transports. + +pub mod identity; + +pub use identity::{ + AuthChallenge, AuthResponse, FipsAddress, Identity, IdentityError, NodeId, +}; diff --git a/src/main.rs b/src/main.rs index e7a11a9..9b51799 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,3 @@ fn main() { - println!("Hello, world!"); + println!("FIPS - Federated Interoperable Peering System"); }