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