use auths_crypto::CryptoProvider;
use auths_keri::witness::StoredReceipt;
use auths_keri::witness::agreement::WitnessAgreement;
use crate::software_verify::verify_with_key_sync;
use crate::{CanonicalDid, Capability, IdentityDID};
use auths_keri::{
Acdc, CesrKey, Event, KeriPublicKey, KeyState, Prefix, Said, TelEvent, Threshold,
compute_capability_schema_said, validate_kel,
};
use chrono::{DateTime, Utc};
use crate::commit_kel::VerifierWitnessPolicy;
use crate::duplicity::{KelEventRef, detect_duplicity};
const CAPABILITY_FIELD: &str = "capability";
const EXPIRY_FIELD: &str = "expiry";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LifecycleEvent {
Vcp,
Iss,
Rev,
}
impl LifecycleEvent {
fn tag(self) -> &'static str {
match self {
LifecycleEvent::Vcp => "vcp",
LifecycleEvent::Iss => "iss",
LifecycleEvent::Rev => "rev",
}
}
}
impl std::fmt::Display for LifecycleEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.tag())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CredentialVerdict {
Valid {
issuer: IdentityDID,
subject: CanonicalDid,
caps: Vec<Capability>,
as_of: u128,
},
SaidMismatch,
SchemaInvalid,
IssuerSignatureInvalid,
RegistryNotEstablished,
CredentialRevoked {
revoked_at: u128,
},
Expired {
expired_at: DateTime<Utc>,
now: DateTime<Utc>,
},
WitnessQuorumNotMet {
event: LifecycleEvent,
collected: usize,
required: usize,
},
IssuerKelDuplicitous,
}
impl CredentialVerdict {
pub fn is_valid(&self) -> bool {
matches!(self, CredentialVerdict::Valid { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedAcdc {
pub acdc: Acdc,
pub signature: Vec<u8>,
}
pub async fn verify_credential(
signed: &SignedAcdc,
issuer_kel: &[Event],
tel_events: &[TelEvent],
receipts: &[StoredReceipt],
witness_policy: VerifierWitnessPolicy,
now: DateTime<Utc>,
_provider: &dyn CryptoProvider,
) -> CredentialVerdict {
verify_credential_sync(
signed,
issuer_kel,
tel_events,
receipts,
witness_policy,
now,
)
}
pub fn verify_credential_sync(
signed: &SignedAcdc,
issuer_kel: &[Event],
tel_events: &[TelEvent],
receipts: &[StoredReceipt],
witness_policy: VerifierWitnessPolicy,
now: DateTime<Utc>,
) -> CredentialVerdict {
let acdc = &signed.acdc;
if acdc.verify_said().is_err() {
return CredentialVerdict::SaidMismatch;
}
if validate_against_schema(acdc).is_err() {
return CredentialVerdict::SchemaInvalid;
}
if let Some(verdict) = check_expiry(acdc, now) {
return verdict;
}
if let Some(prefix) = issuer_kel.first().map(|e| e.prefix())
&& detect_duplicity(&kel_refs(issuer_kel, prefix)).is_diverging()
{
return CredentialVerdict::IssuerKelDuplicitous;
}
let issuer_state = match validate_kel(issuer_kel) {
Ok(state) => state,
Err(_) => return CredentialVerdict::RegistryNotEstablished,
};
let lifecycle = match locate_lifecycle(tel_events, issuer_kel) {
Some(lifecycle) => lifecycle,
None => return CredentialVerdict::RegistryNotEstablished,
};
let quorum = resolve_quorum(&lifecycle, issuer_kel, &issuer_state.prefix, receipts);
if let VerifierWitnessPolicy::RequireWitnesses = witness_policy
&& let Some(verdict) = quorum.fatal_under_quorum()
{
return verdict;
}
if !verify_issuer_signature(signed, issuer_kel, lifecycle.iss_anchor_seq) {
return CredentialVerdict::IssuerSignatureInvalid;
}
if let Some(revoked_at) =
effective_revocation(&lifecycle, &quorum, witness_policy, issuer_state.sequence)
{
return CredentialVerdict::CredentialRevoked { revoked_at };
}
let (Ok(issuer), Ok(subject)) = (
IdentityDID::parse(&format!("did:keri:{}", acdc.i)),
CanonicalDid::parse(&format!("did:keri:{}", acdc.a.i)),
) else {
return CredentialVerdict::SchemaInvalid;
};
let Ok(caps) = capability_claims(acdc)
.iter()
.map(|c| Capability::parse(c))
.collect::<Result<Vec<_>, _>>()
else {
return CredentialVerdict::SchemaInvalid;
};
CredentialVerdict::Valid {
issuer,
subject,
caps,
as_of: issuer_state.sequence,
}
}
fn validate_against_schema(acdc: &Acdc) -> Result<(), ()> {
let pinned = compute_capability_schema_said().map_err(|_| ())?;
if acdc.s != pinned {
return Err(());
}
if !acdc.a.data.contains_key(CAPABILITY_FIELD) {
return Err(());
}
let capability = acdc.a.data.get(CAPABILITY_FIELD).and_then(|v| v.as_str());
match capability {
Some(c) if !c.is_empty() => Ok(()),
_ => Err(()),
}
}
fn capability_claims(acdc: &Acdc) -> Vec<String> {
acdc.a
.data
.get(CAPABILITY_FIELD)
.and_then(|v| v.as_str())
.map(|c| vec![c.to_string()])
.unwrap_or_default()
}
fn check_expiry(acdc: &Acdc, now: DateTime<Utc>) -> Option<CredentialVerdict> {
let raw = acdc.a.data.get(EXPIRY_FIELD).and_then(|v| v.as_str())?;
let expired_at = DateTime::parse_from_rfc3339(raw).ok()?.with_timezone(&Utc);
(now >= expired_at).then_some(CredentialVerdict::Expired { expired_at, now })
}
struct Lifecycle {
iss_anchor_seq: u128,
vcp_anchor: AnchoredTelEvent,
iss_anchor: AnchoredTelEvent,
rev_anchors: Vec<AnchoredTelEvent>,
}
struct AnchoredTelEvent {
tel_said: Said,
kel_seq: u128,
}
fn locate_lifecycle(tel_events: &[TelEvent], issuer_kel: &[Event]) -> Option<Lifecycle> {
let vcp_said = tel_events.iter().find_map(|e| match e {
TelEvent::Vcp(vcp) => Some(vcp.d.clone()),
_ => None,
})?;
let iss_said = tel_events.iter().find_map(|e| match e {
TelEvent::Iss(iss) => Some(iss.d.clone()),
_ => None,
})?;
let vcp_anchor = anchor_position(issuer_kel, &vcp_said)?;
let iss_anchor = anchor_position(issuer_kel, &iss_said)?;
let rev_anchors = tel_events
.iter()
.filter_map(|e| match e {
TelEvent::Rev(rev) => anchor_position(issuer_kel, &rev.d),
_ => None,
})
.collect();
Some(Lifecycle {
iss_anchor_seq: iss_anchor.kel_seq,
vcp_anchor,
iss_anchor,
rev_anchors,
})
}
fn anchor_position(issuer_kel: &[Event], tel_said: &Said) -> Option<AnchoredTelEvent> {
for event in issuer_kel {
if event.is_interaction()
&& event
.anchors()
.iter()
.any(|seal| seal.digest_value().is_some_and(|d| d == tel_said))
{
return Some(AnchoredTelEvent {
tel_said: tel_said.clone(),
kel_seq: event.sequence().value(),
});
}
}
None
}
struct QuorumResolution {
vcp: QuorumOutcome,
iss: QuorumOutcome,
revs: Vec<(u128, QuorumOutcome)>,
}
impl QuorumResolution {
fn fatal_under_quorum(&self) -> Option<CredentialVerdict> {
for (event, outcome) in [
(LifecycleEvent::Vcp, &self.vcp),
(LifecycleEvent::Iss, &self.iss),
] {
if let QuorumOutcome::UnderQuorum {
collected,
required,
} = outcome
{
return Some(CredentialVerdict::WitnessQuorumNotMet {
event,
collected: *collected,
required: *required,
});
}
}
None
}
}
fn resolve_quorum(
lifecycle: &Lifecycle,
issuer_kel: &[Event],
issuer_prefix: &Prefix,
receipts: &[StoredReceipt],
) -> QuorumResolution {
let vcp = anchor_quorum(&lifecycle.vcp_anchor, issuer_kel, issuer_prefix, receipts);
let iss = anchor_quorum(&lifecycle.iss_anchor, issuer_kel, issuer_prefix, receipts);
let mut revs = Vec::with_capacity(lifecycle.rev_anchors.len());
for anchor in &lifecycle.rev_anchors {
let outcome = anchor_quorum(anchor, issuer_kel, issuer_prefix, receipts);
revs.push((anchor.kel_seq, outcome));
}
QuorumResolution { vcp, iss, revs }
}
fn effective_revocation(
lifecycle: &Lifecycle,
quorum: &QuorumResolution,
policy: VerifierWitnessPolicy,
presentation_seq: u128,
) -> Option<u128> {
lifecycle
.rev_anchors
.iter()
.filter(|rev| rev.kel_seq <= presentation_seq)
.filter(|rev| rev_counts(rev.kel_seq, quorum, policy))
.map(|rev| rev.kel_seq)
.min()
}
fn rev_counts(kel_seq: u128, quorum: &QuorumResolution, policy: VerifierWitnessPolicy) -> bool {
match policy {
VerifierWitnessPolicy::Warn => true,
VerifierWitnessPolicy::RequireWitnesses => quorum
.revs
.iter()
.find(|(seq, _)| *seq == kel_seq)
.is_some_and(|(_, outcome)| matches!(outcome, QuorumOutcome::Met)),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum QuorumOutcome {
Met,
UnderQuorum {
collected: usize,
required: usize,
},
}
fn anchor_quorum(
anchor: &AnchoredTelEvent,
issuer_kel: &[Event],
issuer_prefix: &Prefix,
receipts: &[StoredReceipt],
) -> QuorumOutcome {
let backer_state = match replay_to_seq(issuer_kel, anchor.kel_seq) {
Some(state) => state,
None => {
return QuorumOutcome::UnderQuorum {
collected: 0,
required: 0,
};
}
};
let backers = &backer_state.backers;
let bt = &backer_state.backer_threshold;
if backers.is_empty() {
return QuorumOutcome::Met;
}
let agreement = WitnessAgreement::new(1);
let sn = anchor.kel_seq as u64;
agreement.submit_event(issuer_prefix, sn, &anchor.tel_said, bt, backers);
if agreement.is_accepted(issuer_prefix, sn, &anchor.tel_said) {
return QuorumOutcome::Met;
}
let mut collected = 0usize;
for receipt in receipts {
if receipt.signed.receipt.d != anchor.tel_said {
continue;
}
if !backers.contains(&receipt.witness) {
continue;
}
if !verify_receipt(receipt) {
continue;
}
collected += 1;
agreement.add_receipt(
issuer_prefix,
sn,
&anchor.tel_said,
receipt.witness.as_str(),
);
}
if agreement.is_accepted(issuer_prefix, sn, &anchor.tel_said) {
QuorumOutcome::Met
} else {
QuorumOutcome::UnderQuorum {
collected,
required: required_count(bt, backers.len()),
}
}
}
fn required_count(bt: &Threshold, backer_count: usize) -> usize {
bt.simple_value()
.map(|v| v as usize)
.unwrap_or(backer_count)
}
fn verify_receipt(receipt: &StoredReceipt) -> bool {
let Ok(payload) = serde_json::to_vec(&receipt.signed.receipt) else {
return false;
};
let Ok(key) = KeriPublicKey::parse(receipt.witness.as_str()) else {
return false;
};
verify_with_key_sync(&key, &payload, &receipt.signed.signature)
}
fn verify_issuer_signature(
signed: &SignedAcdc,
issuer_kel: &[Event],
iss_anchor_seq: u128,
) -> bool {
let Some(state) = replay_to_seq(issuer_kel, iss_anchor_seq) else {
return false;
};
let Ok(wire) = signed.acdc.to_wire_bytes() else {
return false;
};
for cesr in &state.current_keys {
if let Some(key) = parse_cesr_key(cesr)
&& verify_with_key_sync(&key, &wire, &signed.signature)
{
return true;
}
}
false
}
fn replay_to_seq(issuer_kel: &[Event], seq: u128) -> Option<KeyState> {
let subset: Vec<Event> = issuer_kel
.iter()
.take_while(|e| e.sequence().value() <= seq)
.cloned()
.collect();
validate_kel(&subset).ok()
}
fn parse_cesr_key(cesr: &CesrKey) -> Option<KeriPublicKey> {
KeriPublicKey::parse(cesr.as_str()).ok()
}
fn kel_refs<'a>(issuer_kel: &'a [Event], prefix: &'a Prefix) -> Vec<KelEventRef<'a>> {
issuer_kel
.iter()
.map(|e| KelEventRef {
prefix: prefix.as_str(),
seq: e.sequence().value() as u64,
said: e.said().as_str(),
})
.collect()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use auths_keri::{KeriSequence, Vcp};
fn lifecycle_with_revs(revs: Vec<u128>) -> Lifecycle {
Lifecycle {
iss_anchor_seq: 1,
vcp_anchor: AnchoredTelEvent {
tel_said: Said::new_unchecked("Evcp".into()),
kel_seq: 0,
},
iss_anchor: AnchoredTelEvent {
tel_said: Said::new_unchecked("Eiss".into()),
kel_seq: 1,
},
rev_anchors: revs
.into_iter()
.map(|s| AnchoredTelEvent {
tel_said: Said::new_unchecked(format!("Erev{s}")),
kel_seq: s,
})
.collect(),
}
}
fn warn_quorum(rev_seqs: &[u128]) -> QuorumResolution {
QuorumResolution {
vcp: QuorumOutcome::Met,
iss: QuorumOutcome::Met,
revs: rev_seqs.iter().map(|s| (*s, QuorumOutcome::Met)).collect(),
}
}
#[test]
fn revocation_ordered_by_kel_position() {
let lc = lifecycle_with_revs(vec![3]);
let q = warn_quorum(&[3]);
assert_eq!(
effective_revocation(&lc, &q, VerifierWitnessPolicy::Warn, 2),
None
);
assert_eq!(
effective_revocation(&lc, &q, VerifierWitnessPolicy::Warn, 3),
Some(3)
);
}
#[test]
fn under_quorum_rev_skipped_under_require_witnesses() {
let lc = lifecycle_with_revs(vec![3]);
let q = QuorumResolution {
vcp: QuorumOutcome::Met,
iss: QuorumOutcome::Met,
revs: vec![(
3,
QuorumOutcome::UnderQuorum {
collected: 0,
required: 1,
},
)],
};
assert_eq!(
effective_revocation(&lc, &q, VerifierWitnessPolicy::RequireWitnesses, 9),
None
);
assert_eq!(
effective_revocation(&lc, &q, VerifierWitnessPolicy::Warn, 9),
Some(3)
);
}
#[test]
fn lifecycle_event_display_names_anchor() {
assert_eq!(LifecycleEvent::Vcp.to_string(), "vcp");
assert_eq!(LifecycleEvent::Iss.to_string(), "iss");
assert_eq!(LifecycleEvent::Rev.to_string(), "rev");
}
#[test]
fn required_count_from_simple_threshold() {
assert_eq!(required_count(&Threshold::Simple(2), 3), 2);
}
#[test]
fn vcp_registry_accessor_is_reused() {
let vcp = Vcp::new(Prefix::new_unchecked("Eissuer".into()), "0Anonce".into());
let _ = vcp.s.value();
let _ = KeriSequence::new(0);
}
}