use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use rand_core::OsRng; use std::path::Path; #[derive(Debug, thiserror::Error)] pub enum CryptoError { #[error("io error: {0}")] Io(#[from] std::io::Error), #[error("invalid hex key: {0}")] Hex(#[from] hex::FromHexError), #[error("invalid ed25519 key material")] InvalidKey, #[error("signature verification failed")] Verify, } pub struct AgentKey { signing_key: SigningKey, } impl AgentKey { #[must_use] pub fn generate() -> Self { Self { signing_key: SigningKey::generate(&mut OsRng), } } pub fn load_or_create(path: &Path) -> Result { if path.exists() { return Self::load(path); } let key = Self::generate(); key.save(path)?; Ok(key) } pub fn load(path: &Path) -> Result { let hex_key = std::fs::read_to_string(path)?; let bytes = hex::decode(hex_key.trim())?; let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| CryptoError::InvalidKey)?; Ok(Self { signing_key: SigningKey::from_bytes(&key_bytes), }) } pub fn save(&self, path: &Path) -> Result<(), CryptoError> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } let tmp = path.with_extension("tmp"); std::fs::write(&tmp, hex::encode(self.signing_key.to_bytes()))?; std::fs::rename(tmp, path)?; Ok(()) } #[must_use] pub fn verifying_key(&self) -> VerifyingKey { self.signing_key.verifying_key() } #[must_use] pub fn public_key_hex(&self) -> String { hex::encode(self.verifying_key().to_bytes()) } #[must_use] pub fn agent_id(&self) -> geth_types::AgentId { geth_types::AgentId::new(key_fingerprint(&self.verifying_key().to_bytes())) } #[must_use] pub fn sign(&self, bytes: &[u8]) -> Vec { self.signing_key.sign(bytes).to_bytes().to_vec() } } pub fn verify(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), CryptoError> { let key_bytes: [u8; 32] = public_key.try_into().map_err(|_| CryptoError::InvalidKey)?; let verifying_key = VerifyingKey::from_bytes(&key_bytes).map_err(|_| CryptoError::InvalidKey)?; let sig = Signature::from_slice(signature).map_err(|_| CryptoError::InvalidKey)?; verifying_key .verify(message, &sig) .map_err(|_| CryptoError::Verify) } #[must_use] pub fn blake3_hex(bytes: &[u8]) -> String { blake3::hash(bytes).to_hex().to_string() } #[must_use] pub fn key_fingerprint(public_key: &[u8]) -> String { format!("ed25519:{}", blake3_hex(public_key)) } #[cfg(test)] mod tests { use super::*; #[test] fn agent_identity_persists() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("agent.ed25519"); let first = AgentKey::load_or_create(&path).expect("create"); let second = AgentKey::load_or_create(&path).expect("load"); assert_eq!(first.agent_id(), second.agent_id()); } #[test] fn blake3_helper_matches_known_hash() { assert_eq!( blake3_hex(b"hello geth"), "3a4aa805ade0d4694a1bb69ad5b9a2f1dffcd4de2136df9a465023722d26e325" ); } }