lean_ctx/core/
agent_identity.rs1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
2use std::path::{Path, PathBuf};
3
4pub fn get_or_create_keypair(agent_id: &str) -> Result<SigningKey, String> {
5 let path = key_path(agent_id)?;
6 if path.exists() {
7 load_key(&path)
8 } else {
9 generate_and_save(agent_id)
10 }
11}
12
13pub fn get_public_key(agent_id: &str) -> Result<VerifyingKey, String> {
14 let key = get_or_create_keypair(agent_id)?;
15 Ok(key.verifying_key())
16}
17
18pub fn sign_bytes(agent_id: &str, data: &[u8]) -> Result<Vec<u8>, String> {
19 let key = get_or_create_keypair(agent_id)?;
20 let sig = key.sign(data);
21 Ok(sig.to_bytes().to_vec())
22}
23
24pub fn verify_signature(public_key_bytes: &[u8], data: &[u8], signature_bytes: &[u8]) -> bool {
25 let pk_bytes: [u8; 32] = match public_key_bytes.try_into() {
26 Ok(b) => b,
27 Err(_) => return false,
28 };
29 let Ok(verifying_key) = VerifyingKey::from_bytes(&pk_bytes) else {
30 return false;
31 };
32 let sig_bytes: [u8; 64] = match signature_bytes.try_into() {
33 Ok(b) => b,
34 Err(_) => return false,
35 };
36 let signature = Signature::from_bytes(&sig_bytes);
37 verifying_key.verify(data, &signature).is_ok()
38}
39
40pub fn hex_encode(bytes: &[u8]) -> String {
41 use std::fmt::Write;
42 bytes.iter().fold(String::new(), |mut s, b| {
43 let _ = write!(s, "{b:02x}");
44 s
45 })
46}
47
48pub fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
49 if !s.len().is_multiple_of(2) {
50 return Err("odd-length hex string".to_string());
51 }
52 (0..s.len())
53 .step_by(2)
54 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
55 .collect()
56}
57
58fn key_path(agent_id: &str) -> Result<PathBuf, String> {
59 let base = crate::core::data_dir::lean_ctx_data_dir()?;
60 Ok(base.join("keys").join(format!("{agent_id}.key")))
61}
62
63fn pub_key_path(agent_id: &str) -> Result<PathBuf, String> {
64 let base = crate::core::data_dir::lean_ctx_data_dir()?;
65 Ok(base.join("keys").join(format!("{agent_id}.pub")))
66}
67
68fn generate_and_save(agent_id: &str) -> Result<SigningKey, String> {
69 let mut seed = [0u8; 32];
70 getrandom::fill(&mut seed).map_err(|e| format!("CSPRNG unavailable: {e}"))?;
71 let signing_key = SigningKey::from_bytes(&seed);
72
73 let key_file = key_path(agent_id)?;
74 let pub_file = pub_key_path(agent_id)?;
75
76 if let Some(parent) = key_file.parent() {
77 std::fs::create_dir_all(parent).map_err(|e| format!("mkdir keys: {e}"))?;
78 }
79
80 std::fs::write(&key_file, signing_key.to_bytes()).map_err(|e| format!("write key: {e}"))?;
81
82 #[cfg(unix)]
83 {
84 use std::os::unix::fs::PermissionsExt;
85 let perms = std::fs::Permissions::from_mode(0o600);
86 let _ = std::fs::set_permissions(&key_file, perms);
87 }
88
89 let pub_bytes = signing_key.verifying_key().to_bytes();
90 std::fs::write(&pub_file, pub_bytes).map_err(|e| format!("write pub: {e}"))?;
91
92 Ok(signing_key)
93}
94
95fn load_key(path: &Path) -> Result<SigningKey, String> {
96 let bytes = std::fs::read(path).map_err(|e| format!("read key: {e}"))?;
97 let arr: [u8; 32] = bytes
98 .try_into()
99 .map_err(|_| "invalid key file (expected 32 bytes)".to_string())?;
100 Ok(SigningKey::from_bytes(&arr))
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn sign_and_verify_roundtrip() {
109 let mut seed = [0u8; 32];
110 getrandom::fill(&mut seed).unwrap();
111 let key = SigningKey::from_bytes(&seed);
112 let data = b"test payload";
113 let sig = key.sign(data);
114
115 let pub_bytes = key.verifying_key().to_bytes();
116 assert!(verify_signature(&pub_bytes, data, &sig.to_bytes()));
117 }
118
119 #[test]
120 fn verify_rejects_tampered_data() {
121 let mut seed = [0u8; 32];
122 getrandom::fill(&mut seed).unwrap();
123 let key = SigningKey::from_bytes(&seed);
124 let sig = key.sign(b"original");
125
126 let pub_bytes = key.verifying_key().to_bytes();
127 assert!(!verify_signature(&pub_bytes, b"tampered", &sig.to_bytes()));
128 }
129
130 #[test]
131 fn hex_roundtrip() {
132 let data = vec![0xde, 0xad, 0xbe, 0xef];
133 let encoded = hex_encode(&data);
134 assert_eq!(encoded, "deadbeef");
135 let decoded = hex_decode(&encoded).unwrap();
136 assert_eq!(decoded, data);
137 }
138}