Skip to main content

auths_id/identity/
resolve.rs

1//! DID resolution for did:key and did:keri.
2
3use auths_keri::KeriPublicKey;
4use git2::Repository;
5use std::path::Path;
6
7use std::sync::Arc;
8
9use crate::keri::types::Prefix;
10use crate::keri::{DidKeriResolution, resolve_did_keri};
11use crate::storage::registry::RegistryBackend;
12
13pub use auths_core::signing::{DidResolver, DidResolverError, ResolvedDid};
14
15/// Default resolver handling did:key and did:keri.
16pub struct DefaultDidResolver {
17    repo_path: Option<std::path::PathBuf>,
18}
19
20impl DefaultDidResolver {
21    /// Create a resolver without repository access (did:key only).
22    pub fn new() -> Self {
23        Self { repo_path: None }
24    }
25
26    /// Create a resolver with repository access (did:key and did:keri).
27    pub fn with_repo(path: impl AsRef<Path>) -> Self {
28        Self {
29            repo_path: Some(path.as_ref().to_path_buf()),
30        }
31    }
32
33    fn resolve_did_key(&self, did: &str) -> Result<ResolvedDid, DidResolverError> {
34        // Decode did:key for any supported curve
35        let decoded = auths_crypto::did_key_decode(did)
36            .map_err(|e| DidResolverError::DidKeyDecodingFailed(e.to_string()))?;
37        let public_key_bytes = match decoded {
38            auths_crypto::DecodedDidKey::Ed25519(pk) => pk.to_vec(),
39            auths_crypto::DecodedDidKey::P256(pk) => pk,
40        };
41        Ok(ResolvedDid::Key {
42            did: did.to_string(),
43            public_key_bytes,
44        })
45    }
46
47    fn resolve_did_keri_internal(&self, did: &str) -> Result<ResolvedDid, DidResolverError> {
48        let repo_path = self.repo_path.as_ref().ok_or_else(|| {
49            DidResolverError::Repository("Repository path required for did:keri".into())
50        })?;
51
52        let repo =
53            Repository::open(repo_path).map_err(|e| DidResolverError::Repository(e.to_string()))?;
54
55        let resolution: DidKeriResolution = resolve_did_keri(&repo, did)
56            .map_err(|e| DidResolverError::Resolution(e.to_string()))?;
57
58        let curve = resolution.curve;
59        Ok(ResolvedDid::Keri {
60            did: did.to_string(),
61            public_key_bytes: resolution.public_key,
62            curve,
63            sequence: resolution.sequence,
64            can_rotate: resolution.can_rotate,
65        })
66    }
67}
68
69impl Default for DefaultDidResolver {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl DidResolver for DefaultDidResolver {
76    fn resolve(&self, did: &str) -> Result<ResolvedDid, DidResolverError> {
77        if did.starts_with("did:keri:") {
78            self.resolve_did_keri_internal(did)
79        } else if did.starts_with("did:key:") {
80            self.resolve_did_key(did)
81        } else {
82            let method = did.split(':').nth(1).unwrap_or("unknown");
83            Err(DidResolverError::UnsupportedMethod(method.to_string()))
84        }
85    }
86}
87
88/// Resolver for identities stored in the packed registry backend.
89///
90/// Unlike `DefaultDidResolver`, this resolver reads `did:keri:` DIDs from
91/// the packed registry tree (`refs/auths/registry`) rather than standalone
92/// `refs/did/keri/<prefix>/kel` refs. Use this whenever the identity was
93/// initialized via `RegistryIdentityStorage` from auths-storage.
94pub struct RegistryDidResolver {
95    backend: Arc<dyn RegistryBackend + Send + Sync>,
96}
97
98impl RegistryDidResolver {
99    /// Create a resolver backed by the provided registry backend.
100    ///
101    /// Args:
102    /// * `backend` - The registry backend implementation.
103    ///
104    /// Usage:
105    /// ```ignore
106    /// let resolver = RegistryDidResolver::new(Arc::new(my_backend));
107    /// ```
108    pub fn new(backend: Arc<dyn RegistryBackend + Send + Sync>) -> Self {
109        Self { backend }
110    }
111}
112
113impl DidResolver for RegistryDidResolver {
114    fn resolve(&self, did: &str) -> Result<ResolvedDid, DidResolverError> {
115        if did.starts_with("did:keri:") {
116            let Some(prefix) = did.strip_prefix("did:keri:") else {
117                unreachable!("starts_with guard ensures strip_prefix succeeds");
118            };
119            if prefix.is_empty() {
120                return Err(DidResolverError::InvalidDidKeyFormat(
121                    "Empty KERI prefix".into(),
122                ));
123            }
124            let key_state = self
125                .backend
126                .get_key_state(&Prefix::new_unchecked(prefix.to_string()))
127                .map_err(|e| DidResolverError::Repository(e.to_string()))?;
128            let key_encoded = key_state.current_key().ok_or_else(|| {
129                DidResolverError::Repository("No current key in key state".into())
130            })?;
131            let parsed = KeriPublicKey::parse(key_encoded.as_str())
132                .map_err(|e| DidResolverError::DidKeyDecodingFailed(e.to_string()))?;
133            let curve = parsed.curve();
134            Ok(ResolvedDid::Keri {
135                did: did.to_string(),
136                public_key_bytes: parsed.into_bytes(),
137                curve,
138                sequence: key_state.sequence,
139                can_rotate: key_state.can_rotate(),
140            })
141        } else if did.starts_with("did:key:") {
142            let decoded = auths_crypto::did_key_decode(did)
143                .map_err(|e| DidResolverError::DidKeyDecodingFailed(e.to_string()))?;
144            let public_key_bytes = match decoded {
145                auths_crypto::DecodedDidKey::Ed25519(pk) => pk.to_vec(),
146                auths_crypto::DecodedDidKey::P256(pk) => pk,
147            };
148            Ok(ResolvedDid::Key {
149                did: did.to_string(),
150                public_key_bytes,
151            })
152        } else {
153            let method = did.split(':').nth(1).unwrap_or("unknown");
154            Err(DidResolverError::UnsupportedMethod(method.to_string()))
155        }
156    }
157}
158
159// `did_key_to_ed25519` and `ed25519_to_did_key` wrappers were deleted.
160// Callers should use `auths_crypto::did_key_decode` (returns curve-tagged
161// `DecodedDidKey`) and `auths_crypto::{ed25519_pubkey_to_did_key, p256_pubkey_to_did_key}`
162// directly — there is no need for a re-export layer that hardcodes Ed25519.
163
164#[cfg(test)]
165#[allow(clippy::disallowed_methods)]
166mod tests {
167    use super::*;
168    use crate::keri::create_keri_identity_with_curve;
169    use tempfile::TempDir;
170
171    fn setup_repo() -> (TempDir, Repository) {
172        let dir = TempDir::new().unwrap();
173        let repo = Repository::init(dir.path()).unwrap();
174
175        let mut config = repo.config().unwrap();
176        config.set_str("user.name", "Test User").unwrap();
177        config.set_str("user.email", "test@example.com").unwrap();
178
179        (dir, repo)
180    }
181
182    #[test]
183    fn did_key_roundtrip() {
184        let key_bytes = [42u8; 32];
185        let did = auths_verifier::types::CanonicalDid::from_public_key_did_key(
186            &key_bytes,
187            auths_crypto::CurveType::Ed25519,
188        );
189        let decoded = auths_crypto::did_key_decode(did.as_str()).unwrap();
190        match decoded {
191            auths_crypto::DecodedDidKey::Ed25519(pk) => assert_eq!(pk, key_bytes),
192            _ => panic!("expected Ed25519"),
193        }
194    }
195
196    #[test]
197    fn resolves_did_key_without_repo() {
198        let resolver = DefaultDidResolver::new();
199
200        let key = [1u8; 32];
201        let did = auths_verifier::CanonicalDid::from_public_key_did_key(
202            &key,
203            auths_crypto::CurveType::Ed25519,
204        )
205        .to_string();
206
207        let resolved = resolver.resolve(&did).unwrap();
208        assert_eq!(resolved.public_key_bytes(), &key);
209        assert!(resolved.is_key());
210    }
211
212    #[test]
213    fn resolves_did_keri_with_repo() {
214        let (dir, repo) = setup_repo();
215
216        let init = create_keri_identity_with_curve(
217            &repo,
218            None,
219            chrono::Utc::now(),
220            auths_crypto::CurveType::Ed25519,
221        )
222        .unwrap();
223        let did = format!("did:keri:{}", init.prefix);
224
225        let resolver = DefaultDidResolver::with_repo(dir.path());
226        let resolved = resolver.resolve(&did).unwrap();
227
228        assert_eq!(
229            resolved.public_key_bytes(),
230            init.current_public_key.as_slice()
231        );
232        assert!(matches!(
233            resolved,
234            ResolvedDid::Keri {
235                sequence: 0,
236                can_rotate: true,
237                ..
238            }
239        ));
240    }
241
242    #[test]
243    fn did_keri_fails_without_repo() {
244        let resolver = DefaultDidResolver::new();
245        let result = resolver.resolve("did:keri:ETest123");
246        assert!(matches!(result, Err(DidResolverError::Repository(_))));
247    }
248
249    #[test]
250    fn rejects_unsupported_method() {
251        let resolver = DefaultDidResolver::new();
252        let result = resolver.resolve("did:web:example.com");
253        assert!(matches!(
254            result,
255            Err(DidResolverError::UnsupportedMethod(_))
256        ));
257    }
258
259    #[test]
260    fn rejects_invalid_did_key() {
261        let result = auths_crypto::did_key_decode("did:key:invalid");
262        assert!(result.is_err());
263    }
264
265    #[test]
266    fn rejects_non_did_key_prefix() {
267        let result = auths_crypto::did_key_decode("did:web:example.com");
268        assert!(result.is_err());
269    }
270}