Skip to main content

auths_id/keri/
resolve.rs

1//! did:keri resolution via KEL replay.
2//!
3//! Resolves a `did:keri:<prefix>` to its current public key by:
4//! 1. Loading the KEL from Git
5//! 2. Replaying all events to derive current KeyState
6//! 3. Decoding the current public key
7
8use auths_keri::KeriPublicKey;
9use auths_verifier::types::IdentityDID;
10use git2::Repository;
11
12use super::types::Prefix;
13use super::{Event, GitKel, KelError, ValidationError, validate_kel};
14
15/// Error type for did:keri resolution.
16#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum ResolveError {
19    #[error("Invalid DID format: {0}")]
20    InvalidFormat(String),
21
22    #[error("KEL not found for prefix: {0}")]
23    NotFound(String),
24
25    #[error("KEL error: {0}")]
26    Kel(#[from] KelError),
27
28    #[error("Validation error: {0}")]
29    Validation(#[from] ValidationError),
30
31    #[error("Invalid key encoding: {0}")]
32    InvalidKeyEncoding(String),
33
34    #[error("No current key in identity")]
35    NoCurrentKey,
36
37    #[error("Unknown key type: {0}")]
38    UnknownKeyType(String),
39}
40
41impl auths_core::error::AuthsErrorInfo for ResolveError {
42    fn error_code(&self) -> &'static str {
43        match self {
44            Self::InvalidFormat(_) => "AUTHS-E4801",
45            Self::NotFound(_) => "AUTHS-E4802",
46            Self::Kel(_) => "AUTHS-E4803",
47            Self::Validation(_) => "AUTHS-E4804",
48            Self::InvalidKeyEncoding(_) => "AUTHS-E4805",
49            Self::NoCurrentKey => "AUTHS-E4806",
50            Self::UnknownKeyType(_) => "AUTHS-E4807",
51        }
52    }
53
54    fn suggestion(&self) -> Option<&'static str> {
55        match self {
56            Self::InvalidFormat(_) => Some("Use the format 'did:keri:E<prefix>'"),
57            Self::NotFound(_) => Some("The identity does not exist; check the DID prefix"),
58            Self::Kel(_) => None,
59            Self::Validation(_) => None,
60            Self::InvalidKeyEncoding(_) => None,
61            Self::NoCurrentKey => Some("The identity has no active key; it may be abandoned"),
62            Self::UnknownKeyType(_) => Some("Only Ed25519 keys (D prefix) are currently supported"),
63        }
64    }
65}
66
67/// Result of resolving a did:keri.
68#[derive(Debug, Clone)]
69pub struct DidKeriResolution {
70    /// The full DID string
71    pub did: IdentityDID,
72
73    /// The KERI prefix
74    pub prefix: Prefix,
75
76    /// The current public key (raw bytes, 32 bytes for Ed25519)
77    pub public_key: Vec<u8>,
78
79    /// The curve of the current public key, derived from the CESR prefix.
80    pub curve: auths_crypto::CurveType,
81
82    /// The current sequence number
83    pub sequence: u128,
84
85    /// Whether the identity can still be rotated
86    pub can_rotate: bool,
87
88    /// Whether the identity has been abandoned
89    pub is_abandoned: bool,
90}
91
92/// Resolve a did:keri to its current public key.
93///
94/// This replays the entire KEL to derive the current key state.
95///
96/// # Arguments
97/// * `repo` - Git repository containing the KEL
98/// * `did` - The did:keri string (e.g., "did:keri:EXq5YqaL...")
99///
100/// # Returns
101/// * `DidKeriResolution` with the current public key and state
102pub fn resolve_did_keri(repo: &Repository, did: &str) -> Result<DidKeriResolution, ResolveError> {
103    let prefix = parse_did_keri(did)?;
104
105    // Load KEL
106    let kel = GitKel::new(repo, prefix.as_str());
107    if !kel.exists() {
108        return Err(ResolveError::NotFound(prefix.as_str().to_string()));
109    }
110
111    // Replay KEL to get current state
112    let events = kel.get_events()?;
113    let state = validate_kel(&events)?;
114
115    // Decode current public key
116    let key_encoded = state.current_key().ok_or(ResolveError::NoCurrentKey)?;
117
118    let keri_key = KeriPublicKey::parse(key_encoded.as_str())
119        .map_err(|e| ResolveError::InvalidKeyEncoding(e.to_string()))?;
120    let curve = keri_key.curve();
121    let public_key = keri_key.as_bytes().to_vec();
122
123    Ok(DidKeriResolution {
124        #[allow(clippy::disallowed_methods)] // INVARIANT: parse_did_keri() above validated the did:keri format
125        did: IdentityDID::new_unchecked(did),
126        prefix,
127        public_key,
128        curve,
129        sequence: state.sequence,
130        can_rotate: state.can_rotate(),
131        is_abandoned: state.is_abandoned,
132    })
133}
134
135/// Resolve the raw KEL events for a `did:keri` from local refs (no replay).
136///
137/// Returns the unvalidated event chain — the verifier replays it itself (a device
138/// KEL is `dip`-rooted and needs a delegator lookup, so plain `validate_kel` here
139/// would fail). Used by the commit verifier to obtain the device + root KEL slices.
140///
141/// Args:
142/// * `repo`: Git repository containing the KEL.
143/// * `did`: The `did:keri:` string.
144///
145/// Usage:
146/// ```ignore
147/// let device_kel = resolve_kel_events(&repo, device_did)?;
148/// let root_kel = resolve_kel_events(&repo, root_did)?;
149/// ```
150pub fn resolve_kel_events(repo: &Repository, did: &str) -> Result<Vec<Event>, ResolveError> {
151    let prefix = parse_did_keri(did)?;
152    let kel = GitKel::new(repo, prefix.as_str());
153    if !kel.exists() {
154        return Err(ResolveError::NotFound(prefix.as_str().to_string()));
155    }
156    Ok(kel.get_events()?)
157}
158
159/// Resolve a did:keri at a specific sequence number (historical lookup).
160///
161/// This replays the KEL only up to the target sequence.
162///
163/// # Arguments
164/// * `repo` - Git repository containing the KEL
165/// * `did` - The did:keri string
166/// * `target_sequence` - The sequence number to resolve at
167pub fn resolve_did_keri_at_sequence(
168    repo: &Repository,
169    did: &str,
170    target_sequence: u128,
171) -> Result<DidKeriResolution, ResolveError> {
172    let prefix = parse_did_keri(did)?;
173
174    let kel = GitKel::new(repo, prefix.as_str());
175    if !kel.exists() {
176        return Err(ResolveError::NotFound(prefix.as_str().to_string()));
177    }
178
179    let events = kel.get_events()?;
180
181    // Only process events up to target sequence
182    let events_subset: Vec<_> = events
183        .into_iter()
184        .take_while(|e| e.sequence().value() <= target_sequence)
185        .collect();
186
187    if events_subset.is_empty() {
188        return Err(ResolveError::NotFound(format!(
189            "No events at sequence {}",
190            target_sequence
191        )));
192    }
193
194    let state = validate_kel(&events_subset)?;
195
196    let key_encoded = state.current_key().ok_or(ResolveError::NoCurrentKey)?;
197    let keri_key = KeriPublicKey::parse(key_encoded.as_str())
198        .map_err(|e| ResolveError::InvalidKeyEncoding(e.to_string()))?;
199    let curve = keri_key.curve();
200    let public_key = keri_key.as_bytes().to_vec();
201
202    Ok(DidKeriResolution {
203        #[allow(clippy::disallowed_methods)] // INVARIANT: parse_did_keri() above validated the did:keri format
204        did: IdentityDID::new_unchecked(did),
205        prefix,
206        public_key,
207        curve,
208        sequence: state.sequence,
209        can_rotate: state.can_rotate(),
210        is_abandoned: state.is_abandoned,
211    })
212}
213
214/// Parse a did:keri string to extract the prefix.
215pub fn parse_did_keri(did: &str) -> Result<Prefix, ResolveError> {
216    const PREFIX: &str = "did:keri:";
217
218    if !did.starts_with(PREFIX) {
219        return Err(ResolveError::InvalidFormat(format!(
220            "Expected did:keri: prefix, got: {}",
221            did
222        )));
223    }
224
225    let prefix = &did[PREFIX.len()..];
226    if prefix.is_empty() {
227        return Err(ResolveError::InvalidFormat("Empty KERI prefix".into()));
228    }
229
230    // Validate prefix format (starts with E for Blake3 SAID)
231    if !prefix.starts_with('E') {
232        return Err(ResolveError::InvalidFormat(format!(
233            "Invalid KERI prefix format (expected E prefix): {}",
234            prefix
235        )));
236    }
237
238    Ok(Prefix::new_unchecked(prefix.to_string()))
239}
240
241#[cfg(test)]
242#[allow(clippy::disallowed_methods)]
243mod tests {
244    use super::*;
245    use crate::keri::{create_keri_identity_with_curve, rotate_keys};
246    use tempfile::TempDir;
247
248    fn setup_repo() -> (TempDir, Repository) {
249        let dir = TempDir::new().unwrap();
250        let repo = Repository::init(dir.path()).unwrap();
251
252        let mut config = repo.config().unwrap();
253        config.set_str("user.name", "Test User").unwrap();
254        config.set_str("user.email", "test@example.com").unwrap();
255
256        (dir, repo)
257    }
258
259    #[test]
260    fn parse_did_keri_valid() {
261        let prefix = parse_did_keri("did:keri:EXq5YqaL6L48pf0fu7IUhL0JRaU2").unwrap();
262        assert_eq!(prefix, "EXq5YqaL6L48pf0fu7IUhL0JRaU2");
263    }
264
265    #[test]
266    fn parse_did_keri_rejects_wrong_method() {
267        let result = parse_did_keri("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
268        assert!(matches!(result, Err(ResolveError::InvalidFormat(_))));
269    }
270
271    #[test]
272    fn parse_did_keri_rejects_empty_prefix() {
273        let result = parse_did_keri("did:keri:");
274        assert!(matches!(result, Err(ResolveError::InvalidFormat(_))));
275    }
276
277    #[test]
278    fn parse_did_keri_rejects_invalid_prefix() {
279        let result = parse_did_keri("did:keri:invalid");
280        assert!(matches!(result, Err(ResolveError::InvalidFormat(_))));
281    }
282
283    #[test]
284    fn resolves_after_inception() {
285        let (_dir, repo) = setup_repo();
286
287        let init = create_keri_identity_with_curve(
288            &repo,
289            None,
290            chrono::Utc::now(),
291            auths_crypto::CurveType::Ed25519,
292        )
293        .unwrap();
294        let did = format!("did:keri:{}", init.prefix);
295
296        let resolution = resolve_did_keri(&repo, &did).unwrap();
297
298        assert_eq!(resolution.prefix, init.prefix);
299        assert_eq!(resolution.public_key, init.current_public_key);
300        assert_eq!(resolution.sequence, 0);
301        assert!(resolution.can_rotate);
302        assert!(!resolution.is_abandoned);
303    }
304
305    #[test]
306    fn resolves_after_rotation() {
307        let (_dir, repo) = setup_repo();
308
309        let init = create_keri_identity_with_curve(
310            &repo,
311            None,
312            chrono::Utc::now(),
313            auths_crypto::CurveType::Ed25519,
314        )
315        .unwrap();
316        let rot = rotate_keys(
317            &repo,
318            &init.prefix,
319            &init.next_keypair_pkcs8,
320            None,
321            chrono::Utc::now(),
322        )
323        .unwrap();
324
325        let did = format!("did:keri:{}", init.prefix);
326        let resolution = resolve_did_keri(&repo, &did).unwrap();
327
328        // Should return the NEW key (former next key)
329        assert_eq!(resolution.public_key, rot.new_current_public_key);
330        assert_eq!(resolution.sequence, 1);
331    }
332
333    #[test]
334    fn resolves_at_historical_sequence() {
335        let (_dir, repo) = setup_repo();
336
337        let init = create_keri_identity_with_curve(
338            &repo,
339            None,
340            chrono::Utc::now(),
341            auths_crypto::CurveType::Ed25519,
342        )
343        .unwrap();
344        let _rot = rotate_keys(
345            &repo,
346            &init.prefix,
347            &init.next_keypair_pkcs8,
348            None,
349            chrono::Utc::now(),
350        )
351        .unwrap();
352
353        let did = format!("did:keri:{}", init.prefix);
354
355        // Resolve at sequence 0 should return inception key
356        let resolution = resolve_did_keri_at_sequence(&repo, &did, 0).unwrap();
357        assert_eq!(resolution.public_key, init.current_public_key);
358        assert_eq!(resolution.sequence, 0);
359    }
360
361    #[test]
362    fn not_found_for_missing_kel() {
363        let (_dir, repo) = setup_repo();
364
365        let result = resolve_did_keri(&repo, "did:keri:ENotExist123");
366        assert!(matches!(result, Err(ResolveError::NotFound(_))));
367    }
368
369    #[test]
370    fn decode_ed25519_key() {
371        let key_bytes = [1u8; 32];
372        let encoded = KeriPublicKey::ed25519(&key_bytes)
373            .unwrap()
374            .to_qb64()
375            .unwrap();
376
377        let key = KeriPublicKey::parse(&encoded).unwrap();
378        assert_eq!(key.as_bytes(), &key_bytes);
379    }
380
381    #[test]
382    fn decode_unknown_key_type_fails() {
383        let result = KeriPublicKey::parse("Xsomething");
384        assert!(result.is_err());
385    }
386}