use auths_keri::KeriPublicKey;
use auths_verifier::types::IdentityDID;
use git2::Repository;
use super::types::Prefix;
use super::{Event, GitKel, KelError, ValidationError, validate_kel};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ResolveError {
#[error("Invalid DID format: {0}")]
InvalidFormat(String),
#[error("KEL not found for prefix: {0}")]
NotFound(String),
#[error("KEL error: {0}")]
Kel(#[from] KelError),
#[error("Validation error: {0}")]
Validation(#[from] ValidationError),
#[error("Invalid key encoding: {0}")]
InvalidKeyEncoding(String),
#[error("No current key in identity")]
NoCurrentKey,
#[error("Unknown key type: {0}")]
UnknownKeyType(String),
}
impl auths_core::error::AuthsErrorInfo for ResolveError {
fn error_code(&self) -> &'static str {
match self {
Self::InvalidFormat(_) => "AUTHS-E4801",
Self::NotFound(_) => "AUTHS-E4802",
Self::Kel(_) => "AUTHS-E4803",
Self::Validation(_) => "AUTHS-E4804",
Self::InvalidKeyEncoding(_) => "AUTHS-E4805",
Self::NoCurrentKey => "AUTHS-E4806",
Self::UnknownKeyType(_) => "AUTHS-E4807",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::InvalidFormat(_) => Some("Use the format 'did:keri:E<prefix>'"),
Self::NotFound(_) => Some("The identity does not exist; check the DID prefix"),
Self::Kel(_) => None,
Self::Validation(_) => None,
Self::InvalidKeyEncoding(_) => None,
Self::NoCurrentKey => Some("The identity has no active key; it may be abandoned"),
Self::UnknownKeyType(_) => Some("Only Ed25519 keys (D prefix) are currently supported"),
}
}
}
#[derive(Debug, Clone)]
pub struct DidKeriResolution {
pub did: IdentityDID,
pub prefix: Prefix,
pub public_key: Vec<u8>,
pub curve: auths_crypto::CurveType,
pub sequence: u128,
pub can_rotate: bool,
pub is_abandoned: bool,
}
pub fn resolve_did_keri(repo: &Repository, did: &str) -> Result<DidKeriResolution, ResolveError> {
let prefix = parse_did_keri(did)?;
let kel = GitKel::new(repo, prefix.as_str());
if !kel.exists() {
return Err(ResolveError::NotFound(prefix.as_str().to_string()));
}
let events = kel.get_events()?;
let state = validate_kel(&events)?;
let key_encoded = state.current_key().ok_or(ResolveError::NoCurrentKey)?;
let keri_key = KeriPublicKey::parse(key_encoded.as_str())
.map_err(|e| ResolveError::InvalidKeyEncoding(e.to_string()))?;
let curve = keri_key.curve();
let public_key = keri_key.as_bytes().to_vec();
Ok(DidKeriResolution {
#[allow(clippy::disallowed_methods)] did: IdentityDID::new_unchecked(did),
prefix,
public_key,
curve,
sequence: state.sequence,
can_rotate: state.can_rotate(),
is_abandoned: state.is_abandoned,
})
}
pub fn resolve_kel_events(repo: &Repository, did: &str) -> Result<Vec<Event>, ResolveError> {
let prefix = parse_did_keri(did)?;
let kel = GitKel::new(repo, prefix.as_str());
if !kel.exists() {
return Err(ResolveError::NotFound(prefix.as_str().to_string()));
}
Ok(kel.get_events()?)
}
pub fn resolve_did_keri_at_sequence(
repo: &Repository,
did: &str,
target_sequence: u128,
) -> Result<DidKeriResolution, ResolveError> {
let prefix = parse_did_keri(did)?;
let kel = GitKel::new(repo, prefix.as_str());
if !kel.exists() {
return Err(ResolveError::NotFound(prefix.as_str().to_string()));
}
let events = kel.get_events()?;
let events_subset: Vec<_> = events
.into_iter()
.take_while(|e| e.sequence().value() <= target_sequence)
.collect();
if events_subset.is_empty() {
return Err(ResolveError::NotFound(format!(
"No events at sequence {}",
target_sequence
)));
}
let state = validate_kel(&events_subset)?;
let key_encoded = state.current_key().ok_or(ResolveError::NoCurrentKey)?;
let keri_key = KeriPublicKey::parse(key_encoded.as_str())
.map_err(|e| ResolveError::InvalidKeyEncoding(e.to_string()))?;
let curve = keri_key.curve();
let public_key = keri_key.as_bytes().to_vec();
Ok(DidKeriResolution {
#[allow(clippy::disallowed_methods)] did: IdentityDID::new_unchecked(did),
prefix,
public_key,
curve,
sequence: state.sequence,
can_rotate: state.can_rotate(),
is_abandoned: state.is_abandoned,
})
}
pub fn parse_did_keri(did: &str) -> Result<Prefix, ResolveError> {
const PREFIX: &str = "did:keri:";
if !did.starts_with(PREFIX) {
return Err(ResolveError::InvalidFormat(format!(
"Expected did:keri: prefix, got: {}",
did
)));
}
let prefix = &did[PREFIX.len()..];
if prefix.is_empty() {
return Err(ResolveError::InvalidFormat("Empty KERI prefix".into()));
}
if !prefix.starts_with('E') {
return Err(ResolveError::InvalidFormat(format!(
"Invalid KERI prefix format (expected E prefix): {}",
prefix
)));
}
Ok(Prefix::new_unchecked(prefix.to_string()))
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use crate::keri::{create_keri_identity_with_curve, rotate_keys};
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 parse_did_keri_valid() {
let prefix = parse_did_keri("did:keri:EXq5YqaL6L48pf0fu7IUhL0JRaU2").unwrap();
assert_eq!(prefix, "EXq5YqaL6L48pf0fu7IUhL0JRaU2");
}
#[test]
fn parse_did_keri_rejects_wrong_method() {
let result = parse_did_keri("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
assert!(matches!(result, Err(ResolveError::InvalidFormat(_))));
}
#[test]
fn parse_did_keri_rejects_empty_prefix() {
let result = parse_did_keri("did:keri:");
assert!(matches!(result, Err(ResolveError::InvalidFormat(_))));
}
#[test]
fn parse_did_keri_rejects_invalid_prefix() {
let result = parse_did_keri("did:keri:invalid");
assert!(matches!(result, Err(ResolveError::InvalidFormat(_))));
}
#[test]
fn resolves_after_inception() {
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 resolution = resolve_did_keri(&repo, &did).unwrap();
assert_eq!(resolution.prefix, init.prefix);
assert_eq!(resolution.public_key, init.current_public_key);
assert_eq!(resolution.sequence, 0);
assert!(resolution.can_rotate);
assert!(!resolution.is_abandoned);
}
#[test]
fn resolves_after_rotation() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let rot = rotate_keys(
&repo,
&init.prefix,
&init.next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let did = format!("did:keri:{}", init.prefix);
let resolution = resolve_did_keri(&repo, &did).unwrap();
assert_eq!(resolution.public_key, rot.new_current_public_key);
assert_eq!(resolution.sequence, 1);
}
#[test]
fn resolves_at_historical_sequence() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let _rot = rotate_keys(
&repo,
&init.prefix,
&init.next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let did = format!("did:keri:{}", init.prefix);
let resolution = resolve_did_keri_at_sequence(&repo, &did, 0).unwrap();
assert_eq!(resolution.public_key, init.current_public_key);
assert_eq!(resolution.sequence, 0);
}
#[test]
fn not_found_for_missing_kel() {
let (_dir, repo) = setup_repo();
let result = resolve_did_keri(&repo, "did:keri:ENotExist123");
assert!(matches!(result, Err(ResolveError::NotFound(_))));
}
#[test]
fn decode_ed25519_key() {
let key_bytes = [1u8; 32];
let encoded = KeriPublicKey::ed25519(&key_bytes)
.unwrap()
.to_qb64()
.unwrap();
let key = KeriPublicKey::parse(&encoded).unwrap();
assert_eq!(key.as_bytes(), &key_bytes);
}
#[test]
fn decode_unknown_key_type_fails() {
let result = KeriPublicKey::parse("Xsomething");
assert!(result.is_err());
}
}