use core::time::Duration;
use std::collections::BTreeSet;
use chrono::{DateTime, Utc};
use super::{
Confirmation, SdJwt, SdJwtCredentialV2, SdJwtCredentialV2Error, SdJwtVc, SdJwtVcError,
};
use crate::did::Uri;
use crate::jose::{DefaultVerifier, Jwk, JwsVerifier};
use crate::resolver::{KeyReference, KeyReferenceError, KeyResolver, KeyRole};
use crate::vc::validation::{
CredentialStatus, StatusCheck, StatusPolicy, TimeOptions, ValidationError, check_statuses,
};
use crate::vc::{CredentialError, JwtClaims};
#[cfg(feature = "did-web")]
#[cfg_attr(docsrs, doc(cfg(feature = "did-web")))]
#[deprecated(since = "0.3.0", note = "moved to `status_list::StatusListCheck`")]
pub type StatusListCheck<C, R, V = DefaultVerifier> = crate::status_list::StatusListCheck<C, R, V>;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct KeyBindingOptions {
pub audience: String,
pub nonce: String,
pub time: TimeOptions,
pub max_age: Option<Duration>,
}
impl KeyBindingOptions {
pub fn new(audience: impl Into<String>, nonce: impl Into<String>) -> Self {
Self {
audience: audience.into(),
nonce: nonce.into(),
time: TimeOptions::default(),
max_age: None,
}
}
#[must_use]
pub fn with_time(mut self, time: TimeOptions) -> Self {
self.time = time;
self
}
#[must_use]
pub fn with_max_age(mut self, max_age: Duration) -> Self {
self.max_age = Some(max_age);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct SdJwtValidationOptions {
pub time: TimeOptions,
pub key_binding: Option<KeyBindingOptions>,
pub status: StatusPolicy,
pub reject_legacy_typ: bool,
pub additional_typ: BTreeSet<String>,
}
impl SdJwtValidationOptions {
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_key_binding(mut self, key_binding: KeyBindingOptions) -> Self {
self.key_binding = Some(key_binding);
self
}
#[must_use]
pub fn with_status(mut self, status: StatusPolicy) -> Self {
self.status = status;
self
}
#[must_use]
pub fn rejecting_legacy_typ(mut self) -> Self {
self.reject_legacy_typ = true;
self
}
#[must_use]
pub fn with_additional_typ(mut self, typ: impl Into<String>) -> Self {
self.additional_typ.insert(typ.into());
self
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SdJwtValidationError {
#[error(transparent)]
Credential(#[from] SdJwtVcError),
#[error(transparent)]
CredentialV2(#[from] SdJwtCredentialV2Error),
#[error("header `typ` is the pre-standard `vc+sd-jwt`")]
LegacyTyp,
#[error(transparent)]
Validation(#[from] ValidationError),
#[error("key binding is required, but the SD-JWT has no key binding JWT")]
KeyBindingRequired,
#[error("key binding is required, but `cnf` carries no usable key reference: {0}")]
NoHolderKey(KeyReferenceError),
#[error("key binding JWT issued at {} is older than {max_age:?} (now is {})", issued_at.to_rfc3339(), now.to_rfc3339())]
KeyBindingTooOld {
issued_at: DateTime<Utc>,
now: DateTime<Utc>,
max_age: Duration,
},
#[error("key binding `sd_hash` {found:?} is not the digest {expected:?} of the SD-JWT")]
SdHashMismatch {
expected: String,
found: Option<String>,
},
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ValidatedSdJwtVc {
pub credential: SdJwtVc,
pub issuer_key: KeyReference,
pub key_binding: Option<JwtClaims>,
pub holder_key: Option<(KeyReference, Jwk)>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ValidatedSdJwtCredentialV2 {
pub credential: SdJwtCredentialV2,
pub issuer_key: KeyReference,
pub key_binding: Option<JwtClaims>,
pub holder_key: Option<(KeyReference, Jwk)>,
}
pub struct SdJwtCredentialValidator<R, V = DefaultVerifier> {
resolver: R,
verifier: V,
status: Option<Box<dyn StatusCheck>>,
}
impl<R: KeyResolver, V: JwsVerifier> SdJwtCredentialValidator<R, V> {
pub fn new(resolver: R, verifier: V) -> Self {
Self {
resolver,
verifier,
status: None,
}
}
#[must_use]
pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
self.status = Some(Box::new(check));
self
}
pub fn resolver(&self) -> &R {
&self.resolver
}
pub fn verifier(&self) -> &V {
&self.verifier
}
pub fn has_status_check(&self) -> bool {
self.status.is_some()
}
pub async fn validate(
&self,
sd_jwt: &SdJwt,
options: &SdJwtValidationOptions,
) -> Result<ValidatedSdJwtVc, SdJwtValidationError> {
let credential = SdJwtVc::from_sd_jwt(sd_jwt.clone())?;
self.validate_credential(credential, options).await
}
pub async fn validate_credential(
&self,
credential: SdJwtVc,
options: &SdJwtValidationOptions,
) -> Result<ValidatedSdJwtVc, SdJwtValidationError> {
if options.reject_legacy_typ && credential.has_legacy_typ() {
return Err(SdJwtValidationError::LegacyTyp);
}
let claims = credential.claims();
let issuer = claims
.iss
.as_deref()
.ok_or(CredentialError::MissingClaim("iss"))
.map_err(ValidationError::from)?;
let (issuer_key, key) = self
.verify_issuer_signature(credential.sd_jwt(), issuer)
.await?;
check_issuer(issuer, &issuer_key, &key)?;
let mut time_claims = JwtClaims::new();
time_claims.nbf = claims.nbf;
time_claims.exp = claims.exp;
time_claims.iat = claims.iat;
options.time.check_claims(&time_claims)?;
let (key_binding, holder_key) = match &options.key_binding {
Some(required) => {
let (claims, holder_key) = self
.check_key_binding(credential.sd_jwt(), credential.confirmation(), required)
.await?;
(Some(claims), Some(holder_key))
}
None => (None, None),
};
self.check_status(
claims
.status
.iter()
.map(CredentialStatus::StatusClaim)
.collect(),
options,
)
.await?;
Ok(ValidatedSdJwtVc {
credential,
issuer_key,
key_binding,
holder_key,
})
}
pub async fn validate_v2(
&self,
sd_jwt: &SdJwt,
options: &SdJwtValidationOptions,
) -> Result<ValidatedSdJwtCredentialV2, SdJwtValidationError> {
let credential =
SdJwtCredentialV2::from_sd_jwt_with(sd_jwt.clone(), &options.additional_typ)?;
self.validate_credential_v2(credential, options).await
}
pub async fn validate_credential_v2(
&self,
credential: SdJwtCredentialV2,
options: &SdJwtValidationOptions,
) -> Result<ValidatedSdJwtCredentialV2, SdJwtValidationError> {
let validated = self
.validate_credential_v2_unchecked_status(credential, options)
.await?;
let credential = &validated.credential;
self.check_status(
credential
.credential()
.statuses()
.map(CredentialStatus::Vcdm)
.chain(credential.status().map(CredentialStatus::StatusClaim))
.collect(),
options,
)
.await?;
Ok(validated)
}
pub(crate) async fn validate_credential_v2_unchecked_status(
&self,
credential: SdJwtCredentialV2,
options: &SdJwtValidationOptions,
) -> Result<ValidatedSdJwtCredentialV2, SdJwtValidationError> {
let vc = credential.credential();
let issuer = vc.issuer.id().as_str();
let (issuer_key, key) = self
.verify_issuer_signature(credential.sd_jwt(), issuer)
.await?;
check_issuer(issuer, &issuer_key, &key)?;
options.time.check_claims(credential.claims())?;
options.time.check(vc.valid_from, vc.valid_until, None)?;
let (key_binding, holder_key) = match &options.key_binding {
Some(required) => {
let (claims, holder_key) = self
.check_key_binding(credential.sd_jwt(), credential.confirmation(), required)
.await?;
(Some(claims), Some(holder_key))
}
None => (None, None),
};
Ok(ValidatedSdJwtCredentialV2 {
credential,
issuer_key,
key_binding,
holder_key,
})
}
async fn verify_issuer_signature(
&self,
sd_jwt: &SdJwt,
issuer: &str,
) -> Result<(KeyReference, Jwk), ValidationError> {
let jws = sd_jwt.jwt().decode()?;
let reference = KeyReference::from_jws(&jws, Some(issuer))?;
let key = self
.resolver
.resolve_key(&reference, KeyRole::Issuer)
.await
.map_err(|source| ValidationError::KeyResolution {
key: Box::new(reference.clone()),
source,
})?;
jws.verify(&self.verifier, &key)?;
Ok((reference, key))
}
async fn holder_key(
&self,
cnf: Option<&Confirmation>,
) -> Result<(KeyReference, Jwk), SdJwtValidationError> {
let cnf = cnf.ok_or(SdJwtValidationError::NoHolderKey(
KeyReferenceError::Missing,
))?;
let reference =
KeyReference::from_confirmation(cnf).map_err(SdJwtValidationError::NoHolderKey)?;
let key = match &reference {
KeyReference::Jwk(jwk) => jwk.public(),
reference => self
.resolver
.resolve_key(reference, KeyRole::Subject)
.await
.map_err(|source| ValidationError::KeyResolution {
key: Box::new(reference.clone()),
source,
})?,
};
Ok((reference, key))
}
async fn check_key_binding(
&self,
sd_jwt: &SdJwt,
cnf: Option<&Confirmation>,
required: &KeyBindingOptions,
) -> Result<(JwtClaims, (KeyReference, Jwk)), SdJwtValidationError> {
let kb_jwt = sd_jwt
.key_binding_jwt()
.ok_or(SdJwtValidationError::KeyBindingRequired)?;
let holder_key = self.holder_key(cnf).await?;
let jws = kb_jwt.decode().map_err(ValidationError::from)?;
match jws.header().typ.as_deref() {
Some(super::KB_JWT_TYP) => {}
found => {
return Err(ValidationError::UnexpectedTyp {
expected: super::KB_JWT_TYP,
found: found.unwrap_or("").to_owned(),
}
.into());
}
}
jws.verify(&self.verifier, &holder_key.1)
.map_err(ValidationError::from)?;
let claims: JwtClaims =
serde_json::from_slice(jws.payload()).map_err(ValidationError::Payload)?;
if !claims.audiences().any(|aud| aud == required.audience) {
return Err(ValidationError::AudienceMismatch {
expected: required.audience.clone(),
found: claims.aud.clone(),
}
.into());
}
let nonce = claims.additional.get("nonce").and_then(|v| v.as_str());
if nonce != Some(required.nonce.as_str()) {
return Err(ValidationError::NonceMismatch {
expected: required.nonce.clone(),
found: nonce.map(str::to_owned),
}
.into());
}
let iat = claims
.iat
.ok_or(CredentialError::MissingClaim("iat"))
.map_err(ValidationError::from)?;
let issued_at = JwtClaims::timestamp(iat).ok_or_else(|| {
ValidationError::from(CredentialError::InvalidClaim {
claim: "iat",
reason: format!("{iat} is out of range"),
})
})?;
required.time.check(None, None, Some(issued_at))?;
if let Some(max_age) = required.max_age {
let now = required.time.now();
let oldest = chrono::TimeDelta::from_std(max_age)
.ok()
.and_then(|delta| now.checked_sub_signed(delta))
.unwrap_or(DateTime::<Utc>::MIN_UTC);
if issued_at < oldest {
return Err(SdJwtValidationError::KeyBindingTooOld {
issued_at,
now,
max_age,
});
}
}
let alg = sd_jwt.hash_algorithm().map_err(SdJwtVcError::from)?;
let expected = sd_jwt.sd_hash(alg);
let found = claims.additional.get("sd_hash").and_then(|v| v.as_str());
if found != Some(expected.as_str()) {
return Err(SdJwtValidationError::SdHashMismatch {
expected,
found: found.map(str::to_owned),
});
}
Ok((claims, holder_key))
}
async fn check_status(
&self,
statuses: Vec<CredentialStatus<'_>>,
options: &SdJwtValidationOptions,
) -> Result<(), SdJwtValidationError> {
Ok(check_statuses(
self.status.as_deref(),
statuses,
options.status,
options.time.now(),
)
.await?)
}
}
fn check_issuer(issuer: &str, reference: &KeyReference, key: &Jwk) -> Result<(), ValidationError> {
if !reference.binds_to(Some(issuer), key) {
return Err(ValidationError::IssuerMismatch {
issuer: Uri::parse(issuer).map_err(|e| CredentialError::InvalidClaim {
claim: "iss",
reason: e.to_string(),
})?,
key: Box::new(reference.clone()),
});
}
Ok(())
}
impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for SdJwtCredentialValidator<R, V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SdJwtCredentialValidator")
.field("resolver", &self.resolver)
.field("verifier", &self.verifier)
.field("status", &self.status.as_ref().map(|_| "StatusCheck"))
.finish()
}
}