agentmux_srv/backend/storage/
agent_lan_keys.rs1use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
18use rusqlite::params;
19
20use super::error::StoreError;
21use super::store::Store;
22
23use agentmux_common::time::now_secs;
24
25pub(super) fn random_seed_bytes() -> [u8; 32] {
32 let mut bytes = [0u8; 32];
33 bytes[..16].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
34 bytes[16..].copy_from_slice(uuid::Uuid::new_v4().as_bytes());
35 bytes
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct AgentLanKeypair {
42 pub public_key: String,
43 pub private_key: String,
44}
45
46impl Store {
47 pub fn agent_lan_key_load(&self, agent_id: &str) -> Result<Option<AgentLanKeypair>, StoreError> {
50 let key = agent_id.to_lowercase();
51 let conn = self.conn.lock().unwrap();
52 let mut stmt =
53 conn.prepare("SELECT public_key, private_key FROM db_agent_lan_keys WHERE agent_id = ?1")?;
54 match stmt.query_row(params![key], |row| {
55 Ok(AgentLanKeypair {
56 public_key: row.get(0)?,
57 private_key: row.get(1)?,
58 })
59 }) {
60 Ok(v) => Ok(Some(v)),
61 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
62 Err(e) => Err(e.into()),
63 }
64 }
65
66 pub fn agent_lan_public_key_load(&self, agent_id: &str) -> Result<Option<String>, StoreError> {
71 let key = agent_id.to_lowercase();
72 let conn = self.conn.lock().unwrap();
73 let mut stmt = conn.prepare("SELECT public_key FROM db_agent_lan_keys WHERE agent_id = ?1")?;
74 match stmt.query_row(params![key], |row| row.get(0)) {
75 Ok(v) => Ok(Some(v)),
76 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
77 Err(e) => Err(e.into()),
78 }
79 }
80
81 pub fn agent_lan_key_ensure(&self, agent_id: &str) -> Result<AgentLanKeypair, StoreError> {
87 let key = agent_id.to_lowercase();
88 if let Some(existing) = self.agent_lan_key_load(&key)? {
89 return Ok(existing);
90 }
91 let conn = self.conn.lock().unwrap();
92 let seed = random_seed_bytes();
93 let (public_bytes, private_bytes) = agentmux_common::jekt_sign::generate_lan_keypair(seed);
94 let public_key = BASE64.encode(public_bytes);
95 let private_key = BASE64.encode(private_bytes);
96 conn.execute(
97 "INSERT OR IGNORE INTO db_agent_lan_keys (agent_id, public_key, private_key, created_at) \
98 VALUES (?1, ?2, ?3, ?4)",
99 params![key, public_key, private_key, now_secs()],
100 )?;
101 let mut stmt =
105 conn.prepare("SELECT public_key, private_key FROM db_agent_lan_keys WHERE agent_id = ?1")?;
106 stmt.query_row(params![key], |row| {
107 Ok(AgentLanKeypair {
108 public_key: row.get(0)?,
109 private_key: row.get(1)?,
110 })
111 })
112 .map_err(Into::into)
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn object_store() -> Store {
121 Store::open(std::path::Path::new(":memory:")).unwrap()
122 }
123
124 #[test]
125 fn load_returns_none_when_unminted() {
126 let store = object_store();
127 assert!(store.agent_lan_key_load("agentx").unwrap().is_none());
128 assert!(store.agent_lan_public_key_load("agentx").unwrap().is_none());
129 }
130
131 #[test]
132 fn ensure_mints_a_valid_base64_keypair() {
133 let store = object_store();
134 let keypair = store.agent_lan_key_ensure("agentx").unwrap();
135 let pub_bytes = BASE64.decode(&keypair.public_key).unwrap();
136 let priv_bytes = BASE64.decode(&keypair.private_key).unwrap();
137 assert_eq!(pub_bytes.len(), 32);
138 assert_eq!(priv_bytes.len(), 32);
139 assert_ne!(keypair.public_key, keypair.private_key);
140 }
141
142 #[test]
143 fn ensure_is_idempotent_same_agent_gets_same_keypair() {
144 let store = object_store();
145 let first = store.agent_lan_key_ensure("AgentX").unwrap();
146 let second = store.agent_lan_key_ensure("agentx").unwrap(); assert_eq!(first, second);
148 }
149
150 #[test]
151 fn different_agents_get_different_keypairs() {
152 let store = object_store();
153 let a = store.agent_lan_key_ensure("agentx").unwrap();
154 let b = store.agent_lan_key_ensure("agenty").unwrap();
155 assert_ne!(a.public_key, b.public_key);
156 assert_ne!(a.private_key, b.private_key);
157 }
158
159 #[test]
160 fn ensure_then_load_round_trips() {
161 let store = object_store();
162 let minted = store.agent_lan_key_ensure("agentx").unwrap();
163 let loaded = store.agent_lan_key_load("agentx").unwrap().unwrap();
164 assert_eq!(minted, loaded);
165 }
166
167 #[test]
168 fn public_key_load_matches_the_public_half_of_the_full_keypair() {
169 let store = object_store();
170 let minted = store.agent_lan_key_ensure("agentx").unwrap();
171 let public_only = store.agent_lan_public_key_load("agentx").unwrap().unwrap();
172 assert_eq!(minted.public_key, public_only);
173 }
174
175 #[test]
176 fn a_minted_keypair_can_sign_and_verify_a_real_jekt() {
177 let store = object_store();
181 let keypair = store.agent_lan_key_ensure("agentx").unwrap();
182 let private = BASE64.decode(&keypair.private_key).unwrap();
183 let public = BASE64.decode(&keypair.public_key).unwrap();
184 let sig = agentmux_common::jekt_sign::sign_lan_jekt(
185 &private, "msg-1", "agentx", "agenty", 1_000, "hello",
186 )
187 .unwrap();
188 assert!(agentmux_common::jekt_sign::verify_lan_jekt(
189 &public, "msg-1", "agentx", "agenty", 1_000, "hello", &sig,
190 ));
191 }
192}