Skip to main content

auths_id/trust/
mod.rs

1//! Trust resolution implementation for auths-id.
2//!
3//! This module implements the [`KelContinuityChecker`] trait from auths-core,
4//! providing Git-backed KEL replay for verifying rotation continuity.
5
6use auths_core::trust::continuity::{KelContinuityChecker, RotationProof};
7use auths_crypto::KeriPublicKey;
8use git2::Repository;
9
10use crate::keri::{Event, GitKel, Said, did_to_prefix, validate_kel};
11
12/// KEL-based rotation continuity checker backed by a Git repository.
13///
14/// This implementation verifies that there is a valid, unbroken event chain
15/// from a pinned KEL tip to the current state, enabling trust to be maintained
16/// across key rotations.
17///
18/// # Example
19///
20/// ```ignore
21/// use git2::Repository;
22/// use auths_id::trust::GitKelContinuityChecker;
23/// use auths_core::trust::KelContinuityChecker;
24///
25/// let repo = Repository::open("~/.auths")?;
26/// let checker = GitKelContinuityChecker::new(&repo);
27///
28/// let proof = checker.verify_rotation_continuity(
29///     "did:keri:EPrefix...",
30///     "EOldTipSaid",
31///     &presented_public_key,
32/// )?;
33/// ```
34pub struct GitKelContinuityChecker<'a> {
35    repo: &'a Repository,
36}
37
38impl<'a> GitKelContinuityChecker<'a> {
39    /// Create a new continuity checker for the given repository.
40    pub fn new(repo: &'a Repository) -> Self {
41        Self { repo }
42    }
43}
44
45impl KelContinuityChecker for GitKelContinuityChecker<'_> {
46    fn verify_rotation_continuity(
47        &self,
48        did: &str,
49        pinned_tip_said: &str,
50        presented_pk: &[u8],
51    ) -> Result<Option<RotationProof>, auths_core::error::TrustError> {
52        let pinned_said = Said::new_unchecked(pinned_tip_said.to_string());
53        let prefix = did_to_prefix(did).ok_or_else(|| {
54            auths_core::error::TrustError::InvalidData(format!("Invalid did:keri format: {}", did))
55        })?;
56
57        let kel = GitKel::new(self.repo, prefix);
58        if !kel.exists() {
59            return Ok(None);
60        }
61
62        let events = kel
63            .get_events()
64            .map_err(|e| auths_core::error::TrustError::InvalidData(e.to_string()))?;
65
66        let pinned_idx = events.iter().position(|e| e.said() == pinned_tip_said);
67        let Some(pinned_idx) = pinned_idx else {
68            return Ok(None);
69        };
70
71        let full_state = validate_kel(&events)
72            .map_err(|e| auths_core::error::TrustError::InvalidData(e.to_string()))?;
73
74        if !verify_chain_from_index(&events, pinned_idx, &pinned_said) {
75            return Ok(None);
76        }
77
78        let current_key_encoded = match full_state.current_key() {
79            Some(k) => k,
80            None => return Ok(None),
81        };
82        let current_key_bytes = KeriPublicKey::parse(current_key_encoded).map_err(|e| {
83            auths_core::error::TrustError::InvalidData(format!("KERI key decode failed: {e}"))
84        })?;
85
86        if current_key_bytes.as_bytes().as_slice() != presented_pk {
87            return Ok(None);
88        }
89
90        Ok(Some(RotationProof {
91            new_public_key: current_key_bytes.as_bytes().to_vec(),
92            new_kel_tip: full_state.last_event_said.to_string(),
93            new_sequence: full_state.sequence,
94        }))
95    }
96}
97
98/// Verify that the event chain from `pinned_idx` forward is unbroken.
99///
100/// Walks from `pinned_idx + 1` to end, verifying each event's `p` field
101/// links back to the previous event's SAID.
102fn verify_chain_from_index(events: &[Event], pinned_idx: usize, pinned_tip_said: &Said) -> bool {
103    let mut expected_prev = pinned_tip_said.clone();
104
105    for event in events.iter().skip(pinned_idx + 1) {
106        let prev = match event.previous() {
107            Some(p) => p,
108            None => return false, // Non-inception event missing previous SAID
109        };
110        if *prev != expected_prev {
111            return false; // Chain forks after pinned tip
112        }
113        expected_prev = event.said().clone();
114    }
115
116    true
117}
118
119#[cfg(test)]
120mod tests {
121    use auths_crypto::KeriPublicKey;
122
123    #[test]
124    fn test_keri_key_parse_valid() {
125        let encoded = "DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
126        let key = KeriPublicKey::parse(encoded).unwrap();
127        assert_eq!(key.as_bytes(), &[0u8; 32]);
128    }
129
130    #[test]
131    fn test_keri_key_parse_invalid_prefix() {
132        let result = KeriPublicKey::parse("XInvalidPrefix");
133        assert!(result.is_err());
134    }
135
136    #[test]
137    fn test_keri_key_parse_invalid_base64() {
138        let result = KeriPublicKey::parse("D!!!invalid!!!");
139        assert!(result.is_err());
140    }
141}