use auths_crypto::CryptoProvider;
use auths_keri::{
CesrKey, DelegatorKelLookup, Event, KeriPublicKey, KeyState, Prefix, Said, Seal, SourceSeal,
validate_kel_with_lookup,
};
use chrono::{DateTime, Utc};
use crate::credential::{CredentialVerdict, SignedAcdc, verify_credential_sync};
use crate::software_verify::verify_with_key_sync;
use crate::{CanonicalDid, Capability, IdentityDID};
const ROLE_FIELD: &str = "role";
const EXPIRY_FIELD: &str = "expiry";
struct DelegatorSeals<'a> {
delegator_kel: &'a [Event],
}
impl DelegatorKelLookup for DelegatorSeals<'_> {
fn find_seal(&self, _delegator_aid: &Prefix, seal_said: &Said) -> Option<SourceSeal> {
for event in self.delegator_kel {
for seal in event.anchors() {
if let Seal::KeyEvent { d, .. } = seal
&& d == seal_said
{
return Some(SourceSeal {
s: event.sequence(),
d: event.said().clone(),
});
}
}
}
None
}
}
fn replay_subject(subject_kel: &[Event], subject_delegator_kel: &[Event]) -> Option<KeyState> {
let lookup = DelegatorSeals {
delegator_kel: subject_delegator_kel,
};
validate_kel_with_lookup(subject_kel, 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,
caps: Vec<Capability>,
role: Option<String>,
expires_at: Option<DateTime<Utc>>,
},
HolderNotCurrentKey,
WrongAudience,
NonceMismatchOrConsumed,
Expired,
SubjectKelInvalid,
CredentialNotValid(CredentialVerdict),
}
impl PresentationVerdict {
pub fn is_honored(&self) -> bool {
matches!(self, PresentationVerdict::Valid { .. })
}
}
#[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>,
_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,
)
}
#[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>,
) -> PresentationVerdict {
let credential_verdict = verify_credential_sync(
signed,
issuer_kel,
tel_events,
receipts,
witness_policy,
now,
);
if !credential_verdict.is_valid() {
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) = match credential_verdict {
CredentialVerdict::Valid { issuer, caps, .. } => (issuer, caps),
other => return PresentationVerdict::CredentialNotValid(other),
};
let grant = GrantFacts {
issuer,
caps,
role: read_attribute(signed, ROLE_FIELD),
expires_at: read_expiry(signed),
};
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>>,
}
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)) => {
(nonce.as_slice() != 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 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,
caps: grant.caps,
role: grant.role,
expires_at: grant.expires_at,
};
}
}
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)
);
}
}