use core::time::Duration;
use std::collections::BTreeSet;
use std::sync::Arc;
use chrono::{DateTime, TimeDelta, Utc};
use crate::did::Did;
pub use crate::resolver::{
KeyReference, KeyReferenceError, KeyResolutionError, KeyResolver, KeyRole,
};
use crate::vc::{Jwt, JwtClaims, TypedObject, claims};
mod credential;
mod error;
mod presentation;
pub use credential::{
DecodedCredential, DecodedCredentialV2, JwtCredentialValidator, decode_credential,
decode_credential_v2,
};
pub use error::{PresentationValidationError, StatusError, ValidationError};
#[cfg(feature = "linked-vp")]
pub(crate) use presentation::decode_presentation_with;
pub use presentation::{
DecodedPresentation, DecodedPresentationV2, JwtPresentationValidator, PresentedCredentialV2,
decode_presentation, decode_presentation_v2,
};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum CredentialStatus<'a> {
Vcdm(&'a TypedObject),
#[cfg(feature = "sd-jwt")]
#[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
SdJwt(&'a crate::sd_jwt_vc::Status),
}
impl CredentialStatus<'_> {
pub fn mechanism(&self) -> String {
match self {
Self::Vcdm(entry) => entry.object_type().to_owned(),
#[cfg(feature = "sd-jwt")]
Self::SdJwt(status) => status
.mechanisms()
.map(|mechanism| match mechanism {
crate::sd_jwt_vc::StatusMechanism::StatusList(_) => "status_list",
crate::sd_jwt_vc::StatusMechanism::Other { name, .. } => name,
})
.collect::<Vec<_>>()
.join(", "),
}
}
}
#[async_trait::async_trait]
pub trait StatusCheck: Send + Sync {
async fn check(
&self,
status: CredentialStatus<'_>,
now: DateTime<Utc>,
) -> Result<(), StatusError>;
}
macro_rules! forward_status_check {
($($ty:ty),* $(,)?) => {
$(
#[async_trait::async_trait]
impl<T: StatusCheck + ?Sized> StatusCheck for $ty {
async fn check(
&self,
status: CredentialStatus<'_>,
now: DateTime<Utc>,
) -> Result<(), StatusError> {
(**self).check(status, now).await
}
}
)*
};
}
forward_status_check!(&T, Box<T>, Arc<T>);
pub(crate) async fn check_statuses<'a>(
check: Option<&dyn StatusCheck>,
statuses: impl IntoIterator<Item = CredentialStatus<'a>>,
policy: StatusPolicy,
now: DateTime<Utc>,
) -> Result<(), ValidationError> {
if policy == StatusPolicy::SkipAll {
return Ok(());
}
for status in statuses {
let result = match check {
Some(check) => check.check(status, now).await,
None => Err(StatusError::Unsupported(status.mechanism())),
};
match result {
Ok(()) => {}
Err(StatusError::Unsupported(_)) if policy == StatusPolicy::SkipUnsupported => {}
Err(StatusError::Unsupported(mechanism)) => {
return Err(ValidationError::StatusUnchecked(mechanism));
}
Err(error) => return Err(ValidationError::Status(error)),
}
}
Ok(())
}
#[cfg(feature = "sd-jwt")]
pub(super) type StatusClaim = crate::sd_jwt_vc::Status;
#[cfg(not(feature = "sd-jwt"))]
pub(super) enum StatusClaim {}
pub(super) fn status_claim(
claims: &JwtClaims,
policy: StatusPolicy,
) -> Result<Option<StatusClaim>, ValidationError> {
let Some(status) = claims.additional.get("status") else {
return Ok(None);
};
#[cfg(feature = "sd-jwt")]
{
let _ = policy;
let invalid = |reason: String| crate::vc::CredentialError::InvalidClaim {
claim: "status",
reason,
};
let status: StatusClaim =
serde_json::from_value(status.clone()).map_err(|e| invalid(e.to_string()))?;
if status.is_empty() {
return Err(invalid("names no status mechanism".to_owned()).into());
}
Ok(Some(status))
}
#[cfg(not(feature = "sd-jwt"))]
{
let _ = status;
match policy {
StatusPolicy::Strict => Err(ValidationError::StatusUnchecked("status".to_owned())),
_ => Ok(None),
}
}
}
pub(super) fn status_claim_entry(status: &StatusClaim) -> CredentialStatus<'_> {
#[cfg(feature = "sd-jwt")]
{
CredentialStatus::SdJwt(status)
}
#[cfg(not(feature = "sd-jwt"))]
{
match *status {}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StatusPolicy {
#[default]
Strict,
SkipUnsupported,
SkipAll,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FailFast {
#[default]
FirstError,
AllErrors,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SubjectHolderRelationship {
#[default]
AlwaysSubject,
SubjectOnNonTransferable,
Any,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct TimeOptions {
pub now: Option<DateTime<Utc>>,
pub leeway: Duration,
}
impl TimeOptions {
pub fn at(now: DateTime<Utc>) -> Self {
Self {
now: Some(now),
leeway: Duration::ZERO,
}
}
#[must_use]
pub fn with_leeway(mut self, leeway: Duration) -> Self {
self.leeway = leeway;
self
}
pub fn now(&self) -> DateTime<Utc> {
self.now.unwrap_or_else(Utc::now)
}
fn delta(&self) -> TimeDelta {
TimeDelta::from_std(self.leeway).unwrap_or(TimeDelta::MAX)
}
pub fn check(
&self,
not_before: Option<DateTime<Utc>>,
expires: Option<DateTime<Utc>>,
issued_at: Option<DateTime<Utc>>,
) -> Result<(), ValidationError> {
let now = self.now();
let delta = self.delta();
let latest = now
.checked_add_signed(delta)
.unwrap_or(DateTime::<Utc>::MAX_UTC);
let earliest = now
.checked_sub_signed(delta)
.unwrap_or(DateTime::<Utc>::MIN_UTC);
if let Some(not_before) = not_before
&& not_before > latest
{
return Err(ValidationError::NotYetValid { not_before, now });
}
if let Some(expired_at) = expires
&& expired_at <= earliest
{
return Err(ValidationError::Expired { expired_at, now });
}
if let Some(issued_at) = issued_at
&& issued_at > latest
{
return Err(ValidationError::IssuedInFuture { issued_at, now });
}
Ok(())
}
pub fn check_claims(&self, claims: &JwtClaims) -> Result<(), ValidationError> {
self.check(
timestamp(claims.nbf, "nbf")?,
timestamp(claims.exp, "exp")?,
timestamp(claims.iat, "iat")?,
)
}
}
fn timestamp(
value: Option<i64>,
claim: &'static str,
) -> Result<Option<DateTime<Utc>>, ValidationError> {
value
.map(|value| {
JwtClaims::timestamp(value).ok_or_else(|| {
ValidationError::Credential(crate::vc::CredentialError::InvalidClaim {
claim,
reason: format!("{value} is out of range"),
})
})
})
.transpose()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct CredentialValidationOptions {
pub time: TimeOptions,
pub status: StatusPolicy,
pub additional_typ: BTreeSet<String>,
}
impl CredentialValidationOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_time(mut self, time: TimeOptions) -> Self {
self.time = time;
self
}
#[must_use]
pub fn with_status(mut self, status: StatusPolicy) -> Self {
self.status = status;
self
}
#[must_use]
pub fn with_additional_typ(mut self, typ: impl Into<String>) -> Self {
self.additional_typ.insert(typ.into());
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PresentationValidationOptions {
pub time: TimeOptions,
pub audience: Option<String>,
pub nonce: Option<String>,
pub subject_holder: SubjectHolderRelationship,
pub credentials: CredentialValidationOptions,
pub fail_fast: FailFast,
pub additional_typ: BTreeSet<String>,
#[cfg(feature = "sd-jwt")]
#[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
pub key_binding: Option<crate::sd_jwt_vc::KeyBindingOptions>,
}
impl PresentationValidationOptions {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_time(mut self, time: TimeOptions) -> Self {
self.time = time;
self
}
#[must_use]
pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
self.audience = Some(audience.into());
self
}
#[must_use]
pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
self.nonce = Some(nonce.into());
self
}
#[must_use]
pub fn with_subject_holder(mut self, relationship: SubjectHolderRelationship) -> Self {
self.subject_holder = relationship;
self
}
#[must_use]
pub fn with_credentials(mut self, credentials: CredentialValidationOptions) -> Self {
self.credentials = credentials;
self
}
#[must_use]
pub fn with_fail_fast(mut self, fail_fast: FailFast) -> Self {
self.fail_fast = fail_fast;
self
}
#[must_use]
pub fn with_additional_typ(mut self, typ: impl Into<String>) -> Self {
self.additional_typ.insert(typ.into());
self
}
#[cfg(feature = "sd-jwt")]
#[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
#[must_use]
pub fn with_key_binding(mut self, key_binding: crate::sd_jwt_vc::KeyBindingOptions) -> Self {
self.key_binding = Some(key_binding);
self
}
}
pub fn extract_issuer(jwt: &Jwt) -> Result<Did, ValidationError> {
let claims = decode_claims(jwt)?;
let issuer = claims
.iss
.clone()
.or_else(|| property_id(&claims, "vc", "issuer"))
.ok_or(crate::vc::CredentialError::MissingClaim("iss"))?;
Did::parse(&issuer).map_err(|_| ValidationError::NotDid {
role: "issuer",
id: issuer,
})
}
pub fn extract_holder(jwt: &Jwt) -> Result<Did, ValidationError> {
let claims = decode_claims(jwt)?;
let holder = claims
.iss
.clone()
.or_else(|| property_id(&claims, "vp", "holder"))
.ok_or(crate::vc::CredentialError::MissingClaim("iss"))?;
Did::parse(&holder).map_err(|_| ValidationError::NotDid {
role: "holder",
id: holder,
})
}
fn property_id(claims: &JwtClaims, container: &str, property: &str) -> Option<String> {
let value = match claims.additional.get(container) {
Some(container) => container.get(property)?,
None => claims.additional.get(property)?,
};
claims::id_of(value).map(str::to_owned)
}
pub(super) fn decode_claims(jwt: &Jwt) -> Result<JwtClaims, ValidationError> {
let jws = jwt.decode()?;
serde_json::from_slice(jws.payload()).map_err(|e| ValidationError::Payload(e.to_string()))
}