//! Ed25519 key management and Noise library integration. use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use rand::rngs::OsRng; use thiserror::Error; use whalescale_types::{NodeId, PROLOGUE}; use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519StaticSecret}; /// Errors from crypto operations #[derive(Error, Debug)] pub enum CryptoError { #[error("snow error: {0}")] Snow(#[from] snow::Error), #[error("invalid key: {0}")] InvalidKey(&'static str), #[error("invalid payload: {0}")] InvalidPayload(&'static str), #[error("handshake not complete")] HandshakeIncomplete, #[error("decryption failed")] DecryptionFailed, #[error("session exhausted")] SessionExhausted, } /// Convert an Ed25519 public key to X25519 public key /// Uses Montgomery form: ed25519 -> Curve25519 pub fn ed25519_to_x25519_public(ed25519_public: &[u8; 32]) -> [u8; 32] { let verifying_key = VerifyingKey::from_bytes(ed25519_public).expect("invalid ed25519 public key"); // Convert Ed25519 point to Montgomery u-coordinate // This is the standard conversion used by libsodium and others let montgomery_u = curve25519_dalek::edwards::CompressedEdwardsY(verifying_key.to_bytes()) .decompress() .map(|edwards| edwards.to_montgomery()) .map(|montgomery| montgomery.0); match montgomery_u { Some(u) => u, None => { // Invalid point - return zeros (this should never happen with valid keys) [0u8; 32] } } } /// Convert an Ed25519 private key to X25519 private key /// The Ed25519 private key (32 bytes) is expanded via SHA-512 /// The first 32 bytes are the scalar, which we use directly for X25519 pub fn ed25519_to_x25519_private(ed25519_private: &[u8; 32]) -> [u8; 32] { use ed25519_dalek::SecretKey; let secret_key = SecretKey::from(*ed25519_private); let signing_key = SigningKey::from(&secret_key); // Get the scalar bytes directly from the expanded secret key // For ed25519-dalek 2.x, we access via to_scalar_bytes() signing_key.to_scalar_bytes() } /// Identity keypair: Ed25519 for identity, derived X25519 for Noise DH #[derive(Clone)] pub struct IdentityKeypair { ed25519_signing: SigningKey, ed25519_verifying: VerifyingKey, x25519_static: X25519StaticSecret, } impl IdentityKeypair { /// Generate a new identity keypair pub fn generate() -> Self { let mut csprng = OsRng; let signing_key = SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); // Derive X25519 key from Ed25519 private key let ed25519_private = signing_key.to_scalar_bytes(); let x25519_static = X25519StaticSecret::from(ed25519_private); Self { ed25519_signing: signing_key, ed25519_verifying: verifying_key, x25519_static, } } /// Load from existing Ed25519 private key (32 bytes) pub fn from_private_key(private_key: &[u8; 32]) -> Self { let secret_key = ed25519_dalek::SecretKey::from(*private_key); let signing_key = SigningKey::from(&secret_key); let verifying_key = signing_key.verifying_key(); let ed25519_private = signing_key.to_scalar_bytes(); let x25519_static = X25519StaticSecret::from(ed25519_private); Self { ed25519_signing: signing_key, ed25519_verifying: verifying_key, x25519_static, } } /// Get the NodeId (Ed25519 public key) pub fn node_id(&self) -> NodeId { NodeId::new(self.ed25519_verifying.to_bytes()) } /// Get Ed25519 public key bytes pub fn ed25519_public(&self) -> [u8; 32] { self.ed25519_verifying.to_bytes() } /// Get X25519 public key bytes pub fn x25519_public(&self) -> [u8; 32] { X25519PublicKey::from(&self.x25519_static).to_bytes() } /// Get X25519 private key bytes (for Noise) pub fn x25519_private(&self) -> [u8; 32] { // X25519StaticSecret doesn't expose the bytes directly // We need to re-derive from our stored Ed25519-derived scalar // For this, we recompute from the Ed25519 signing key self.ed25519_signing.to_scalar_bytes() } /// Sign a message with Ed25519 pub fn sign(&self, message: &[u8]) -> [u8; 64] { let signature: Signature = self.ed25519_signing.sign(message); signature.to_bytes() } /// Verify a signature pub fn verify( public_key: &[u8; 32], message: &[u8], signature: &[u8; 64], ) -> Result<(), CryptoError> { let verifying_key = VerifyingKey::from_bytes(public_key) .map_err(|_| CryptoError::InvalidKey("invalid public key"))?; let sig = Signature::from_bytes(signature); verifying_key .verify(message, &sig) .map_err(|_| CryptoError::InvalidPayload("signature verification failed")) } } /// Noise handshake state wrapper for initiator pub struct NoiseInitiator { state: snow::HandshakeState, remote_public_key: [u8; 32], } impl NoiseInitiator { /// Create a new initiator with remote static X25519 public key pub fn new( identity: &IdentityKeypair, remote_x25519_public: &[u8; 32], ) -> Result { let params: snow::params::NoiseParams = "Noise_IK_25519_ChaChaPoly_BLAKE2s" .parse() .map_err(|_| CryptoError::InvalidKey("invalid noise params"))?; let private_key = identity.x25519_private(); let builder = snow::Builder::new(params) .prologue(PROLOGUE)? .local_private_key(&private_key)? .remote_public_key(remote_x25519_public); let state = builder?.build_initiator()?; Ok(Self { state: state, remote_public_key: *remote_x25519_public, }) } /// Perform one step of the handshake /// Returns: (bytes to send, optional transport if handshake complete) pub fn step( &mut self, payload: &[u8], ) -> Result<(Vec, Option), CryptoError> { if self.state.is_handshake_finished() { return Err(CryptoError::HandshakeIncomplete); } let mut buf = vec![0u8; 65535]; let len = self.state.write_message(payload, &mut buf)?; buf.truncate(len); if self.state.is_handshake_finished() { let transport = self.state.into_stateless_transport_mode()?; Ok((buf, Some(NoiseTransport { inner: transport }))) } else { Ok((buf, None)) } } /// Check if handshake is complete pub fn is_complete(&self) -> bool { self.state.is_handshake_finished() } /// Get remote public key pub fn remote_public_key(&self) -> &[u8; 32] { &self.remote_public_key } } /// Noise handshake state wrapper for responder pub struct NoiseResponder { state: snow::HandshakeState, remote_public_key: Option<[u8; 32]>, } impl NoiseResponder { /// Create a new responder (waits for remote public key during handshake) pub fn new(identity: &IdentityKeypair) -> Result { let params: snow::params::NoiseParams = "Noise_IK_25519_ChaChaPoly_BLAKE2s" .parse() .map_err(|_| CryptoError::InvalidKey("invalid noise params"))?; let private_key = identity.x25519_private(); let builder = snow::Builder::new(params) .prologue(PROLOGUE)? .local_private_key(&private_key); let state = builder?.build_responder()?; Ok(Self { state, remote_public_key: None, }) } /// Perform one step of the handshake /// Returns: (bytes to send, optional transport if handshake complete) pub fn step( &mut self, payload: &[u8], ) -> Result<(Vec, Option), CryptoError> { if self.state.is_handshake_finished() { return Err(CryptoError::HandshakeIncomplete); } let mut buf = vec![0u8; 65535]; let len = self.state.read_message(payload, &mut buf)?; buf.truncate(len); // Store the remote public key if we just processed the first message if self.remote_public_key.is_none() { // In Noise_IK, the initiator's static key is sent in the first message // Snow doesn't expose this directly, but it's authenticated after read_message // For now, we rely on the handshake completing successfully // In a real implementation, we'd want to extract the initiator's public key self.remote_public_key = Some([0u8; 32]); // Placeholder } if self.state.is_handshake_finished() { let transport = self.state.into_stateless_transport_mode()?; Ok((buf, Some(NoiseTransport { inner: transport }))) } else { Ok((buf, None)) } } /// Check if handshake is complete pub fn is_complete(&self) -> bool { self.state.is_handshake_finished() } /// Get remote public key (available after first handshake message) pub fn remote_public_key(&self) -> Option<&[u8; 32]> { self.remote_public_key.as_ref() } } /// Noise transport state (post-handshake encryption/decryption) pub struct NoiseTransport { inner: snow::StatelessTransportState, } impl NoiseTransport { /// Encrypt a payload with the given nonce/sequence number /// Returns: ciphertext + 16-byte AEAD tag pub fn encrypt(&self, nonce: u64, plaintext: &[u8]) -> Result, CryptoError> { // Allocate a buffer large enough for ciphertext + 16-byte AEAD tag let mut buf = vec![0u8; plaintext.len() + 16]; let len = self .inner .write_message(nonce, plaintext, &mut buf) .map_err(|_| CryptoError::InvalidPayload("encryption failed"))?; buf.truncate(len); Ok(buf) } /// Decrypt a payload with the given nonce/sequence number /// Returns: plaintext pub fn decrypt(&self, nonce: u64, ciphertext: &[u8]) -> Result, CryptoError> { // Allocate a buffer large enough for plaintext (ciphertext - tag) let mut buf = vec![0u8; ciphertext.len()]; let len = self .inner .read_message(nonce, ciphertext, &mut buf) .map_err(|_| CryptoError::DecryptionFailed)?; buf.truncate(len); Ok(buf) } /// Rekey the outgoing direction (sender) pub fn rekey_outgoing(&self) { // Note: snow's StatelessTransportState doesn't have rekey methods // We need to manually manage rekeying through the snow state // For Phase 1, we'll implement rekey by creating a new session // This is a stub - full rekey implementation in session layer } /// Rekey the incoming direction (receiver) pub fn rekey_incoming(&self) { // Same as above - stub for now } /// Check if nonce space is exhausted pub fn is_exhausted(&self) -> bool { // In stateless mode, we manage nonces externally (64-bit) // Exhaustion at 2^64 is not practically reachable false } } #[cfg(test)] mod tests { use super::*; #[test] fn test_identity_keypair_generation() { let keypair = IdentityKeypair::generate(); let node_id = keypair.node_id(); assert_eq!(node_id.as_bytes().len(), 32); // Verify keys are non-zero assert!(!keypair.ed25519_public().iter().all(|&b| b == 0)); assert!(!keypair.x25519_public().iter().all(|&b| b == 0)); } #[test] fn test_key_conversion_consistency() { // Generate an Ed25519 keypair let identity = IdentityKeypair::generate(); let ed25519_pub = identity.ed25519_public(); let x25519_pub = identity.x25519_public(); // Convert Ed25519 public to X25519 and verify it matches let converted = ed25519_to_x25519_public(&ed25519_pub); assert_eq!(x25519_pub, converted); } #[test] fn test_sign_and_verify() { let identity = IdentityKeypair::generate(); let message = b"test message"; let signature = identity.sign(message); // Verify with correct key let result = IdentityKeypair::verify(&identity.ed25519_public(), message, &signature); assert!(result.is_ok()); // Verify with wrong message let result = IdentityKeypair::verify(&identity.ed25519_public(), b"wrong message", &signature); assert!(result.is_err()); } #[test] fn test_noise_ik_handshake_in_memory() { // Generate two identities let identity_a = IdentityKeypair::generate(); let identity_b = IdentityKeypair::generate(); // A initiates to B (A knows B's X25519 public key) let mut initiator = NoiseInitiator::new(&identity_a, &identity_b.x25519_public()).unwrap(); // B responds let mut responder = NoiseResponder::new(&identity_b).unwrap(); // Step 1: A -> B (handshake init) let (msg1, transport_a) = initiator.step(b"").unwrap(); assert!(transport_a.is_none()); // Not complete yet // Step 2: B processes init, sends response let (msg2, transport_b) = responder.step(&msg1).unwrap(); assert!(transport_b.is_some()); // Responder is done let transport_b = transport_b.unwrap(); // Step 3: A processes response // For initiator, we need a new mechanism to read the response // Since initiator consumed itself in step(), we need to handle this differently // In the actual implementation, the initiator would keep state // For this test, we verify the responder is ready // Note: The current API design has a limitation - the initiator's step() // method consumes the handshake state. We need a different approach. // For now, we verify the responder side works. } #[test] fn test_full_handshake_roundtrip() { // This test demonstrates the full handshake flow // using snow's lower-level API directly let identity_a = IdentityKeypair::generate(); let identity_b = IdentityKeypair::generate(); let params: snow::params::NoiseParams = "Noise_IK_25519_ChaChaPoly_BLAKE2s".parse().unwrap(); // Create initiator let mut initiator = snow::Builder::new(params.clone()) .prologue(PROLOGUE) .unwrap() .local_private_key(&identity_a.x25519_private()) .unwrap() .remote_public_key(&identity_b.x25519_public()) .unwrap() .build_initiator() .unwrap(); // Create responder let mut responder = snow::Builder::new(params) .prologue(PROLOGUE) .unwrap() .local_private_key(&identity_b.x25519_private()) .unwrap() .build_responder() .unwrap(); // Message 1: Initiator -> Responder let mut msg1 = vec![0u8; 65535]; let len1 = initiator.write_message(b"", &mut msg1).unwrap(); msg1.truncate(len1); assert!(!initiator.is_handshake_finished()); // Responder processes message 1 let mut payload1 = vec![0u8; 65535]; let len1_resp = responder.read_message(&msg1, &mut payload1).unwrap(); payload1.truncate(len1_resp); assert!(!responder.is_handshake_finished()); // Message 2: Responder -> Initiator let mut msg2 = vec![0u8; 65535]; let len2 = responder.write_message(b"", &mut msg2).unwrap(); msg2.truncate(len2); assert!(responder.is_handshake_finished()); // Initiator processes message 2 let mut payload2 = vec![0u8; 65535]; let len2_resp = initiator.read_message(&msg2, &mut payload2).unwrap(); payload2.truncate(len2_resp); assert!(initiator.is_handshake_finished()); // Both sides have completed the handshake // Convert to transport mode let transport_a = initiator.into_stateless_transport_mode().unwrap(); let transport_b = responder.into_stateless_transport_mode().unwrap(); // Test encryption/decryption let plaintext = b"hello from A to B"; let nonce = 1u64; let mut ciphertext_buf = vec![0u8; plaintext.len() + 16]; let ct_len = transport_a .write_message(nonce, plaintext, &mut ciphertext_buf) .unwrap(); ciphertext_buf.truncate(ct_len); let mut decrypted_buf = vec![0u8; ciphertext_buf.len()]; let pt_len = transport_b .read_message(nonce, &ciphertext_buf, &mut decrypted_buf) .unwrap(); decrypted_buf.truncate(pt_len); assert_eq!(plaintext.as_slice(), decrypted_buf.as_slice()); } #[test] fn test_transport_encryption_roundtrip() { let identity_a = IdentityKeypair::generate(); let identity_b = IdentityKeypair::generate(); let params: snow::params::NoiseParams = "Noise_IK_25519_ChaChaPoly_BLAKE2s".parse().unwrap(); // Complete handshake let mut initiator = snow::Builder::new(params.clone()) .prologue(PROLOGUE) .unwrap() .local_private_key(&identity_a.x25519_private()) .unwrap() .remote_public_key(&identity_b.x25519_public()) .unwrap() .build_initiator() .unwrap(); let mut responder = snow::Builder::new(params) .prologue(PROLOGUE) .unwrap() .local_private_key(&identity_b.x25519_private()) .unwrap() .build_responder() .unwrap(); // Exchange handshake messages let mut msg1 = vec![0u8; 65535]; let len1 = initiator.write_message(b"", &mut msg1).unwrap(); msg1.truncate(len1); let mut payload1 = vec![0u8; 65535]; responder.read_message(&msg1, &mut payload1).unwrap(); let mut msg2 = vec![0u8; 65535]; let len2 = responder.write_message(b"", &mut msg2).unwrap(); msg2.truncate(len2); let mut payload2 = vec![0u8; 65535]; initiator.read_message(&msg2, &mut payload2).unwrap(); let transport_a = initiator.into_stateless_transport_mode().unwrap(); let transport_b = responder.into_stateless_transport_mode().unwrap(); // Test bidirectional encryption for i in 0..100 { let plaintext_a = format!("A message {}", i); let nonce_a = i as u64; let mut ciphertext_a_buf = vec![0u8; plaintext_a.len() + 16]; let ct_a_len = transport_a .write_message(nonce_a, plaintext_a.as_bytes(), &mut ciphertext_a_buf) .unwrap(); ciphertext_a_buf.truncate(ct_a_len); let mut decrypted_a_buf = vec![0u8; ciphertext_a_buf.len()]; let pt_a_len = transport_b .read_message(nonce_a, &ciphertext_a_buf, &mut decrypted_a_buf) .unwrap(); decrypted_a_buf.truncate(pt_a_len); assert_eq!(plaintext_a.as_bytes(), decrypted_a_buf.as_slice()); let plaintext_b = format!("B message {}", i); let nonce_b = (i + 1000) as u64; let mut ciphertext_b_buf = vec![0u8; plaintext_b.len() + 16]; let ct_b_len = transport_b .write_message(nonce_b, plaintext_b.as_bytes(), &mut ciphertext_b_buf) .unwrap(); ciphertext_b_buf.truncate(ct_b_len); let mut decrypted_b_buf = vec![0u8; ciphertext_b_buf.len()]; let pt_b_len = transport_a .read_message(nonce_b, &ciphertext_b_buf, &mut decrypted_b_buf) .unwrap(); decrypted_b_buf.truncate(pt_b_len); assert_eq!(plaintext_b.as_bytes(), decrypted_b_buf.as_slice()); } } #[test] fn test_wrong_key_fails() { let identity_a = IdentityKeypair::generate(); let identity_b = IdentityKeypair::generate(); let identity_c = IdentityKeypair::generate(); let params: snow::params::NoiseParams = "Noise_IK_25519_ChaChaPoly_BLAKE2s".parse().unwrap(); // A tries to talk to B, but we verify with C's key let mut initiator = snow::Builder::new(params.clone()) .prologue(PROLOGUE) .unwrap() .local_private_key(&identity_a.x25519_private()) .unwrap() .remote_public_key(&identity_b.x25519_public()) .unwrap() .build_initiator() .unwrap(); let mut responder = snow::Builder::new(params) .prologue(PROLOGUE) .unwrap() .local_private_key(&identity_b.x25519_private()) .unwrap() .build_responder() .unwrap(); // Exchange handshake let mut msg1 = vec![0u8; 65535]; let len1 = initiator.write_message(b"", &mut msg1).unwrap(); msg1.truncate(len1); let mut payload1 = vec![0u8; 65535]; responder.read_message(&msg1, &mut payload1).unwrap(); let mut msg2 = vec![0u8; 65535]; let len2 = responder.write_message(b"", &mut msg2).unwrap(); msg2.truncate(len2); let mut payload2 = vec![0u8; 65535]; initiator.read_message(&msg2, &mut payload2).unwrap(); let transport_a = initiator.into_stateless_transport_mode().unwrap(); let transport_b = responder.into_stateless_transport_mode().unwrap(); // Try to decrypt with wrong nonce let plaintext = b"test"; let mut ciphertext_buf = vec![0u8; plaintext.len() + 16]; let ct_len = transport_a .write_message(1, plaintext, &mut ciphertext_buf) .unwrap(); ciphertext_buf.truncate(ct_len); // Wrong nonce let mut decrypted_buf = vec![0u8; ciphertext_buf.len()]; let result = transport_b.read_message(2, &ciphertext_buf, &mut decrypted_buf); assert!(result.is_err()); } #[test] fn test_tampered_ciphertext() { let identity_a = IdentityKeypair::generate(); let identity_b = IdentityKeypair::generate(); let params: snow::params::NoiseParams = "Noise_IK_25519_ChaChaPoly_BLAKE2s".parse().unwrap(); let mut initiator = snow::Builder::new(params.clone()) .prologue(PROLOGUE) .unwrap() .local_private_key(&identity_a.x25519_private()) .unwrap() .remote_public_key(&identity_b.x25519_public()) .unwrap() .build_initiator() .unwrap(); let mut responder = snow::Builder::new(params) .prologue(PROLOGUE) .unwrap() .local_private_key(&identity_b.x25519_private()) .unwrap() .build_responder() .unwrap(); // Complete handshake let mut msg1 = vec![0u8; 65535]; let len1 = initiator.write_message(b"", &mut msg1).unwrap(); msg1.truncate(len1); let mut payload1 = vec![0u8; 65535]; responder.read_message(&msg1, &mut payload1).unwrap(); let mut msg2 = vec![0u8; 65535]; let len2 = responder.write_message(b"", &mut msg2).unwrap(); msg2.truncate(len2); let mut payload2 = vec![0u8; 65535]; initiator.read_message(&msg2, &mut payload2).unwrap(); let transport_a = initiator.into_stateless_transport_mode().unwrap(); let transport_b = responder.into_stateless_transport_mode().unwrap(); let plaintext = b"test message"; let mut ciphertext_buf = vec![0u8; plaintext.len() + 16]; let ct_len = transport_a .write_message(1, plaintext, &mut ciphertext_buf) .unwrap(); ciphertext_buf.truncate(ct_len); // Tamper with ciphertext ciphertext_buf[10] ^= 0xFF; let mut decrypted_buf = vec![0u8; ciphertext_buf.len()]; let result = transport_b.read_message(1, &ciphertext_buf, &mut decrypted_buf); assert!(result.is_err()); } #[test] fn test_private_key_load() { // Generate a keypair let original = IdentityKeypair::generate(); let original_node_id = original.node_id(); // Extract private key (ed25519) let private_key = original.ed25519_signing.to_scalar_bytes(); // Load from private key let loaded = IdentityKeypair::from_private_key(&private_key); let loaded_node_id = loaded.node_id(); // Should be identical assert_eq!(original_node_id, loaded_node_id); assert_eq!(original.ed25519_public(), loaded.ed25519_public()); assert_eq!(original.x25519_public(), loaded.x25519_public()); } }