1use 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#[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#[derive(Debug, Clone)]
69pub struct DidKeriResolution {
70 pub did: IdentityDID,
72
73 pub prefix: Prefix,
75
76 pub public_key: Vec<u8>,
78
79 pub curve: auths_crypto::CurveType,
81
82 pub sequence: u128,
84
85 pub can_rotate: bool,
87
88 pub is_abandoned: bool,
90}
91
92pub fn resolve_did_keri(repo: &Repository, did: &str) -> Result<DidKeriResolution, ResolveError> {
103 let prefix = parse_did_keri(did)?;
104
105 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 let events = kel.get_events()?;
113 let state = validate_kel(&events)?;
114
115 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)] 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
135pub 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
159pub 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 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)] 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
214pub 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 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 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 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}