use auths_core::storage::keychain::KeyAlias;
use auths_crypto::RingCryptoProvider;
use auths_rp::{
Audience, ChallengeError, ChallengeStore, Denied, NONCE_LEN, Nonce, VerifiedPrincipal,
WireError, WirePresentation,
};
use auths_verifier::{
PresentationBinding, PresentationVerdict, VerifierWitnessPolicy, verify_presentation,
};
use chrono::{DateTime, Utc};
use auths_id::keri::types::Prefix;
use auths_id::policy::context_from_credential;
use crate::context::AuthsContext;
use crate::domains::credentials::error::CredentialError;
use crate::domains::credentials::present_inputs::load_presentation_inputs;
use crate::domains::org::error::OrgError;
use crate::domains::org::policy::{evaluate_with_org_policy, load_org_policy};
#[derive(Debug, thiserror::Error)]
pub enum PresentationAuthError {
#[error("wire: {0}")]
Wire(WireError),
#[error("nonce must be 32 bytes")]
NonceLength,
#[error("challenge: {0}")]
Challenge(ChallengeError),
#[error("resolve: {0}")]
Resolve(CredentialError),
#[error("denied: {0}")]
Denied(Denied),
#[error("org policy denied: {reason}")]
PolicyDenied {
reason: String,
},
#[error("org policy could not be evaluated: {0}")]
Policy(OrgError),
}
impl PresentationAuthError {
pub fn http_status(&self) -> u16 {
match self {
PresentationAuthError::Wire(_) | PresentationAuthError::NonceLength => 400,
PresentationAuthError::Denied(denied) => denied.http_status(),
PresentationAuthError::PolicyDenied { .. } | PresentationAuthError::Policy(_) => 403,
PresentationAuthError::Challenge(_) | PresentationAuthError::Resolve(_) => 401,
}
}
}
pub async fn authenticate_presentation(
ctx: &AuthsContext,
issuer_alias: &KeyAlias,
challenges: &dyn ChallengeStore,
config_audience: &Audience,
wire: WirePresentation,
now: DateTime<Utc>,
) -> Result<VerifiedPrincipal, PresentationAuthError> {
let (envelope, presented_audience) = wire.parse().map_err(PresentationAuthError::Wire)?;
let nonce = binding_nonce(&envelope.binding)?;
let expected = challenges
.consume(&presented_audience, &nonce, now)
.map_err(PresentationAuthError::Challenge)?;
let inputs = load_presentation_inputs(ctx, issuer_alias, &envelope.credential_said)
.map_err(PresentationAuthError::Resolve)?;
let verdict = verify_presentation(
&envelope,
&inputs.signed,
&inputs.issuer_kel,
&inputs.tel,
&inputs.receipts,
VerifierWitnessPolicy::Warn,
&inputs.subject_kel,
&inputs.subject_delegator_kel,
config_audience.as_str(),
Some(expected.as_bytes()),
now,
&RingCryptoProvider,
)
.await;
enforce_issuer_policy(ctx, &verdict, now)?;
VerifiedPrincipal::from_verdict(verdict).map_err(PresentationAuthError::Denied)
}
fn enforce_issuer_policy(
ctx: &AuthsContext,
verdict: &PresentationVerdict,
now: DateTime<Utc>,
) -> Result<(), PresentationAuthError> {
let PresentationVerdict::Valid {
issuer, subject, ..
} = verdict
else {
return Ok(()); };
let issuer_prefix = Prefix::new_unchecked(
issuer
.as_str()
.strip_prefix("did:keri:")
.unwrap_or(issuer.as_str())
.to_string(),
);
let Some(policy) =
load_org_policy(ctx, &issuer_prefix).map_err(PresentationAuthError::Policy)?
else {
return Ok(()); };
let eval_ctx = context_from_credential(verdict, now)
.map_err(|e| PresentationAuthError::Policy(OrgError::InvalidDid(e.to_string())))?;
let decision = evaluate_with_org_policy(&policy, &eval_ctx);
crate::audit::emit_policy_decision(ctx, "request", subject.as_str(), &decision);
if decision.is_allowed() {
Ok(())
} else {
Err(PresentationAuthError::PolicyDenied {
reason: format!("{} [{}]", decision.message, decision.reason),
})
}
}
fn binding_nonce(binding: &PresentationBinding) -> Result<Nonce, PresentationAuthError> {
let bytes = match binding {
PresentationBinding::Challenge { nonce } => nonce.as_slice(),
PresentationBinding::Ttl { nonce, .. } => nonce.as_slice(),
};
let arr: [u8; NONCE_LEN] = bytes
.try_into()
.map_err(|_| PresentationAuthError::NonceLength)?;
Ok(Nonce::from_bytes(arr))
}