lean_ctx/core/
agent_identity.rs1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
2use std::path::{Path, PathBuf};
3use std::sync::OnceLock;
4
5#[must_use]
10pub fn current_agent_id() -> &'static str {
11 static CACHE: OnceLock<String> = OnceLock::new();
12 CACHE.get_or_init(|| {
13 std::env::var("LEAN_CTX_AGENT_ID")
14 .or_else(|_| std::env::var("LCTX_AGENT_ID"))
15 .unwrap_or_else(|_| "local".to_string())
16 })
17}
18
19pub fn get_or_create_keypair(agent_id: &str) -> Result<SigningKey, String> {
20 let path = key_path(agent_id)?;
21 if path.exists() {
22 load_key(&path)
23 } else {
24 generate_and_save(agent_id)
25 }
26}
27
28pub fn get_public_key(agent_id: &str) -> Result<VerifyingKey, String> {
29 let key = get_or_create_keypair(agent_id)?;
30 Ok(key.verifying_key())
31}
32
33pub fn sign_bytes(agent_id: &str, data: &[u8]) -> Result<Vec<u8>, String> {
34 let key = get_or_create_keypair(agent_id)?;
35 let sig = key.sign(data);
36 Ok(sig.to_bytes().to_vec())
37}
38
39pub fn sign_with_public_key(
49 agent_id: &str,
50 data: &[u8],
51) -> Result<(Vec<u8>, VerifyingKey), String> {
52 let key = get_or_create_keypair(agent_id)?;
53 let sig = key.sign(data);
54 Ok((sig.to_bytes().to_vec(), key.verifying_key()))
55}
56
57#[must_use]
61pub fn sign_bytes_with(key: &SigningKey, data: &[u8]) -> Vec<u8> {
62 key.sign(data).to_bytes().to_vec()
63}
64
65pub fn verify_signature(public_key_bytes: &[u8], data: &[u8], signature_bytes: &[u8]) -> bool {
66 let pk_bytes: [u8; 32] = match public_key_bytes.try_into() {
67 Ok(b) => b,
68 Err(_) => return false,
69 };
70 let Ok(verifying_key) = VerifyingKey::from_bytes(&pk_bytes) else {
71 return false;
72 };
73 let sig_bytes: [u8; 64] = match signature_bytes.try_into() {
74 Ok(b) => b,
75 Err(_) => return false,
76 };
77 let signature = Signature::from_bytes(&sig_bytes);
78 verifying_key.verify(data, &signature).is_ok()
79}
80
81pub fn hex_encode(bytes: &[u8]) -> String {
82 use std::fmt::Write;
83 bytes.iter().fold(String::new(), |mut s, b| {
84 let _ = write!(s, "{b:02x}");
85 s
86 })
87}
88
89pub fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
90 if !s.len().is_multiple_of(2) {
91 return Err("odd-length hex string".to_string());
92 }
93 (0..s.len())
94 .step_by(2)
95 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string()))
96 .collect()
97}
98
99fn key_path(agent_id: &str) -> Result<PathBuf, String> {
100 let base = crate::core::data_dir::lean_ctx_data_dir()?;
101 Ok(base.join("keys").join(format!("{agent_id}.key")))
102}
103
104fn pub_key_path(agent_id: &str) -> Result<PathBuf, String> {
105 let base = crate::core::data_dir::lean_ctx_data_dir()?;
106 Ok(base.join("keys").join(format!("{agent_id}.pub")))
107}
108
109fn generate_and_save(agent_id: &str) -> Result<SigningKey, String> {
110 let mut seed = [0u8; 32];
111 getrandom::fill(&mut seed).map_err(|e| format!("CSPRNG unavailable: {e}"))?;
112 let signing_key = SigningKey::from_bytes(&seed);
113
114 let key_file = key_path(agent_id)?;
115 let pub_file = pub_key_path(agent_id)?;
116
117 if let Some(parent) = key_file.parent() {
118 std::fs::create_dir_all(parent).map_err(|e| format!("mkdir keys: {e}"))?;
119 }
120
121 std::fs::write(&key_file, signing_key.to_bytes()).map_err(|e| format!("write key: {e}"))?;
122
123 #[cfg(unix)]
124 {
125 use std::os::unix::fs::PermissionsExt;
126 let perms = std::fs::Permissions::from_mode(0o600);
127 let _ = std::fs::set_permissions(&key_file, perms);
128 }
129
130 let pub_bytes = signing_key.verifying_key().to_bytes();
131 std::fs::write(&pub_file, pub_bytes).map_err(|e| format!("write pub: {e}"))?;
132
133 Ok(signing_key)
134}
135
136fn load_key(path: &Path) -> Result<SigningKey, String> {
137 let bytes = std::fs::read(path).map_err(|e| format!("read key: {e}"))?;
138 let arr: [u8; 32] = bytes
139 .try_into()
140 .map_err(|_| "invalid key file (expected 32 bytes)".to_string())?;
141 Ok(SigningKey::from_bytes(&arr))
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn sign_and_verify_roundtrip() {
150 let mut seed = [0u8; 32];
151 getrandom::fill(&mut seed).unwrap();
152 let key = SigningKey::from_bytes(&seed);
153 let data = b"test payload";
154 let sig = key.sign(data);
155
156 let pub_bytes = key.verifying_key().to_bytes();
157 assert!(verify_signature(&pub_bytes, data, &sig.to_bytes()));
158 }
159
160 #[test]
161 fn verify_rejects_tampered_data() {
162 let mut seed = [0u8; 32];
163 getrandom::fill(&mut seed).unwrap();
164 let key = SigningKey::from_bytes(&seed);
165 let sig = key.sign(b"original");
166
167 let pub_bytes = key.verifying_key().to_bytes();
168 assert!(!verify_signature(&pub_bytes, b"tampered", &sig.to_bytes()));
169 }
170
171 #[test]
172 fn hex_roundtrip() {
173 let data = vec![0xde, 0xad, 0xbe, 0xef];
174 let encoded = hex_encode(&data);
175 assert_eq!(encoded, "deadbeef");
176 let decoded = hex_decode(&encoded).unwrap();
177 assert_eq!(decoded, data);
178 }
179}