agent_mesh_protocol/
user_key.rs1use crate::fingerprint::Fingerprint;
10use crate::{MeshError, Result};
11use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
12use ed25519_dalek::pkcs8::{DecodePrivateKey, EncodePrivateKey};
13use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
14use rand::rngs::OsRng;
15use serde::{Deserialize, Serialize};
16use std::path::Path;
17use zeroize::Zeroize;
18
19pub struct UserKey {
25 signing: SigningKey,
26}
27
28impl UserKey {
29 #[must_use]
31 pub fn generate() -> Self {
32 let mut csprng = OsRng;
33 let signing = SigningKey::generate(&mut csprng);
34 Self { signing }
35 }
36
37 #[must_use]
39 pub fn public(&self) -> UserPublic {
40 UserPublic {
41 verifying: self.signing.verifying_key(),
42 }
43 }
44
45 #[must_use]
47 pub fn fingerprint(&self) -> Fingerprint {
48 self.public().fingerprint()
49 }
50
51 pub fn sign(&self, message: &[u8]) -> Signature {
56 self.signing.sign(message)
57 }
58
59 pub fn save(&self, path: &Path) -> Result<()> {
66 if path.exists() {
67 return Err(MeshError::Io(std::io::Error::new(
68 std::io::ErrorKind::AlreadyExists,
69 format!("refusing to overwrite existing key at {}", path.display()),
70 )));
71 }
72 if let Some(parent) = path.parent() {
73 if !parent.as_os_str().is_empty() {
74 std::fs::create_dir_all(parent)?;
75 }
76 }
77 let pem = self
78 .signing
79 .to_pkcs8_pem(LineEnding::LF)
80 .map_err(|e| MeshError::InvalidKey(e.to_string()))?;
81 std::fs::write(path, pem.as_bytes())?;
82 #[cfg(unix)]
83 {
84 use std::os::unix::fs::PermissionsExt;
85 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
86 }
87 Ok(())
88 }
89
90 pub fn load(path: &Path) -> Result<Self> {
92 let pem = std::fs::read_to_string(path)?;
93 let signing =
94 SigningKey::from_pkcs8_pem(&pem).map_err(|e| MeshError::InvalidKey(e.to_string()))?;
95 Ok(Self { signing })
96 }
97}
98
99impl std::fmt::Debug for UserKey {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("UserKey")
103 .field("fingerprint", &self.fingerprint())
104 .finish_non_exhaustive()
105 }
106}
107
108impl Drop for UserKey {
109 fn drop(&mut self) {
110 let mut bytes = self.signing.to_bytes();
114 bytes.zeroize();
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct UserPublic {
122 #[serde(with = "verifying_key_serde")]
123 pub verifying: VerifyingKey,
124}
125
126impl UserPublic {
127 #[must_use]
130 pub fn fingerprint(&self) -> Fingerprint {
131 Fingerprint::of_bytes(self.verifying.as_bytes())
132 }
133
134 pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<()> {
137 self.verifying
138 .verify(message, signature)
139 .map_err(|_| MeshError::BadSignature)
140 }
141
142 #[must_use]
144 pub fn as_bytes(&self) -> [u8; 32] {
145 *self.verifying.as_bytes()
146 }
147}
148
149mod verifying_key_serde {
150 use ed25519_dalek::VerifyingKey;
151 use serde::{Deserialize, Deserializer, Serialize, Serializer};
152
153 pub fn serialize<S: Serializer>(key: &VerifyingKey, ser: S) -> Result<S::Ok, S::Error> {
154 let bytes: &[u8] = key.as_bytes();
155 bytes.serialize(ser)
156 }
157
158 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<VerifyingKey, D::Error> {
159 let bytes: Vec<u8> = Vec::deserialize(de)?;
160 if bytes.len() != 32 {
161 return Err(serde::de::Error::custom("expected 32 bytes"));
162 }
163 let mut arr = [0u8; 32];
164 arr.copy_from_slice(&bytes);
165 VerifyingKey::from_bytes(&arr).map_err(serde::de::Error::custom)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use tempfile::TempDir;
173
174 #[test]
175 fn generate_different_keys() {
176 let a = UserKey::generate();
177 let b = UserKey::generate();
178 assert_ne!(
179 a.fingerprint(),
180 b.fingerprint(),
181 "two fresh keys must not collide"
182 );
183 }
184
185 #[test]
186 fn roundtrip_save_load_disk() {
187 let dir = TempDir::new().unwrap();
188 let path = dir.path().join("user.key");
189 let key = UserKey::generate();
190 let fp = key.fingerprint();
191 key.save(&path).expect("save");
192 let loaded = UserKey::load(&path).expect("load");
193 assert_eq!(loaded.fingerprint(), fp);
194 }
195
196 #[test]
197 fn save_refuses_overwrite() {
198 let dir = TempDir::new().unwrap();
199 let path = dir.path().join("user.key");
200 let key = UserKey::generate();
201 key.save(&path).expect("first save");
202 let key2 = UserKey::generate();
203 let err = key2.save(&path).expect_err("must refuse");
204 match err {
205 MeshError::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::AlreadyExists),
206 other => panic!("expected Io(AlreadyExists), got {other:?}"),
207 }
208 }
209
210 #[test]
211 fn save_creates_parent_directory() {
212 let dir = TempDir::new().unwrap();
213 let path = dir.path().join("nested").join("dir").join("user.key");
214 UserKey::generate().save(&path).expect("save with mkdir -p");
215 assert!(path.exists());
216 }
217
218 #[test]
219 #[cfg(unix)]
220 fn save_sets_0600_permissions() {
221 use std::os::unix::fs::PermissionsExt;
222 let dir = TempDir::new().unwrap();
223 let path = dir.path().join("user.key");
224 UserKey::generate().save(&path).expect("save");
225 let mode = std::fs::metadata(&path).unwrap().permissions().mode();
226 assert_eq!(mode & 0o777, 0o600, "expected 0600, got {mode:o}");
227 }
228
229 #[test]
230 fn sign_verify() {
231 let key = UserKey::generate();
232 let pubk = key.public();
233 let msg = b"hello agent-mesh";
234 let sig = key.sign(msg);
235 pubk.verify(msg, &sig).expect("verify own signature");
236 }
237
238 #[test]
239 fn wrong_message_fails_verify() {
240 let key = UserKey::generate();
241 let pubk = key.public();
242 let sig = key.sign(b"original");
243 let err = pubk.verify(b"tampered", &sig).expect_err("must fail");
244 assert!(matches!(err, MeshError::BadSignature));
245 }
246
247 #[test]
248 fn fingerprint_stable_across_loads() {
249 let dir = TempDir::new().unwrap();
250 let path = dir.path().join("user.key");
251 let key = UserKey::generate();
252 let fp1 = key.fingerprint();
253 key.save(&path).unwrap();
254 drop(key);
255 let loaded = UserKey::load(&path).unwrap();
256 let fp2 = loaded.fingerprint();
257 assert_eq!(fp1, fp2);
258 }
259
260 #[test]
261 fn serde_roundtrip_public() {
262 let key = UserKey::generate();
263 let pubk = key.public();
264 let json = serde_json::to_string(&pubk).unwrap();
265 let parsed: UserPublic = serde_json::from_str(&json).unwrap();
266 assert_eq!(parsed, pubk);
267 assert_eq!(parsed.fingerprint(), pubk.fingerprint());
268 }
269
270 #[test]
271 fn public_as_bytes_is_32() {
272 let key = UserKey::generate();
273 let bytes = key.public().as_bytes();
274 assert_eq!(bytes.len(), 32);
275 }
276
277 #[test]
278 fn load_fails_on_missing_file() {
279 let dir = TempDir::new().unwrap();
280 let path = dir.path().join("nope.key");
281 let err = UserKey::load(&path).expect_err("must fail");
282 assert!(matches!(err, MeshError::Io(_)));
283 }
284
285 #[test]
286 fn load_fails_on_garbage_file() {
287 let dir = TempDir::new().unwrap();
288 let path = dir.path().join("garbage");
289 std::fs::write(&path, b"not a pem").unwrap();
290 let err = UserKey::load(&path).expect_err("must fail");
291 assert!(matches!(err, MeshError::InvalidKey(_)));
292 }
293}