use std::ops::ControlFlow;
use auths_crypto::RingCryptoProvider;
use auths_id::keri::Event;
use auths_id::keri::credential_registry::{find_registry, read_credential_tel};
use auths_id::keri::types::Prefix;
use auths_id::storage::GitReceiptStorage;
use auths_id::storage::receipts::ReceiptStorage;
use auths_keri::witness::StoredReceipt;
use auths_keri::{Said, TelEvent};
use chrono::{DateTime, Utc};
pub use auths_verifier::VerifierWitnessPolicy;
use crate::context::AuthsContext;
use crate::domains::credentials::error::CredentialError;
use crate::domains::credentials::stored::StoredCredential;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedAsOf {
pub seq: u128,
pub said: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CredentialVerdict {
Resolved {
verdict: auths_verifier::CredentialVerdict,
as_of: ResolvedAsOf,
},
StaleOrUnresolvable {
as_of: ResolvedAsOf,
reason: String,
},
}
impl CredentialVerdict {
pub fn is_valid(&self) -> bool {
matches!(
self,
CredentialVerdict::Resolved { verdict, .. } if verdict.is_valid()
)
}
}
pub async fn verify(
ctx: &AuthsContext,
stored: &StoredCredential,
witness_policy: VerifierWitnessPolicy,
now: DateTime<Utc>,
) -> Result<CredentialVerdict, CredentialError> {
let issuer_prefix = Prefix::new_unchecked(stored.acdc.i.as_str().to_string());
let issuer_kel = resolve_kel(ctx, &issuer_prefix)?;
if issuer_kel.is_empty() {
return Err(CredentialError::StaleOrUnresolvable {
reason: format!("issuer KEL not found: {issuer_prefix}"),
});
}
let tel = resolve_tel(ctx, &issuer_prefix, &stored.acdc.ri, &stored.acdc.d)?;
let receipts = collect_lifecycle_receipts(ctx, &issuer_prefix, &issuer_kel, &tel);
let as_of = tip_as_of(&issuer_kel);
if let VerifierWitnessPolicy::RequireWitnesses = witness_policy
&& declares_backers(ctx, &issuer_prefix)
&& receipts.is_empty()
{
return Ok(CredentialVerdict::StaleOrUnresolvable {
as_of,
reason:
"issuer declares witnesses but no receipts were reachable for any lifecycle anchor"
.to_string(),
});
}
let signed = auths_verifier::SignedAcdc {
acdc: stored.acdc.clone(),
signature: stored.signature.clone(),
};
let provider = RingCryptoProvider;
let verdict = auths_verifier::verify_credential(
&signed,
&issuer_kel,
&tel,
&receipts,
witness_policy,
now,
&provider,
)
.await;
Ok(CredentialVerdict::Resolved { verdict, as_of })
}
pub async fn verify_by_said(
ctx: &AuthsContext,
issuer_alias: &auths_core::storage::keychain::KeyAlias,
credential_said: &str,
witness_policy: VerifierWitnessPolicy,
now: DateTime<Utc>,
) -> Result<CredentialVerdict, CredentialError> {
let issuer_prefix =
crate::domains::credentials::issue::resolve_issuer_prefix(ctx, issuer_alias)?;
let cred = Said::new_unchecked(credential_said.to_string());
let blob = ctx
.registry
.load_credential(&issuer_prefix, &cred)
.map_err(|e| CredentialError::StaleOrUnresolvable {
reason: format!("credential blob read failed: {e}"),
})?
.ok_or_else(|| CredentialError::StaleOrUnresolvable {
reason: format!("credential not found: {credential_said}"),
})?;
let stored =
StoredCredential::from_bytes(&blob).map_err(|e| CredentialError::StaleOrUnresolvable {
reason: format!("credential blob parse failed: {e}"),
})?;
verify(ctx, &stored, witness_policy, now).await
}
pub(crate) fn resolve_kel(
ctx: &AuthsContext,
prefix: &Prefix,
) -> Result<Vec<Event>, CredentialError> {
let mut events = Vec::new();
ctx.registry
.visit_events(prefix, 0, &mut |e| {
events.push(e.clone());
ControlFlow::Continue(())
})
.map_err(|e| CredentialError::StaleOrUnresolvable {
reason: format!("issuer KEL read failed: {e}"),
})?;
Ok(events)
}
pub(crate) fn resolve_tel(
ctx: &AuthsContext,
issuer_prefix: &Prefix,
registry_said: &Said,
credential_said: &Said,
) -> Result<Vec<TelEvent>, CredentialError> {
let registry = match find_registry(ctx.registry.as_ref(), issuer_prefix)? {
Some(reg) => reg,
None => Said::new_unchecked(registry_said.as_str().to_string()),
};
Ok(read_credential_tel(
ctx.registry.as_ref(),
issuer_prefix,
®istry,
credential_said,
)?)
}
pub(crate) fn tip_as_of(issuer_kel: &[Event]) -> ResolvedAsOf {
match issuer_kel.last() {
Some(tip) => ResolvedAsOf {
seq: tip.sequence().value(),
said: tip.said().as_str().to_string(),
},
None => ResolvedAsOf {
seq: 0,
said: String::new(),
},
}
}
fn declares_backers(ctx: &AuthsContext, issuer_prefix: &Prefix) -> bool {
ctx.registry
.get_key_state(issuer_prefix)
.map(|state| !state.backers.is_empty())
.unwrap_or(false)
}
pub(crate) fn collect_lifecycle_receipts(
ctx: &AuthsContext,
issuer_prefix: &Prefix,
issuer_kel: &[Event],
tel: &[TelEvent],
) -> Vec<StoredReceipt> {
let Some(repo_path) = ctx.repo_path.as_ref() else {
return Vec::new();
};
let lookup = GitReceiptStorage::new(repo_path.clone());
let mut saids: Vec<Said> = Vec::new();
for event in issuer_kel {
if event.is_inception() || event.is_rotation() {
push_unique(&mut saids, event.said().clone());
}
}
for event in tel {
push_unique(&mut saids, tel_event_said(event));
}
let mut receipts: Vec<StoredReceipt> = Vec::new();
for said in &saids {
if let Ok(Some(event_receipts)) = lookup.get_receipts(issuer_prefix, said) {
for stored in event_receipts.receipts {
receipts.push(stored);
}
}
}
receipts
}
fn push_unique(saids: &mut Vec<Said>, said: Said) {
if !saids.contains(&said) {
saids.push(said);
}
}
fn tel_event_said(event: &TelEvent) -> Said {
match event {
TelEvent::Vcp(vcp) => vcp.d.clone(),
TelEvent::Iss(iss) => iss.d.clone(),
TelEvent::Rev(rev) => rev.d.clone(),
}
}