use auths_crypto::CryptoProvider;
use auths_keri::{CesrKey, Event, KelSealIndex, KeriPublicKey, KeyState, TrustedKel};
use chrono::{DateTime, Utc};
use subtle::ConstantTimeEq;
use crate::credential::{CredentialVerdict, SignedAcdc, verify_credential_sync};
use crate::freshness::{Freshness, FreshnessEvidence, FreshnessPolicy};
use crate::software_verify::verify_with_key_sync;
use crate::{CanonicalDid, Capability, IdentityDID};
const ROLE_FIELD: &str = "role";
const EXPIRY_FIELD: &str = "expiry";
fn replay_subject(subject_kel: &[Event], subject_delegator_kel: &[Event]) -> Option<KeyState> {
let lookup = KelSealIndex::from_events(subject_delegator_kel);
TrustedKel::from_trusted_source(subject_kel)
.replay_with_lookup(Some(&lookup))
.ok()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PresentationBinding {
Challenge {
nonce: Vec<u8>,
},
Ttl {
nonce: Vec<u8>,
not_after: DateTime<Utc>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PresentationEnvelope {
pub credential_said: String,
pub audience: String,
pub binding: PresentationBinding,
pub signature: Vec<u8>,
}
impl PresentationEnvelope {
fn signed_message(credential_said: &str, audience: &str, nonce: &[u8]) -> Vec<u8> {
let mut message =
Vec::with_capacity(credential_said.len() + audience.len() + nonce.len() + 2);
message.extend_from_slice(credential_said.as_bytes());
message.push(0);
message.extend_from_slice(audience.as_bytes());
message.push(0);
message.extend_from_slice(nonce);
message
}
fn nonce(&self) -> &[u8] {
match &self.binding {
PresentationBinding::Challenge { nonce } => nonce,
PresentationBinding::Ttl { nonce, .. } => nonce,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PresentationVerdict {
Valid {
issuer: IdentityDID,
subject: CanonicalDid,
subject_root: CanonicalDid,
caps: Vec<Capability>,
role: Option<String>,
expires_at: Option<DateTime<Utc>>,
freshness: Freshness,
as_of: u128,
},
HolderNotCurrentKey,
WrongAudience,
NonceMismatchOrConsumed,
Expired,
SubjectKelInvalid,
CredentialNotValid(CredentialVerdict),
}
impl PresentationVerdict {
pub fn is_honored(&self) -> bool {
matches!(self, PresentationVerdict::Valid { .. })
}
pub fn freshness(&self) -> Option<Freshness> {
match self {
PresentationVerdict::Valid { freshness, .. } => Some(*freshness),
_ => None,
}
}
pub fn as_of(&self) -> Option<u128> {
match self {
PresentationVerdict::Valid { as_of, .. } => Some(*as_of),
_ => None,
}
}
}
#[allow(clippy::too_many_arguments)]
pub async fn verify_presentation(
envelope: &PresentationEnvelope,
signed: &SignedAcdc,
issuer_kel: &[Event],
tel_events: &[auths_keri::TelEvent],
receipts: &[auths_keri::witness::StoredReceipt],
witness_policy: crate::commit_kel::VerifierWitnessPolicy,
subject_kel: &[Event],
subject_delegator_kel: &[Event],
expected_audience: &str,
expected_challenge: Option<&[u8]>,
now: DateTime<Utc>,
policy: &FreshnessPolicy,
fresher_tip_seq: Option<u128>,
_provider: &dyn CryptoProvider,
) -> PresentationVerdict {
verify_presentation_sync(
envelope,
signed,
issuer_kel,
tel_events,
receipts,
witness_policy,
subject_kel,
subject_delegator_kel,
expected_audience,
expected_challenge,
now,
policy,
fresher_tip_seq,
)
}
#[allow(clippy::too_many_arguments)]
pub fn verify_presentation_sync(
envelope: &PresentationEnvelope,
signed: &SignedAcdc,
issuer_kel: &[Event],
tel_events: &[auths_keri::TelEvent],
receipts: &[auths_keri::witness::StoredReceipt],
witness_policy: crate::commit_kel::VerifierWitnessPolicy,
subject_kel: &[Event],
subject_delegator_kel: &[Event],
expected_audience: &str,
expected_challenge: Option<&[u8]>,
now: DateTime<Utc>,
policy: &FreshnessPolicy,
fresher_tip_seq: Option<u128>,
) -> PresentationVerdict {
let credential_verdict = verify_credential_sync(
signed,
issuer_kel,
tel_events,
receipts,
witness_policy,
now,
);
let evidence = match (&credential_verdict, fresher_tip_seq) {
(CredentialVerdict::Valid { as_of, .. }, Some(latest_seq)) => {
FreshnessEvidence::FresherTip {
latest_seq,
slice_as_of: *as_of,
}
}
_ => FreshnessEvidence::Offline,
};
let credential_verdict = credential_verdict.with_freshness(policy, evidence);
if !credential_verdict.is_trusted(policy) {
return PresentationVerdict::CredentialNotValid(credential_verdict);
}
if envelope.credential_said != signed.acdc.d.as_str() {
return PresentationVerdict::CredentialNotValid(CredentialVerdict::SaidMismatch);
}
if envelope.audience != expected_audience {
return PresentationVerdict::WrongAudience;
}
if let Some(verdict) = check_binding(&envelope.binding, expected_challenge, now) {
return verdict;
}
let (issuer, caps, freshness, as_of) = match credential_verdict {
CredentialVerdict::Valid {
issuer,
caps,
freshness,
as_of,
..
} => (issuer, caps, freshness, as_of),
other => return PresentationVerdict::CredentialNotValid(other),
};
let grant = GrantFacts {
issuer,
caps,
role: read_attribute(signed, ROLE_FIELD),
expires_at: read_expiry(signed),
freshness,
as_of,
};
verify_holder_signature(envelope, signed, subject_kel, subject_delegator_kel, grant)
}
struct GrantFacts {
issuer: IdentityDID,
caps: Vec<Capability>,
role: Option<String>,
expires_at: Option<DateTime<Utc>>,
freshness: Freshness,
as_of: u128,
}
fn read_attribute(signed: &SignedAcdc, field: &str) -> Option<String> {
signed
.acdc
.a
.data
.get(field)
.and_then(|v| v.as_str())
.map(str::to_string)
}
fn read_expiry(signed: &SignedAcdc) -> Option<DateTime<Utc>> {
let raw = read_attribute(signed, EXPIRY_FIELD)?;
DateTime::parse_from_rfc3339(&raw)
.ok()
.map(|dt| dt.with_timezone(&Utc))
}
fn check_binding(
binding: &PresentationBinding,
expected_challenge: Option<&[u8]>,
now: DateTime<Utc>,
) -> Option<PresentationVerdict> {
match (binding, expected_challenge) {
(PresentationBinding::Challenge { nonce }, Some(expected)) => {
(!bool::from(nonce.as_slice().ct_eq(expected)))
.then_some(PresentationVerdict::NonceMismatchOrConsumed)
}
(PresentationBinding::Ttl { not_after, .. }, None) => {
(now >= *not_after).then_some(PresentationVerdict::Expired)
}
(PresentationBinding::Challenge { .. }, None)
| (PresentationBinding::Ttl { .. }, Some(_)) => {
Some(PresentationVerdict::NonceMismatchOrConsumed)
}
}
}
fn verify_holder_signature(
envelope: &PresentationEnvelope,
signed: &SignedAcdc,
subject_kel: &[Event],
subject_delegator_kel: &[Event],
grant: GrantFacts,
) -> PresentationVerdict {
let Some(state) = replay_subject(subject_kel, subject_delegator_kel) else {
return PresentationVerdict::SubjectKelInvalid;
};
let Ok(subject) = CanonicalDid::parse(&format!("did:keri:{}", signed.acdc.a.i)) else {
return PresentationVerdict::SubjectKelInvalid;
};
let subject_root = match subject_kel.iter().find_map(|event| event.delegator()) {
Some(delegator) => {
let Ok(root) = CanonicalDid::parse(&format!("did:keri:{delegator}")) else {
return PresentationVerdict::SubjectKelInvalid;
};
root
}
None => subject.clone(),
};
let message = PresentationEnvelope::signed_message(
&envelope.credential_said,
&envelope.audience,
envelope.nonce(),
);
for cesr in &state.current_keys {
if let Some(key) = parse_cesr_key(cesr)
&& verify_with_key_sync(&key, &message, &envelope.signature)
{
return PresentationVerdict::Valid {
issuer: grant.issuer,
subject,
subject_root,
caps: grant.caps,
role: grant.role,
expires_at: grant.expires_at,
freshness: grant.freshness,
as_of: grant.as_of,
};
}
}
PresentationVerdict::HolderNotCurrentKey
}
fn parse_cesr_key(cesr: &CesrKey) -> Option<KeriPublicKey> {
KeriPublicKey::parse(cesr.as_str()).ok()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn ttl_envelope(not_after: DateTime<Utc>) -> PresentationEnvelope {
PresentationEnvelope {
credential_said: "ECred".to_string(),
audience: "aud".to_string(),
binding: PresentationBinding::Ttl {
nonce: vec![1, 2, 3],
not_after,
},
signature: vec![],
}
}
fn challenge_envelope(nonce: Vec<u8>) -> PresentationEnvelope {
PresentationEnvelope {
credential_said: "ECred".to_string(),
audience: "aud".to_string(),
binding: PresentationBinding::Challenge { nonce },
signature: vec![],
}
}
#[test]
fn signed_message_separates_fields() {
let a = PresentationEnvelope::signed_message("E1", "aud", &[9]);
let b = PresentationEnvelope::signed_message("E1a", "ud", &[9]);
assert_ne!(a, b, "field boundaries must be unambiguous");
}
#[test]
fn challenge_match_passes_binding() {
let env = challenge_envelope(vec![7, 7, 7]);
let now = chrono::Utc::now();
assert_eq!(check_binding(&env.binding, Some(&[7, 7, 7]), now), None);
}
#[test]
fn challenge_mismatch_rejected() {
let env = challenge_envelope(vec![7, 7, 7]);
let now = chrono::Utc::now();
assert_eq!(
check_binding(&env.binding, Some(&[1, 2, 3]), now),
Some(PresentationVerdict::NonceMismatchOrConsumed)
);
}
#[test]
fn consumed_challenge_is_none_expected() {
let env = challenge_envelope(vec![7, 7, 7]);
let now = chrono::Utc::now();
assert_eq!(
check_binding(&env.binding, None, now),
Some(PresentationVerdict::NonceMismatchOrConsumed)
);
}
#[test]
fn ttl_unexpired_passes_binding() {
let now = chrono::Utc::now();
let env = ttl_envelope(now + chrono::Duration::seconds(60));
assert_eq!(check_binding(&env.binding, None, now), None);
}
#[test]
fn ttl_expired_rejected() {
let now = chrono::Utc::now();
let env = ttl_envelope(now - chrono::Duration::seconds(1));
assert_eq!(
check_binding(&env.binding, None, now),
Some(PresentationVerdict::Expired)
);
}
}