1use auths_crypto::KeriPublicKey;
9use auths_verifier::types::IdentityDID;
10use git2::Repository;
11
12use super::types::Prefix;
13use super::{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 sequence: u64,
81
82 pub can_rotate: bool,
84
85 pub is_abandoned: bool,
87}
88
89pub fn resolve_did_keri(repo: &Repository, did: &str) -> Result<DidKeriResolution, ResolveError> {
100 let prefix = parse_did_keri(did)?;
101
102 let kel = GitKel::new(repo, prefix.as_str());
104 if !kel.exists() {
105 return Err(ResolveError::NotFound(prefix.as_str().to_string()));
106 }
107
108 let events = kel.get_events()?;
110 let state = validate_kel(&events)?;
111
112 let key_encoded = state.current_key().ok_or(ResolveError::NoCurrentKey)?;
114
115 let public_key = KeriPublicKey::parse(key_encoded)
116 .map(|k| k.as_bytes().to_vec())
117 .map_err(|e| ResolveError::InvalidKeyEncoding(e.to_string()))?;
118
119 Ok(DidKeriResolution {
120 #[allow(clippy::disallowed_methods)] did: IdentityDID::new_unchecked(did),
122 prefix,
123 public_key,
124 sequence: state.sequence,
125 can_rotate: state.can_rotate(),
126 is_abandoned: state.is_abandoned,
127 })
128}
129
130pub fn resolve_did_keri_at_sequence(
139 repo: &Repository,
140 did: &str,
141 target_sequence: u64,
142) -> Result<DidKeriResolution, ResolveError> {
143 let prefix = parse_did_keri(did)?;
144
145 let kel = GitKel::new(repo, prefix.as_str());
146 if !kel.exists() {
147 return Err(ResolveError::NotFound(prefix.as_str().to_string()));
148 }
149
150 let events = kel.get_events()?;
151
152 let events_subset: Vec<_> = events
154 .into_iter()
155 .take_while(|e| e.sequence().value() <= target_sequence)
156 .collect();
157
158 if events_subset.is_empty() {
159 return Err(ResolveError::NotFound(format!(
160 "No events at sequence {}",
161 target_sequence
162 )));
163 }
164
165 let state = validate_kel(&events_subset)?;
166
167 let key_encoded = state.current_key().ok_or(ResolveError::NoCurrentKey)?;
168 let public_key = KeriPublicKey::parse(key_encoded)
169 .map(|k| k.as_bytes().to_vec())
170 .map_err(|e| ResolveError::InvalidKeyEncoding(e.to_string()))?;
171
172 Ok(DidKeriResolution {
173 #[allow(clippy::disallowed_methods)] did: IdentityDID::new_unchecked(did),
175 prefix,
176 public_key,
177 sequence: state.sequence,
178 can_rotate: state.can_rotate(),
179 is_abandoned: state.is_abandoned,
180 })
181}
182
183pub fn parse_did_keri(did: &str) -> Result<Prefix, ResolveError> {
185 const PREFIX: &str = "did:keri:";
186
187 if !did.starts_with(PREFIX) {
188 return Err(ResolveError::InvalidFormat(format!(
189 "Expected did:keri: prefix, got: {}",
190 did
191 )));
192 }
193
194 let prefix = &did[PREFIX.len()..];
195 if prefix.is_empty() {
196 return Err(ResolveError::InvalidFormat("Empty KERI prefix".into()));
197 }
198
199 if !prefix.starts_with('E') {
201 return Err(ResolveError::InvalidFormat(format!(
202 "Invalid KERI prefix format (expected E prefix): {}",
203 prefix
204 )));
205 }
206
207 Ok(Prefix::new_unchecked(prefix.to_string()))
208}
209
210#[cfg(test)]
211#[allow(clippy::disallowed_methods)]
212mod tests {
213 use super::*;
214 use crate::keri::{create_keri_identity, rotate_keys};
215 use tempfile::TempDir;
216
217 fn setup_repo() -> (TempDir, Repository) {
218 let dir = TempDir::new().unwrap();
219 let repo = Repository::init(dir.path()).unwrap();
220
221 let mut config = repo.config().unwrap();
222 config.set_str("user.name", "Test User").unwrap();
223 config.set_str("user.email", "test@example.com").unwrap();
224
225 (dir, repo)
226 }
227
228 #[test]
229 fn parse_did_keri_valid() {
230 let prefix = parse_did_keri("did:keri:EXq5YqaL6L48pf0fu7IUhL0JRaU2").unwrap();
231 assert_eq!(prefix, "EXq5YqaL6L48pf0fu7IUhL0JRaU2");
232 }
233
234 #[test]
235 fn parse_did_keri_rejects_wrong_method() {
236 let result = parse_did_keri("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
237 assert!(matches!(result, Err(ResolveError::InvalidFormat(_))));
238 }
239
240 #[test]
241 fn parse_did_keri_rejects_empty_prefix() {
242 let result = parse_did_keri("did:keri:");
243 assert!(matches!(result, Err(ResolveError::InvalidFormat(_))));
244 }
245
246 #[test]
247 fn parse_did_keri_rejects_invalid_prefix() {
248 let result = parse_did_keri("did:keri:invalid");
249 assert!(matches!(result, Err(ResolveError::InvalidFormat(_))));
250 }
251
252 #[test]
253 fn resolves_after_inception() {
254 let (_dir, repo) = setup_repo();
255
256 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
257 let did = format!("did:keri:{}", init.prefix);
258
259 let resolution = resolve_did_keri(&repo, &did).unwrap();
260
261 assert_eq!(resolution.prefix, init.prefix);
262 assert_eq!(resolution.public_key, init.current_public_key);
263 assert_eq!(resolution.sequence, 0);
264 assert!(resolution.can_rotate);
265 assert!(!resolution.is_abandoned);
266 }
267
268 #[test]
269 fn resolves_after_rotation() {
270 let (_dir, repo) = setup_repo();
271
272 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
273 let rot = rotate_keys(
274 &repo,
275 &init.prefix,
276 &init.next_keypair_pkcs8,
277 None,
278 chrono::Utc::now(),
279 )
280 .unwrap();
281
282 let did = format!("did:keri:{}", init.prefix);
283 let resolution = resolve_did_keri(&repo, &did).unwrap();
284
285 assert_eq!(resolution.public_key, rot.new_current_public_key);
287 assert_eq!(resolution.sequence, 1);
288 }
289
290 #[test]
291 fn resolves_at_historical_sequence() {
292 let (_dir, repo) = setup_repo();
293
294 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
295 let _rot = rotate_keys(
296 &repo,
297 &init.prefix,
298 &init.next_keypair_pkcs8,
299 None,
300 chrono::Utc::now(),
301 )
302 .unwrap();
303
304 let did = format!("did:keri:{}", init.prefix);
305
306 let resolution = resolve_did_keri_at_sequence(&repo, &did, 0).unwrap();
308 assert_eq!(resolution.public_key, init.current_public_key);
309 assert_eq!(resolution.sequence, 0);
310 }
311
312 #[test]
313 fn not_found_for_missing_kel() {
314 let (_dir, repo) = setup_repo();
315
316 let result = resolve_did_keri(&repo, "did:keri:ENotExist123");
317 assert!(matches!(result, Err(ResolveError::NotFound(_))));
318 }
319
320 #[test]
321 fn decode_ed25519_key() {
322 use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
323 let key_bytes = [1u8; 32];
324 let encoded = format!("D{}", URL_SAFE_NO_PAD.encode(key_bytes));
325
326 let key = KeriPublicKey::parse(&encoded).unwrap();
327 assert_eq!(key.as_bytes(), &key_bytes);
328 }
329
330 #[test]
331 fn decode_unknown_key_type_fails() {
332 let result = KeriPublicKey::parse("Xsomething");
333 assert!(result.is_err());
334 }
335}