use auths_keri::KeriPublicKey;
use git2::Repository;
use std::path::Path;
use std::sync::Arc;
use crate::keri::types::Prefix;
use crate::keri::{DidKeriResolution, resolve_did_keri};
use crate::storage::registry::RegistryBackend;
pub use auths_core::signing::{DidResolver, DidResolverError, ResolvedDid};
pub struct DefaultDidResolver {
repo_path: Option<std::path::PathBuf>,
}
impl DefaultDidResolver {
pub fn new() -> Self {
Self { repo_path: None }
}
pub fn with_repo(path: impl AsRef<Path>) -> Self {
Self {
repo_path: Some(path.as_ref().to_path_buf()),
}
}
fn resolve_did_key(&self, did: &str) -> Result<ResolvedDid, DidResolverError> {
let decoded = auths_crypto::did_key_decode(did)
.map_err(|e| DidResolverError::DidKeyDecodingFailed(e.to_string()))?;
let public_key_bytes = match decoded {
auths_crypto::DecodedDidKey::Ed25519(pk) => pk.to_vec(),
auths_crypto::DecodedDidKey::P256(pk) => pk,
};
Ok(ResolvedDid::Key {
did: did.to_string(),
public_key_bytes,
})
}
fn resolve_did_keri_internal(&self, did: &str) -> Result<ResolvedDid, DidResolverError> {
let repo_path = self.repo_path.as_ref().ok_or_else(|| {
DidResolverError::Repository("Repository path required for did:keri".into())
})?;
let repo =
Repository::open(repo_path).map_err(|e| DidResolverError::Repository(e.to_string()))?;
let resolution: DidKeriResolution = resolve_did_keri(&repo, did)
.map_err(|e| DidResolverError::Resolution(e.to_string()))?;
let curve = resolution.curve;
Ok(ResolvedDid::Keri {
did: did.to_string(),
public_key_bytes: resolution.public_key,
curve,
sequence: resolution.sequence,
can_rotate: resolution.can_rotate,
})
}
}
impl Default for DefaultDidResolver {
fn default() -> Self {
Self::new()
}
}
impl DidResolver for DefaultDidResolver {
fn resolve(&self, did: &str) -> Result<ResolvedDid, DidResolverError> {
if did.starts_with("did:keri:") {
self.resolve_did_keri_internal(did)
} else if did.starts_with("did:key:") {
self.resolve_did_key(did)
} else {
let method = did.split(':').nth(1).unwrap_or("unknown");
Err(DidResolverError::UnsupportedMethod(method.to_string()))
}
}
}
pub struct RegistryDidResolver {
backend: Arc<dyn RegistryBackend + Send + Sync>,
}
impl RegistryDidResolver {
pub fn new(backend: Arc<dyn RegistryBackend + Send + Sync>) -> Self {
Self { backend }
}
}
impl DidResolver for RegistryDidResolver {
fn resolve(&self, did: &str) -> Result<ResolvedDid, DidResolverError> {
if did.starts_with("did:keri:") {
let Some(prefix) = did.strip_prefix("did:keri:") else {
unreachable!("starts_with guard ensures strip_prefix succeeds");
};
if prefix.is_empty() {
return Err(DidResolverError::InvalidDidKeyFormat(
"Empty KERI prefix".into(),
));
}
let key_state = self
.backend
.get_key_state(&Prefix::new_unchecked(prefix.to_string()))
.map_err(|e| DidResolverError::Repository(e.to_string()))?;
let key_encoded = key_state.current_key().ok_or_else(|| {
DidResolverError::Repository("No current key in key state".into())
})?;
let parsed = KeriPublicKey::parse(key_encoded.as_str())
.map_err(|e| DidResolverError::DidKeyDecodingFailed(e.to_string()))?;
let curve = parsed.curve();
Ok(ResolvedDid::Keri {
did: did.to_string(),
public_key_bytes: parsed.into_bytes(),
curve,
sequence: key_state.sequence,
can_rotate: key_state.can_rotate(),
})
} else if did.starts_with("did:key:") {
let decoded = auths_crypto::did_key_decode(did)
.map_err(|e| DidResolverError::DidKeyDecodingFailed(e.to_string()))?;
let public_key_bytes = match decoded {
auths_crypto::DecodedDidKey::Ed25519(pk) => pk.to_vec(),
auths_crypto::DecodedDidKey::P256(pk) => pk,
};
Ok(ResolvedDid::Key {
did: did.to_string(),
public_key_bytes,
})
} else {
let method = did.split(':').nth(1).unwrap_or("unknown");
Err(DidResolverError::UnsupportedMethod(method.to_string()))
}
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use crate::keri::create_keri_identity_with_curve;
use tempfile::TempDir;
fn setup_repo() -> (TempDir, Repository) {
let dir = TempDir::new().unwrap();
let repo = Repository::init(dir.path()).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test User").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
(dir, repo)
}
#[test]
fn did_key_roundtrip() {
let key_bytes = [42u8; 32];
let did = auths_verifier::types::CanonicalDid::from_public_key_did_key(
&key_bytes,
auths_crypto::CurveType::Ed25519,
);
let decoded = auths_crypto::did_key_decode(did.as_str()).unwrap();
match decoded {
auths_crypto::DecodedDidKey::Ed25519(pk) => assert_eq!(pk, key_bytes),
_ => panic!("expected Ed25519"),
}
}
#[test]
fn resolves_did_key_without_repo() {
let resolver = DefaultDidResolver::new();
let key = [1u8; 32];
let did = auths_verifier::CanonicalDid::from_public_key_did_key(
&key,
auths_crypto::CurveType::Ed25519,
)
.to_string();
let resolved = resolver.resolve(&did).unwrap();
assert_eq!(resolved.public_key_bytes(), &key);
assert!(resolved.is_key());
}
#[test]
fn resolves_did_keri_with_repo() {
let (dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let did = format!("did:keri:{}", init.prefix);
let resolver = DefaultDidResolver::with_repo(dir.path());
let resolved = resolver.resolve(&did).unwrap();
assert_eq!(
resolved.public_key_bytes(),
init.current_public_key.as_slice()
);
assert!(matches!(
resolved,
ResolvedDid::Keri {
sequence: 0,
can_rotate: true,
..
}
));
}
#[test]
fn did_keri_fails_without_repo() {
let resolver = DefaultDidResolver::new();
let result = resolver.resolve("did:keri:ETest123");
assert!(matches!(result, Err(DidResolverError::Repository(_))));
}
#[test]
fn rejects_unsupported_method() {
let resolver = DefaultDidResolver::new();
let result = resolver.resolve("did:web:example.com");
assert!(matches!(
result,
Err(DidResolverError::UnsupportedMethod(_))
));
}
#[test]
fn rejects_invalid_did_key() {
let result = auths_crypto::did_key_decode("did:key:invalid");
assert!(result.is_err());
}
#[test]
fn rejects_non_did_key_prefix() {
let result = auths_crypto::did_key_decode("did:web:example.com");
assert!(result.is_err());
}
}