use core::time::Duration;
use std::sync::Arc;
use chrono::{DateTime, TimeDelta, Utc};
use crate::did::{Did, DidUrl, VerificationRelationship};
use crate::jose::Jwk;
use crate::resolver::Resolver;
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::{KeyResolutionError, PresentationValidationError, StatusError, ValidationError};
pub use presentation::{DecodedPresentation, JwtPresentationValidator, decode_presentation};
#[async_trait::async_trait]
pub trait KeyResolver: Send + Sync {
async fn resolve_key(
&self,
url: &DidUrl,
relationship: VerificationRelationship,
) -> Result<Jwk, KeyResolutionError>;
}
#[async_trait::async_trait]
impl KeyResolver for Resolver {
async fn resolve_key(
&self,
url: &DidUrl,
relationship: VerificationRelationship,
) -> Result<Jwk, KeyResolutionError> {
self.resolve_public_key(url, Some(relationship))
.await
.map_err(KeyResolutionError::from)
}
}
macro_rules! forward_key_resolver {
($($ty:ty),* $(,)?) => {$(
#[async_trait::async_trait]
impl<T: KeyResolver + ?Sized> KeyResolver for $ty {
async fn resolve_key(
&self,
url: &DidUrl,
relationship: VerificationRelationship,
) -> Result<Jwk, KeyResolutionError> {
(**self).resolve_key(url, relationship).await
}
}
)*};
}
forward_key_resolver!(&T, Box<T>, Arc<T>);
#[async_trait::async_trait]
pub trait StatusCheck: Send + Sync {
async fn check(&self, status: &TypedObject) -> Result<(), StatusError>;
}
#[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,
}
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
}
}
#[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,
}
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
}
}
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()))
}