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")]
pub use status_list::StatusListCheck;
#[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::SdJwt), 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::SdJwt)),
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(|e| ValidationError::Payload(e.to_string()))?;
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<'a>(
&self,
statuses: impl IntoIterator<Item = CredentialStatus<'a>>,
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()
}
}
#[cfg(feature = "did-web")]
mod status_list {
use chrono::{DateTime, Utc};
use url::Url;
use crate::jose::{DefaultVerifier, JwsVerifier};
use crate::methods::web::HttpClient;
use crate::resolver::{KeyReference, KeyResolver, KeyRole};
use crate::sd_jwt_vc::{StatusListToken, StatusMechanism};
use crate::vc::Jwt;
use crate::vc::validation::{CredentialStatus, StatusCheck, StatusError};
pub struct StatusListCheck<C, R, V = DefaultVerifier> {
client: C,
resolver: R,
verifier: V,
}
impl<C: HttpClient, R: KeyResolver, V: JwsVerifier> StatusListCheck<C, R, V> {
pub fn new(client: C, resolver: R, verifier: V) -> Self {
Self {
client,
resolver,
verifier,
}
}
async fn fetch(&self, uri: &str) -> Result<StatusListToken, StatusError> {
let url = Url::parse(uri)
.map_err(|e| StatusError::Unavailable(format!("`uri` {uri:?}: {e}")))?;
let response = self
.client
.get(&url)
.await
.map_err(|e| StatusError::Unavailable(format!("fetching {uri}: {e}")))?;
if !(200..300).contains(&response.status) {
return Err(StatusError::Unavailable(format!(
"fetching {uri}: HTTP {}",
response.status
)));
}
let body = String::from_utf8(response.body)
.map_err(|_| StatusError::Unavailable(format!("{uri}: body is not UTF-8")))?;
let jwt = Jwt::new(body.trim())
.map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
let (_, token) = StatusListToken::decode(&jwt)
.map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
let jws = jwt
.decode()
.map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
let iss = token.additional.get("iss").and_then(|iss| iss.as_str());
let reference = KeyReference::from_jws(&jws, iss)
.map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
let key = self
.resolver
.resolve_key(&reference, KeyRole::Issuer)
.await
.map_err(|e| {
StatusError::Unavailable(format!("{uri}: resolving key {reference}: {e}"))
})?;
jws.verify(&self.verifier, &key)
.map_err(|e| StatusError::Unavailable(format!("{uri}: signature: {e}")))?;
let bound = match iss {
Some(iss) => reference.binds_to(Some(iss), &key),
None => matches!(reference, KeyReference::DidUrl(_)),
};
if !bound {
return Err(StatusError::Unavailable(format!(
"{uri}: key {reference} does not belong to the issuer {iss:?}"
)));
}
Ok(token)
}
}
#[async_trait::async_trait]
impl<C: HttpClient, R: KeyResolver, V: JwsVerifier> StatusCheck for StatusListCheck<C, R, V> {
async fn check(
&self,
status: CredentialStatus<'_>,
now: DateTime<Utc>,
) -> Result<(), StatusError> {
let CredentialStatus::SdJwt(status) = status else {
return Err(StatusError::Unsupported(status.mechanism()));
};
let Some(reference) = &status.status_list else {
let names: Vec<&str> = status
.mechanisms()
.filter_map(|m| match m {
StatusMechanism::Other { name, .. } => Some(name),
_ => None,
})
.collect();
return Err(StatusError::Unsupported(names.join(", ")));
};
let token = self.fetch(&reference.uri).await?;
let status_type = token
.status_of(reference, now)
.map_err(|e| StatusError::Unavailable(e.to_string()))?;
if status_type.is_valid() {
Ok(())
} else {
Err(StatusError::Invalid(status_type.to_string()))
}
}
}
impl<C: core::fmt::Debug, R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug
for StatusListCheck<C, R, V>
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("StatusListCheck")
.field("client", &self.client)
.field("resolver", &self.resolver)
.field("verifier", &self.verifier)
.finish()
}
}
}