253 lines
7.2 KiB
Rust
253 lines
7.2 KiB
Rust
//! Node identity and cryptographic keys
|
|
//!
|
|
//! Each node has an Ed25519 keypair:
|
|
//! - Private key: stored locally in `identity.key` (never replicated)
|
|
//! - Public key: serves as the node's identity (32 bytes)
|
|
|
|
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
|
use rand::rngs::OsRng;
|
|
use std::fs;
|
|
use std::io::{self, Read, Write};
|
|
use std::path::Path;
|
|
use thiserror::Error;
|
|
|
|
/// Errors that can occur during node operations
|
|
#[derive(Error, Debug)]
|
|
pub enum NodeError {
|
|
#[error("IO error: {0}")]
|
|
Io(#[from] io::Error),
|
|
|
|
#[error("Invalid key length: expected 32 bytes, got {0}")]
|
|
InvalidKeyLength(usize),
|
|
|
|
#[error("Invalid signature")]
|
|
InvalidSignature,
|
|
}
|
|
|
|
/// A node in the Lattice mesh.
|
|
///
|
|
/// Each node has an Ed25519 keypair used for signing sigchain entries
|
|
/// and establishing trust within the network.
|
|
#[derive(Clone)]
|
|
pub struct NodeIdentity {
|
|
signing_key: SigningKey,
|
|
}
|
|
|
|
impl NodeIdentity {
|
|
/// Generate a new node with a random keypair.
|
|
pub fn generate() -> Self {
|
|
let signing_key = SigningKey::generate(&mut OsRng);
|
|
Self { signing_key }
|
|
}
|
|
|
|
/// Create a node from an existing signing key.
|
|
pub fn from_signing_key(signing_key: SigningKey) -> Self {
|
|
Self { signing_key }
|
|
}
|
|
|
|
/// Load a node's identity from a key file, or generate and save if it doesn't exist.
|
|
pub fn load_or_generate(path: impl AsRef<Path>) -> Result<Self, NodeError> {
|
|
let path = path.as_ref();
|
|
if path.exists() {
|
|
Self::load(path)
|
|
} else {
|
|
let node = Self::generate();
|
|
node.save(path)?;
|
|
Ok(node)
|
|
}
|
|
}
|
|
|
|
/// Load a node's identity from a key file.
|
|
pub fn load(path: impl AsRef<Path>) -> Result<Self, NodeError> {
|
|
let mut file = fs::File::open(path)?;
|
|
let mut bytes = Vec::new();
|
|
file.read_to_end(&mut bytes)?;
|
|
|
|
if bytes.len() != 32 {
|
|
return Err(NodeError::InvalidKeyLength(bytes.len()));
|
|
}
|
|
|
|
let key_bytes: [u8; 32] = bytes.try_into().unwrap();
|
|
let signing_key = SigningKey::from_bytes(&key_bytes);
|
|
Ok(Self { signing_key })
|
|
}
|
|
|
|
/// Save the node's private key to a file.
|
|
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), NodeError> {
|
|
let path = path.as_ref();
|
|
|
|
// Create parent directories if they don't exist
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent)?;
|
|
}
|
|
|
|
let mut file = fs::File::create(path)?;
|
|
file.write_all(self.signing_key.as_bytes())?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the node's public key (identity).
|
|
pub fn public_key(&self) -> VerifyingKey {
|
|
self.signing_key.verifying_key()
|
|
}
|
|
|
|
/// Get the node's public key as bytes (32 bytes).
|
|
pub fn public_key_bytes(&self) -> [u8; 32] {
|
|
self.signing_key.verifying_key().to_bytes()
|
|
}
|
|
|
|
/// Get the signing key for creating signatures.
|
|
pub fn signing_key(&self) -> &SigningKey {
|
|
&self.signing_key
|
|
}
|
|
|
|
/// Get the secret key bytes (32 bytes) for Iroh integration.
|
|
/// WARNING: Handle with care - this exposes the private key material.
|
|
pub fn secret_key_bytes(&self) -> [u8; 32] {
|
|
self.signing_key.to_bytes()
|
|
}
|
|
|
|
/// Sign a message.
|
|
pub fn sign(&self, message: &[u8]) -> Signature {
|
|
self.signing_key.sign(message)
|
|
}
|
|
|
|
/// Verify a signature against this node's public key.
|
|
pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), NodeError> {
|
|
self.public_key()
|
|
.verify(message, signature)
|
|
.map_err(|_| NodeError::InvalidSignature)
|
|
}
|
|
|
|
/// Verify a signature using a raw public key.
|
|
pub fn verify_with_key(
|
|
public_key: &VerifyingKey,
|
|
message: &[u8],
|
|
signature: &Signature,
|
|
) -> Result<(), NodeError> {
|
|
public_key
|
|
.verify(message, signature)
|
|
.map_err(|_| NodeError::InvalidSignature)
|
|
}
|
|
}
|
|
|
|
/// Peer status values used across the system
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum PeerStatus {
|
|
/// Peer has been invited but hasn't joined yet
|
|
Invited,
|
|
/// Peer is active and can sync
|
|
Active,
|
|
/// Peer has been removed from the mesh
|
|
Removed,
|
|
}
|
|
|
|
impl PeerStatus {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
PeerStatus::Invited => "invited",
|
|
PeerStatus::Active => "active",
|
|
PeerStatus::Removed => "removed",
|
|
}
|
|
}
|
|
|
|
pub fn from_str(s: &str) -> Option<PeerStatus> {
|
|
match s {
|
|
"invited" => Some(PeerStatus::Invited),
|
|
"active" => Some(PeerStatus::Active),
|
|
"removed" => Some(PeerStatus::Removed),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::env::temp_dir;
|
|
|
|
#[test]
|
|
fn test_generate() {
|
|
let node = NodeIdentity::generate();
|
|
let pk = node.public_key_bytes();
|
|
assert_eq!(pk.len(), 32);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sign_and_verify() {
|
|
let node = NodeIdentity::generate();
|
|
let message = b"hello lattice";
|
|
|
|
let signature = node.sign(message);
|
|
assert!(node.verify(message, &signature).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_verify_wrong_message() {
|
|
let node = NodeIdentity::generate();
|
|
let signature = node.sign(b"original");
|
|
|
|
assert!(node.verify(b"tampered", &signature).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_verify_with_different_key() {
|
|
let node1 = NodeIdentity::generate();
|
|
let node2 = NodeIdentity::generate();
|
|
|
|
let signature = node1.sign(b"message");
|
|
assert!(node2.verify(b"message", &signature).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_save_and_load() {
|
|
let temp_path = temp_dir().join("lattice_test_identity.key");
|
|
|
|
// Generate and save
|
|
let node1 = NodeIdentity::generate();
|
|
let pk1 = node1.public_key_bytes();
|
|
node1.save(&temp_path).unwrap();
|
|
|
|
// Load and verify same key
|
|
let node2 = NodeIdentity::load(&temp_path).unwrap();
|
|
let pk2 = node2.public_key_bytes();
|
|
|
|
assert_eq!(pk1, pk2);
|
|
|
|
// Cleanup
|
|
fs::remove_file(&temp_path).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn test_load_or_generate() {
|
|
let temp_path = temp_dir().join("lattice_test_identity2.key");
|
|
|
|
// Remove if exists
|
|
fs::remove_file(&temp_path).ok();
|
|
|
|
// First call: generates
|
|
let node1 = NodeIdentity::load_or_generate(&temp_path).unwrap();
|
|
let pk1 = node1.public_key_bytes();
|
|
|
|
// Second call: loads existing
|
|
let node2 = NodeIdentity::load_or_generate(&temp_path).unwrap();
|
|
let pk2 = node2.public_key_bytes();
|
|
|
|
assert_eq!(pk1, pk2);
|
|
|
|
// Cleanup
|
|
fs::remove_file(&temp_path).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn test_verify_with_key_static() {
|
|
let node = NodeIdentity::generate();
|
|
let pk = node.public_key();
|
|
let message = b"test message";
|
|
|
|
let signature = node.sign(message);
|
|
|
|
assert!(NodeIdentity::verify_with_key(&pk, message, &signature).is_ok());
|
|
}
|
|
}
|