use auths_core::storage::keychain::{KeyAlias, sign_with_key};
use auths_id::keri::credential_registry::{
anchor_tel_event, build_iss, build_rev, ensure_registry, find_registry, read_credential_tel,
};
use auths_id::keri::parse_did_keri;
use auths_id::keri::types::Prefix;
use auths_keri::{Acdc, Said, TelEvent, compute_capability_schema_said, validate_tel};
use chrono::{DateTime, Utc};
use crate::context::AuthsContext;
use crate::domains::credentials::error::CredentialError;
use crate::domains::credentials::stored::StoredCredential;
const CAPABILITY_FIELD: &str = "capability";
const EXPIRY_FIELD: &str = "expiry";
const ROLE_FIELD: &str = "role";
#[derive(Debug, Clone)]
pub struct CredentialIssuance {
pub credential_said: String,
pub registry_said: String,
pub issuer_did: String,
pub issuee_did: String,
}
pub fn resolve_issuer_prefix(
ctx: &AuthsContext,
_issuer_alias: &KeyAlias,
) -> Result<Prefix, CredentialError> {
let managed =
ctx.identity_storage
.load_identity()
.map_err(|e| CredentialError::IssueeNotFound {
did: format!("issuer identity load failed: {e}"),
})?;
parse_did_keri(managed.controller_did.as_str()).map_err(|e| CredentialError::IssueeNotFound {
did: format!("invalid issuer did:keri: {e}"),
})
}
fn resolve_issuer(
ctx: &AuthsContext,
issuer_alias: &KeyAlias,
) -> Result<(Prefix, auths_crypto::CurveType), CredentialError> {
let issuer_prefix = resolve_issuer_prefix(ctx, issuer_alias)?;
let (_pk, curve) = auths_core::storage::keychain::extract_public_key_bytes(
ctx.key_storage.as_ref(),
issuer_alias,
ctx.passphrase_provider.as_ref(),
)?;
Ok((issuer_prefix, curve))
}
fn guard_issuee_exists(ctx: &AuthsContext, issuee_did: &str) -> Result<Prefix, CredentialError> {
let issuee_prefix =
parse_did_keri(issuee_did).map_err(|_| CredentialError::IssueeNotFound {
did: issuee_did.to_string(),
})?;
ctx.registry
.get_event(&issuee_prefix, 0)
.map_err(|_| CredentialError::IssueeNotFound {
did: issuee_did.to_string(),
})?;
Ok(issuee_prefix)
}
fn build_attributes(
capabilities: &[String],
role: Option<&str>,
expires_at: Option<DateTime<Utc>>,
) -> serde_json::Map<String, serde_json::Value> {
let mut data = serde_json::Map::new();
let capability = capabilities.join(",");
data.insert(
CAPABILITY_FIELD.to_string(),
serde_json::Value::String(capability),
);
if let Some(role) = role {
data.insert(
ROLE_FIELD.to_string(),
serde_json::Value::String(role.to_string()),
);
}
if let Some(exp) = expires_at {
data.insert(
EXPIRY_FIELD.to_string(),
serde_json::Value::String(exp.to_rfc3339()),
);
}
data
}
pub fn issue(
ctx: &AuthsContext,
issuer_alias: &KeyAlias,
issuee_did: &str,
capabilities: &[String],
role: Option<&str>,
expires_at: Option<DateTime<Utc>>,
) -> Result<CredentialIssuance, CredentialError> {
let (issuer_prefix, issuer_curve) = resolve_issuer(ctx, issuer_alias)?;
let issuee_prefix = guard_issuee_exists(ctx, issuee_did)?;
let registry = ensure_registry(
ctx.registry.as_ref(),
&issuer_prefix,
issuer_alias,
issuer_curve,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)?;
let schema = compute_capability_schema_said().map_err(|_| CredentialError::SchemaUnknown)?;
let now = ctx.clock.now();
let data = build_attributes(capabilities, role, expires_at);
let acdc = Acdc::new(
issuer_prefix.clone(),
registry.clone(),
schema,
issuee_prefix.clone(),
now.to_rfc3339(),
data,
)
.saidify()
.map_err(|_| CredentialError::SchemaUnknown)?;
let credential_said = Said::new_unchecked(acdc.d.as_str().to_string());
let wire = acdc
.to_wire_bytes()
.map_err(|_| CredentialError::SchemaUnknown)?;
let (signature, _pk, _curve) = sign_with_key(
ctx.key_storage.as_ref(),
issuer_alias,
ctx.passphrase_provider.as_ref(),
&wire,
)?;
let stored = StoredCredential {
acdc: acdc.clone(),
signature,
};
let blob = stored
.to_bytes()
.map_err(|_| CredentialError::SchemaUnknown)?;
let iss = build_iss(&credential_said, ®istry, now.to_rfc3339())?;
anchor_tel_event(
ctx.registry.as_ref(),
&issuer_prefix,
issuer_alias,
issuer_curve,
&TelEvent::Iss(iss),
Some((credential_said.clone(), blob)),
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)?;
Ok(CredentialIssuance {
credential_said: credential_said.as_str().to_string(),
registry_said: registry.as_str().to_string(),
issuer_did: format!("did:keri:{issuer_prefix}"),
issuee_did: format!("did:keri:{issuee_prefix}"),
})
}
pub fn revoke(
ctx: &AuthsContext,
issuer_alias: &KeyAlias,
credential_said: &str,
) -> Result<(), CredentialError> {
let (issuer_prefix, issuer_curve) = resolve_issuer(ctx, issuer_alias)?;
let registry = find_registry(ctx.registry.as_ref(), &issuer_prefix)?.ok_or(
CredentialError::RegistryError(
auths_id::keri::credential_registry::CredentialRegistryError::Tel(
"issuer has no registry".to_string(),
),
),
)?;
let cred = Said::new_unchecked(credential_said.to_string());
let tel = read_credential_tel(ctx.registry.as_ref(), &issuer_prefix, ®istry, &cred)?;
let iss_said = tel
.iter()
.find_map(|e| match e {
TelEvent::Iss(iss) if iss.i == cred => Some(iss.d.clone()),
_ => None,
})
.ok_or(CredentialError::RegistryError(
auths_id::keri::credential_registry::CredentialRegistryError::Tel(
"credential has no iss event".to_string(),
),
))?;
if tel
.iter()
.any(|e| matches!(e, TelEvent::Rev(rev) if rev.i == cred))
{
return Ok(());
}
let now = ctx.clock.now();
let rev = build_rev(&cred, ®istry, &iss_said, now.to_rfc3339())?;
anchor_tel_event(
ctx.registry.as_ref(),
&issuer_prefix,
issuer_alias,
issuer_curve,
&TelEvent::Rev(rev),
None,
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct CredentialSummary {
pub credential_said: String,
pub subject_did: String,
pub capabilities: Vec<String>,
pub revoked: bool,
}
pub fn list(
ctx: &AuthsContext,
issuer_alias: &KeyAlias,
) -> Result<Vec<CredentialSummary>, CredentialError> {
let (issuer_prefix, _curve) = resolve_issuer(ctx, issuer_alias)?;
let Some(registry) = find_registry(ctx.registry.as_ref(), &issuer_prefix)? else {
return Ok(Vec::new());
};
let credential_saids = collect_credential_saids(ctx, &issuer_prefix, ®istry)?;
let mut summaries = Vec::new();
for cred in credential_saids {
let tel = read_credential_tel(ctx.registry.as_ref(), &issuer_prefix, ®istry, &cred)?;
let state = validate_tel(&tel).map_err(|e| {
CredentialError::RegistryError(
auths_id::keri::credential_registry::CredentialRegistryError::Tel(e.to_string()),
)
})?;
let revoked = !state.is_valid(&cred);
let (subject_did, capabilities) = credential_claims(ctx, &issuer_prefix, &cred);
summaries.push(CredentialSummary {
credential_said: cred.as_str().to_string(),
subject_did,
capabilities,
revoked,
});
}
Ok(summaries)
}
fn collect_credential_saids(
ctx: &AuthsContext,
issuer_prefix: &Prefix,
registry: &Said,
) -> Result<Vec<Said>, CredentialError> {
use auths_id::keri::Seal;
use std::ops::ControlFlow;
let mut saids: Vec<Said> = Vec::new();
ctx.registry
.visit_events(issuer_prefix, 0, &mut |event| {
for seal in event.anchors() {
if let Seal::KeyEvent { i, .. } = seal
&& i.as_str() != registry.as_str()
{
let said = Said::new_unchecked(i.as_str().to_string());
if !saids.contains(&said) {
saids.push(said);
}
}
}
ControlFlow::Continue(())
})
.map_err(|e| {
CredentialError::RegistryError(
auths_id::keri::credential_registry::CredentialRegistryError::Tel(e.to_string()),
)
})?;
Ok(saids)
}
fn credential_claims(
ctx: &AuthsContext,
issuer_prefix: &Prefix,
credential_said: &Said,
) -> (String, Vec<String>) {
let Ok(Some(blob)) = ctx.registry.load_credential(issuer_prefix, credential_said) else {
return (String::new(), Vec::new());
};
let Ok(stored) = StoredCredential::from_bytes(&blob) else {
return (String::new(), Vec::new());
};
let subject = format!("did:keri:{}", stored.acdc.a.i);
let caps = stored
.acdc
.a
.data
.get(CAPABILITY_FIELD)
.and_then(|v| v.as_str())
.map(|c| c.split(',').map(|s| s.to_string()).collect())
.unwrap_or_default();
(subject, caps)
}