use std::collections::{BTreeMap, BTreeSet};
use indexmap::IndexMap;
use serde_json::Value;
use crate::did::Uri;
use crate::jose::{CompactJws, DefaultVerifier, JwsHeader, JwsVerifier};
#[cfg(feature = "sd-jwt")]
use crate::sd_jwt_vc::{SdJwtCredentialV2, SdJwtCredentialValidator, SdJwtValidationOptions};
use crate::vc::{
CredentialError, CredentialV2, EnvelopedVc, Jwt, JwtClaims, Party, Presentation,
PresentationV2, Subject,
};
#[cfg(feature = "sd-jwt")]
use super::CredentialStatus;
use super::credential::{NO_TYP, check_controller, check_typ, decode, decode_v2, plain_claims};
use super::{
DecodedCredential, DecodedCredentialV2, FailFast, JwtCredentialValidator, KeyResolver, KeyRole,
PresentationValidationError, PresentationValidationOptions, StatusCheck,
SubjectHolderRelationship, ValidationError,
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DecodedPresentation {
pub header: JwsHeader,
pub claims: JwtClaims,
pub presentation: Presentation,
pub credentials: Vec<DecodedCredential>,
}
impl DecodedPresentation {
pub fn holder(&self) -> Option<&Uri> {
self.presentation.holder.as_ref().map(Party::id)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DecodedPresentationV2 {
pub header: JwsHeader,
pub claims: JwtClaims,
pub presentation: PresentationV2,
pub credentials: Vec<PresentedCredentialV2>,
}
impl DecodedPresentationV2 {
pub fn holder(&self) -> Option<&Uri> {
self.presentation.holder.as_ref().map(Party::id)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum PresentedCredentialV2 {
Jwt(Box<DecodedCredentialV2>),
#[cfg(feature = "sd-jwt")]
#[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
SdJwt(Box<SdJwtCredentialV2>),
}
impl PresentedCredentialV2 {
pub fn credential(&self) -> &CredentialV2 {
match self {
Self::Jwt(decoded) => &decoded.credential,
#[cfg(feature = "sd-jwt")]
Self::SdJwt(credential) => credential.credential(),
}
}
pub fn header(&self) -> &JwsHeader {
match self {
Self::Jwt(decoded) => &decoded.header,
#[cfg(feature = "sd-jwt")]
Self::SdJwt(credential) => credential.header(),
}
}
pub fn as_jwt(&self) -> Option<&DecodedCredentialV2> {
match self {
Self::Jwt(decoded) => Some(decoded),
#[cfg(feature = "sd-jwt")]
Self::SdJwt(_) => None,
}
}
#[cfg(feature = "sd-jwt")]
#[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
pub fn as_sd_jwt(&self) -> Option<&SdJwtCredentialV2> {
match self {
Self::SdJwt(credential) => Some(credential),
Self::Jwt(_) => None,
}
}
}
pub struct JwtPresentationValidator<R, V = DefaultVerifier> {
credentials: JwtCredentialValidator<R, V>,
}
impl<R: KeyResolver, V: JwsVerifier> JwtPresentationValidator<R, V> {
pub fn new(resolver: R, verifier: V) -> Self {
Self {
credentials: JwtCredentialValidator::new(resolver, verifier),
}
}
pub fn from_credential_validator(credentials: JwtCredentialValidator<R, V>) -> Self {
Self { credentials }
}
#[must_use]
pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
self.credentials = self.credentials.with_status_check(check);
self
}
pub fn credential_validator(&self) -> &JwtCredentialValidator<R, V> {
&self.credentials
}
pub async fn validate(
&self,
jwt: &Jwt,
options: &PresentationValidationOptions,
) -> Result<DecodedPresentation, PresentationValidationError> {
let mut decoded = self
.validate_presentation(jwt, options)
.await
.map_err(PresentationValidationError::presentation)?;
let holder = decoded.holder().cloned();
let mut errors = BTreeMap::new();
for (index, credential) in decoded
.presentation
.verifiable_credential
.iter()
.enumerate()
{
let result = self
.credentials
.validate(credential, &options.credentials)
.await
.and_then(|validated| {
let credential = &validated.credential;
check_subject_holder(
&credential.properties,
credential.credential_subject.iter(),
holder.as_ref(),
options.subject_holder,
)?;
Ok(validated)
});
match result {
Ok(validated) => decoded.credentials.push(validated),
Err(error) => {
errors.insert(index, error);
if options.fail_fast == FailFast::FirstError {
break;
}
}
}
}
if errors.is_empty() {
Ok(decoded)
} else {
Err(PresentationValidationError::credentials(errors))
}
}
pub async fn validate_v2(
&self,
jwt: &Jwt,
options: &PresentationValidationOptions,
) -> Result<DecodedPresentationV2, PresentationValidationError> {
let mut decoded = self
.validate_presentation_v2(jwt, options)
.await
.map_err(PresentationValidationError::presentation)?;
let holder = decoded.holder().cloned();
let mut errors = BTreeMap::new();
for (index, enveloped) in decoded
.presentation
.verifiable_credential
.iter()
.enumerate()
{
match self
.validate_enveloped(enveloped, holder.as_ref(), options)
.await
{
Ok(validated) => decoded.credentials.push(validated),
Err(error) => {
errors.insert(index, error);
if options.fail_fast == FailFast::FirstError {
break;
}
}
}
}
if errors.is_empty() {
Ok(decoded)
} else {
Err(PresentationValidationError::credentials(errors))
}
}
async fn validate_enveloped(
&self,
enveloped: &EnvelopedVc,
holder: Option<&Uri>,
options: &PresentationValidationOptions,
) -> Result<PresentedCredentialV2, ValidationError> {
#[cfg(feature = "sd-jwt")]
if enveloped.is_sd_jwt() {
let sd_jwt = enveloped.to_sd_jwt()?;
let credential =
SdJwtCredentialV2::from_sd_jwt_with(sd_jwt, &options.credentials.additional_typ)
.map_err(crate::sd_jwt_vc::SdJwtValidationError::from)?;
let sd_jwt_options = SdJwtValidationOptions {
time: options.credentials.time,
key_binding: options.key_binding.clone(),
status: options.credentials.status,
..SdJwtValidationOptions::default()
};
let validated = SdJwtCredentialValidator::new(
self.credentials.resolver(),
self.credentials.verifier(),
)
.validate_credential_v2_unchecked_status(credential, &sd_jwt_options)
.await?;
let credential = validated.credential;
self.credentials
.check_status(
credential
.credential()
.statuses()
.map(CredentialStatus::Vcdm)
.chain(credential.status().map(CredentialStatus::SdJwt)),
options.credentials.status,
options.credentials.time.now(),
)
.await?;
check_subject_holder(
&credential.credential().properties,
credential.credential().credential_subject.iter(),
holder,
options.subject_holder,
)?;
return Ok(PresentedCredentialV2::SdJwt(Box::new(credential)));
}
let jwt = enveloped.to_jwt()?;
let validated = self
.credentials
.validate_v2(&jwt, &options.credentials)
.await?;
check_subject_holder(
&validated.credential.properties,
validated.credential.credential_subject.iter(),
holder,
options.subject_holder,
)?;
Ok(PresentedCredentialV2::Jwt(Box::new(validated)))
}
async fn validate_presentation(
&self,
jwt: &Jwt,
options: &PresentationValidationOptions,
) -> Result<DecodedPresentation, ValidationError> {
let jws = jwt.decode()?;
check_typ(jws.header(), "JWT", &options.additional_typ)?;
let claims = plain_claims(&jws)?;
let presentation = Presentation::from_jwt_claims(&claims)?;
self.check_holder_and_claims(&jws, presentation.holder.as_ref(), "iss", &claims, options)
.await?;
Ok(DecodedPresentation {
header: jws.header().clone(),
claims,
presentation,
credentials: Vec::new(),
})
}
async fn validate_presentation_v2(
&self,
jwt: &Jwt,
options: &PresentationValidationOptions,
) -> Result<DecodedPresentationV2, ValidationError> {
let jws = jwt.decode()?;
let (claims, presentation) = decode_payload_v2(&jws, &options.additional_typ)?;
self.check_holder_and_claims(
&jws,
presentation.holder.as_ref(),
"holder",
&claims,
options,
)
.await?;
Ok(DecodedPresentationV2 {
header: jws.header().clone(),
claims,
presentation,
credentials: Vec::new(),
})
}
async fn check_holder_and_claims(
&self,
jws: &CompactJws,
holder: Option<&Party>,
missing: &'static str,
claims: &JwtClaims,
options: &PresentationValidationOptions,
) -> Result<(), ValidationError> {
let (reference, key) = self
.credentials
.verify(
jws,
KeyRole::Subject,
holder.map(|holder| holder.id().as_str()),
)
.await?;
let holder = holder.ok_or(CredentialError::MissingClaim(missing))?;
check_controller(holder, &reference, &key, "holder")?;
options.time.check_claims(claims)?;
if let Some(expected) = &options.audience
&& !claims.audiences().any(|aud| aud == expected)
{
return Err(ValidationError::AudienceMismatch {
expected: expected.clone(),
found: claims.aud.clone(),
});
}
if let Some(expected) = &options.nonce {
let found = claims.additional.get("nonce").and_then(Value::as_str);
if found != Some(expected.as_str()) {
return Err(ValidationError::NonceMismatch {
expected: expected.clone(),
found: found.map(str::to_owned),
});
}
}
Ok(())
}
}
impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtPresentationValidator<R, V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("JwtPresentationValidator")
.field("credentials", &self.credentials)
.finish()
}
}
pub fn decode_presentation(jwt: &Jwt) -> Result<DecodedPresentation, ValidationError> {
decode_presentation_with(jwt, &NO_TYP, &NO_TYP)
}
pub(crate) fn decode_presentation_with(
jwt: &Jwt,
presentation_typ: &BTreeSet<String>,
credential_typ: &BTreeSet<String>,
) -> Result<DecodedPresentation, ValidationError> {
let jws = jwt.decode()?;
check_typ(jws.header(), "JWT", presentation_typ)?;
let claims = plain_claims(&jws)?;
let presentation = Presentation::from_jwt_claims(&claims)?;
let credentials = presentation
.verifiable_credential
.iter()
.map(|credential| decode(&credential.decode()?, credential_typ))
.collect::<Result<_, _>>()?;
Ok(DecodedPresentation {
header: jws.header().clone(),
claims,
presentation,
credentials,
})
}
pub fn decode_presentation_v2(jwt: &Jwt) -> Result<DecodedPresentationV2, ValidationError> {
let jws = jwt.decode()?;
let (claims, presentation) = decode_payload_v2(&jws, &NO_TYP)?;
let credentials = presentation
.verifiable_credential
.iter()
.map(decode_enveloped)
.collect::<Result<_, _>>()?;
Ok(DecodedPresentationV2 {
header: jws.header().clone(),
claims,
presentation,
credentials,
})
}
fn decode_enveloped(enveloped: &EnvelopedVc) -> Result<PresentedCredentialV2, ValidationError> {
#[cfg(feature = "sd-jwt")]
if enveloped.is_sd_jwt() {
let credential = SdJwtCredentialV2::from_sd_jwt(enveloped.to_sd_jwt()?)
.map_err(crate::sd_jwt_vc::SdJwtValidationError::from)?;
return Ok(PresentedCredentialV2::SdJwt(Box::new(credential)));
}
Ok(PresentedCredentialV2::Jwt(Box::new(decode_v2(
&enveloped.to_jwt()?.decode()?,
&NO_TYP,
)?)))
}
fn decode_payload_v2(
jws: &CompactJws,
additional_typ: &BTreeSet<String>,
) -> Result<(JwtClaims, PresentationV2), ValidationError> {
check_typ(jws.header(), PresentationV2::JWT_TYP, additional_typ)?;
let claims = plain_claims(jws)?;
let presentation = PresentationV2::from_jwt_claims(&claims)?;
Ok((claims, presentation))
}
fn check_subject_holder<'a>(
properties: &IndexMap<String, Value>,
subjects: impl Iterator<Item = &'a Subject>,
holder: Option<&Uri>,
relationship: SubjectHolderRelationship,
) -> Result<(), ValidationError> {
let required = match relationship {
SubjectHolderRelationship::AlwaysSubject => true,
SubjectHolderRelationship::SubjectOnNonTransferable => properties
.get("nonTransferable")
.is_some_and(|value| value == &Value::Bool(true)),
SubjectHolderRelationship::Any => false,
};
if !required {
return Ok(());
}
let holder = holder.expect("a validated presentation has a holder");
for subject in subjects {
if subject.id.as_ref() != Some(holder) {
return Err(ValidationError::SubjectHolderMismatch {
subject: subject.id.clone(),
holder: holder.clone(),
});
}
}
Ok(())
}