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