1use std::fs;
7use std::io::Write;
8use std::path::PathBuf;
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12
13use crate::error::TrustError;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct PinnedIdentity {
21 pub did: String,
23
24 pub public_key_hex: String,
29
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub kel_tip_said: Option<String>,
33
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub kel_sequence: Option<u64>,
37
38 pub first_seen: DateTime<Utc>,
40
41 pub origin: String,
43
44 pub trust_level: TrustLevel,
46}
47
48impl PinnedIdentity {
49 pub fn public_key_bytes(&self) -> Result<Vec<u8>, TrustError> {
53 hex::decode(&self.public_key_hex).map_err(|e| {
54 TrustError::InvalidData(format!("Corrupt pin for {}: invalid hex: {}", self.did, e))
55 })
56 }
57
58 pub fn key_matches(&self, presented_pk: &[u8]) -> Result<bool, TrustError> {
63 Ok(self.public_key_bytes()? == presented_pk)
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69#[serde(rename_all = "snake_case")]
70pub enum TrustLevel {
71 Tofu,
73
74 Manual,
76
77 OrgPolicy,
79}
80
81pub struct PinnedIdentityStore {
100 path: PathBuf,
101}
102
103impl PinnedIdentityStore {
104 pub fn new(path: PathBuf) -> Self {
106 Self { path }
107 }
108
109 pub fn default_path() -> PathBuf {
111 dirs::home_dir()
112 .unwrap_or_else(|| PathBuf::from("."))
113 .join(".auths")
114 .join("known_identities.json")
115 }
116
117 pub fn lookup(&self, did: &str) -> Result<Option<PinnedIdentity>, TrustError> {
119 let _lock = self.lock()?;
120 Ok(self.read_all()?.into_iter().find(|e| e.did == did))
121 }
122
123 pub fn pin(&self, identity: PinnedIdentity) -> Result<(), TrustError> {
128 let _ = hex::decode(&identity.public_key_hex)
129 .map_err(|e| TrustError::InvalidData(format!("Invalid public_key_hex: {}", e)))?;
130
131 let _lock = self.lock()?;
132 let mut entries = self.read_all()?;
133 if entries.iter().any(|e| e.did == identity.did) {
134 return Err(TrustError::AlreadyExists(format!(
135 "Identity {} already pinned. Use `auths trust remove` first, or rotation \
136 will be handled automatically via continuity checking.",
137 identity.did
138 )));
139 }
140 entries.push(identity);
141 self.write_all(&entries)
142 }
143
144 pub fn update(&self, identity: PinnedIdentity) -> Result<(), TrustError> {
146 let _lock = self.lock()?;
147 let mut entries = self.read_all()?;
148 let pos = entries
149 .iter()
150 .position(|e| e.did == identity.did)
151 .ok_or_else(|| {
152 TrustError::NotFound(format!(
153 "Cannot update: identity {} not found in pin store.",
154 identity.did
155 ))
156 })?;
157 entries[pos] = identity;
158 self.write_all(&entries)
159 }
160
161 pub fn remove(&self, did: &str) -> Result<bool, TrustError> {
165 let _lock = self.lock()?;
166 let mut entries = self.read_all()?;
167 let before = entries.len();
168 entries.retain(|e| e.did != did);
169 if entries.len() < before {
170 self.write_all(&entries)?;
171 Ok(true)
172 } else {
173 Ok(false)
174 }
175 }
176
177 pub fn list(&self) -> Result<Vec<PinnedIdentity>, TrustError> {
179 let _lock = self.lock()?;
180 self.read_all()
181 }
182
183 fn read_all(&self) -> Result<Vec<PinnedIdentity>, TrustError> {
186 if !self.path.exists() {
187 return Ok(vec![]);
188 }
189 let content = fs::read_to_string(&self.path)?;
190 let entries: Vec<PinnedIdentity> = serde_json::from_str(&content).map_err(|e| {
191 TrustError::InvalidData(format!(
192 "Corrupt pin store at {:?}: {}. Consider deleting and re-pinning.",
193 self.path, e
194 ))
195 })?;
196 Ok(entries)
197 }
198
199 fn write_all(&self, entries: &[PinnedIdentity]) -> Result<(), TrustError> {
200 if let Some(parent) = self.path.parent() {
201 fs::create_dir_all(parent)?;
202 }
203 let tmp = self.path.with_extension("tmp");
204 {
205 let mut file = fs::File::create(&tmp)?;
206 let json = serde_json::to_string_pretty(entries)?;
207 file.write_all(json.as_bytes())?;
208 file.write_all(b"\n")?;
209 file.sync_all()?;
210 }
211 fs::rename(&tmp, &self.path)?;
212 Ok(())
213 }
214
215 fn lock(&self) -> Result<LockGuard, TrustError> {
216 let lock_path = self.path.with_extension("lock");
217 if let Some(parent) = lock_path.parent() {
218 fs::create_dir_all(parent)?;
219 }
220 LockGuard::acquire(lock_path)
221 }
222}
223
224struct LockGuard {
229 _file: fs::File,
230}
231
232impl LockGuard {
233 fn acquire(path: PathBuf) -> Result<Self, TrustError> {
234 let file = fs::OpenOptions::new()
235 .create(true)
236 .truncate(true)
237 .write(true)
238 .open(&path)?;
239
240 #[cfg(unix)]
241 {
242 use std::os::unix::io::AsRawFd;
243 let fd = file.as_raw_fd();
244 let ret = unsafe { libc::flock(fd, libc::LOCK_EX) };
245 if ret != 0 {
246 return Err(TrustError::Lock(format!(
247 "Failed to acquire lock on {:?}",
248 path
249 )));
250 }
251 }
252
253 #[cfg(not(unix))]
254 {
255 }
257
258 Ok(Self { _file: file })
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn make_test_pin() -> PinnedIdentity {
267 PinnedIdentity {
268 did: "did:keri:ETest123".to_string(),
269 public_key_hex: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
270 .to_string(),
271 kel_tip_said: Some("ETip".to_string()),
272 kel_sequence: Some(0),
273 first_seen: Utc::now(),
274 origin: "test".to_string(),
275 trust_level: TrustLevel::Tofu,
276 }
277 }
278
279 #[test]
280 fn test_public_key_bytes_valid() {
281 let pin = make_test_pin();
282 let bytes = pin.public_key_bytes().unwrap();
283 assert_eq!(bytes.len(), 32);
284 assert_eq!(bytes[0], 0x01);
285 assert_eq!(bytes[31], 0x20);
286 }
287
288 #[test]
289 fn test_public_key_bytes_invalid_hex() {
290 let mut pin = make_test_pin();
291 pin.public_key_hex = "not-valid-hex".to_string();
292 let result = pin.public_key_bytes();
293 assert!(result.is_err());
294 assert!(result.unwrap_err().to_string().contains("Corrupt pin"));
295 }
296
297 #[test]
298 fn test_key_matches_true() {
299 let pin = make_test_pin();
300 let expected: Vec<u8> = (1..=32).collect();
301 assert!(pin.key_matches(&expected).unwrap());
302 }
303
304 #[test]
305 fn test_key_matches_false() {
306 let pin = make_test_pin();
307 let wrong: Vec<u8> = vec![0; 32];
308 assert!(!pin.key_matches(&wrong).unwrap());
309 }
310
311 #[test]
312 fn test_key_matches_case_insensitive() {
313 let mut pin = make_test_pin();
315 pin.public_key_hex =
316 "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20".to_string();
317 let expected: Vec<u8> = (1..=32).collect();
318 assert!(pin.key_matches(&expected).unwrap());
319 }
320
321 #[test]
322 fn test_trust_level_serialization() {
323 assert_eq!(
324 serde_json::to_string(&TrustLevel::Tofu).unwrap(),
325 "\"tofu\""
326 );
327 assert_eq!(
328 serde_json::to_string(&TrustLevel::Manual).unwrap(),
329 "\"manual\""
330 );
331 assert_eq!(
332 serde_json::to_string(&TrustLevel::OrgPolicy).unwrap(),
333 "\"org_policy\""
334 );
335 }
336
337 #[test]
338 fn test_pinned_identity_serialization_roundtrip() {
339 let pin = make_test_pin();
340 let json = serde_json::to_string(&pin).unwrap();
341 let parsed: PinnedIdentity = serde_json::from_str(&json).unwrap();
342
343 assert_eq!(pin.did, parsed.did);
344 assert_eq!(pin.public_key_hex, parsed.public_key_hex);
345 assert_eq!(pin.kel_tip_said, parsed.kel_tip_said);
346 assert_eq!(pin.kel_sequence, parsed.kel_sequence);
347 assert_eq!(pin.trust_level, parsed.trust_level);
348 }
349
350 #[test]
351 fn test_optional_fields_skipped() {
352 let mut pin = make_test_pin();
353 pin.kel_tip_said = None;
354 pin.kel_sequence = None;
355
356 let json = serde_json::to_string(&pin).unwrap();
357 assert!(!json.contains("kel_tip_said"));
358 assert!(!json.contains("kel_sequence"));
359 }
360
361 fn temp_store() -> (tempfile::TempDir, PinnedIdentityStore) {
364 let dir = tempfile::tempdir().unwrap();
365 let path = dir.path().join("known_identities.json");
366 let store = PinnedIdentityStore::new(path);
367 (dir, store)
368 }
369
370 #[test]
371 fn test_store_lookup_empty() {
372 let (_dir, store) = temp_store();
373 let result = store.lookup("did:keri:ENonexistent").unwrap();
374 assert!(result.is_none());
375 }
376
377 #[test]
378 fn test_store_pin_and_lookup() {
379 let (_dir, store) = temp_store();
380 let pin = make_test_pin();
381
382 store.pin(pin.clone()).unwrap();
383
384 let found = store.lookup(&pin.did).unwrap();
385 assert!(found.is_some());
386 let found = found.unwrap();
387 assert_eq!(found.did, pin.did);
388 assert_eq!(found.public_key_hex, pin.public_key_hex);
389 }
390
391 #[test]
392 fn test_store_pin_rejects_invalid_hex() {
393 let (_dir, store) = temp_store();
394 let mut pin = make_test_pin();
395 pin.public_key_hex = "not-valid-hex".to_string();
396
397 let result = store.pin(pin);
398 assert!(result.is_err());
399 assert!(result.unwrap_err().to_string().contains("Invalid"));
400 }
401
402 #[test]
403 fn test_store_pin_rejects_duplicate() {
404 let (_dir, store) = temp_store();
405 let pin = make_test_pin();
406
407 store.pin(pin.clone()).unwrap();
408 let result = store.pin(pin);
409
410 assert!(result.is_err());
411 assert!(result.unwrap_err().to_string().contains("already pinned"));
412 }
413
414 #[test]
415 fn test_store_update() {
416 let (_dir, store) = temp_store();
417 let mut pin = make_test_pin();
418 store.pin(pin.clone()).unwrap();
419
420 pin.public_key_hex =
422 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string();
423 pin.kel_sequence = Some(1);
424 store.update(pin.clone()).unwrap();
425
426 let found = store.lookup(&pin.did).unwrap().unwrap();
427 assert_eq!(found.kel_sequence, Some(1));
428 assert!(found.public_key_hex.starts_with("aaaa"));
429 }
430
431 #[test]
432 fn test_store_update_nonexistent() {
433 let (_dir, store) = temp_store();
434 let pin = make_test_pin();
435
436 let result = store.update(pin);
437 assert!(result.is_err());
438 assert!(result.unwrap_err().to_string().contains("not found"));
439 }
440
441 #[test]
442 fn test_store_remove() {
443 let (_dir, store) = temp_store();
444 let pin = make_test_pin();
445 store.pin(pin.clone()).unwrap();
446
447 assert!(store.remove(&pin.did).unwrap());
448 assert!(store.lookup(&pin.did).unwrap().is_none());
449 }
450
451 #[test]
452 fn test_store_remove_nonexistent() {
453 let (_dir, store) = temp_store();
454 assert!(!store.remove("did:keri:ENonexistent").unwrap());
455 }
456
457 #[test]
458 fn test_store_list() {
459 let (_dir, store) = temp_store();
460
461 let mut pin1 = make_test_pin();
462 pin1.did = "did:keri:E111".to_string();
463 let mut pin2 = make_test_pin();
464 pin2.did = "did:keri:E222".to_string();
465
466 store.pin(pin1).unwrap();
467 store.pin(pin2).unwrap();
468
469 let all = store.list().unwrap();
470 assert_eq!(all.len(), 2);
471 }
472
473 #[test]
474 fn test_concurrent_access_no_corruption() {
475 use std::sync::Arc;
476 use std::thread;
477
478 let dir = tempfile::tempdir().unwrap();
479 let path = dir.path().join("known_identities.json");
480
481 std::fs::write(&path, "[]").unwrap();
483
484 let store = Arc::new(PinnedIdentityStore::new(path));
485
486 let handles: Vec<_> = (0..10)
487 .map(|i| {
488 let store = Arc::clone(&store);
489 thread::spawn(move || {
490 let mut pin = make_test_pin();
491 pin.did = format!("did:keri:E{:03}", i);
492 store.pin(pin).unwrap();
493 })
494 })
495 .collect();
496
497 for handle in handles {
498 handle.join().unwrap();
499 }
500
501 let all = store.list().unwrap();
502 assert_eq!(all.len(), 10);
503
504 for i in 0..10 {
505 let did = format!("did:keri:E{:03}", i);
506 assert!(
507 store.lookup(&did).unwrap().is_some(),
508 "Missing pin: {}",
509 did
510 );
511 }
512 }
513}