use aingle_graph::{Triple, TripleId};
use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
use rand::RngCore;
use std::path::Path;
pub struct NodeIdentity {
signing_key: SigningKey,
}
impl NodeIdentity {
pub fn load_or_generate(data_dir: &Path) -> std::io::Result<Self> {
let key_path = data_dir.join("node.key");
if key_path.exists() {
let seed = std::fs::read(&key_path)?;
if seed.len() != 32 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"node.key must be exactly 32 bytes",
));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&seed);
Ok(Self {
signing_key: SigningKey::from_bytes(&arr),
})
} else {
let mut rng = rand::rng();
let mut seed = [0u8; 32];
rng.fill_bytes(&mut seed);
std::fs::create_dir_all(data_dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.mode(0o600)
.open(&key_path)?;
f.write_all(&seed)?;
f.sync_all()?;
}
#[cfg(not(unix))]
{
let mut f = std::fs::File::create(&key_path)?;
std::io::Write::write_all(&mut f, &seed)?;
f.sync_all()?;
}
Ok(Self {
signing_key: SigningKey::from_bytes(&seed),
})
}
}
pub fn node_id(&self) -> String {
hex::encode(self.signing_key.verifying_key().to_bytes())
}
pub fn public_key(&self) -> [u8; 32] {
self.signing_key.verifying_key().to_bytes()
}
pub fn sign(&self, data: &[u8]) -> [u8; 64] {
self.signing_key.sign(data).to_bytes()
}
}
pub fn verify(pubkey: &[u8; 32], data: &[u8], sig: &[u8; 64]) -> bool {
let Ok(vk) = VerifyingKey::from_bytes(pubkey) else {
return false;
};
let signature = ed25519_dalek::Signature::from_bytes(sig);
vk.verify(data, &signature).is_ok()
}
pub fn triple_hash(triple: &Triple) -> [u8; 32] {
TripleId::from_triple(triple).0
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn generate_creates_key_file() {
let dir = TempDir::new().unwrap();
let _ = NodeIdentity::load_or_generate(dir.path()).unwrap();
assert!(dir.path().join("node.key").exists());
}
#[test]
fn load_returns_same_identity() {
let dir = TempDir::new().unwrap();
let id1 = NodeIdentity::load_or_generate(dir.path()).unwrap();
let id2 = NodeIdentity::load_or_generate(dir.path()).unwrap();
assert_eq!(id1.node_id(), id2.node_id());
}
#[test]
fn sign_and_verify() {
let dir = TempDir::new().unwrap();
let id = NodeIdentity::load_or_generate(dir.path()).unwrap();
let data = b"hello cortex p2p";
let sig = id.sign(data);
assert!(verify(&id.public_key(), data, &sig));
}
#[test]
fn verify_rejects_bad_signature() {
let dir = TempDir::new().unwrap();
let id = NodeIdentity::load_or_generate(dir.path()).unwrap();
let data = b"hello cortex p2p";
let mut sig = id.sign(data);
sig[0] ^= 0xff;
assert!(!verify(&id.public_key(), data, &sig));
}
#[cfg(unix)]
#[test]
fn file_permissions_are_restrictive() {
use std::os::unix::fs::MetadataExt;
let dir = TempDir::new().unwrap();
let _ = NodeIdentity::load_or_generate(dir.path()).unwrap();
let meta = std::fs::metadata(dir.path().join("node.key")).unwrap();
assert_eq!(meta.mode() & 0o777, 0o600);
}
#[test]
fn node_id_is_64_hex_chars() {
let dir = TempDir::new().unwrap();
let id = NodeIdentity::load_or_generate(dir.path()).unwrap();
let nid = id.node_id();
assert_eq!(nid.len(), 64);
assert!(nid.chars().all(|c| c.is_ascii_hexdigit()));
}
}