use std::collections::BTreeSet;
use crate::did::Uri;
use crate::jose::{CompactJws, DefaultVerifier, Jwk, JwsHeader, JwsVerifier};
use crate::vc::{Credential, CredentialV2, Jwt, JwtClaims, Party};
use chrono::{DateTime, Utc};
use serde_json::Value;
use super::{
CredentialStatus, CredentialValidationOptions, KeyReference, KeyResolver, KeyRole, StatusCheck,
StatusPolicy, ValidationError, check_statuses, status_claim, status_claim_entry,
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DecodedCredential {
pub header: JwsHeader,
pub claims: JwtClaims,
pub credential: Credential,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DecodedCredentialV2 {
pub header: JwsHeader,
pub claims: JwtClaims,
pub credential: CredentialV2,
}
pub struct JwtCredentialValidator<R, V = DefaultVerifier> {
resolver: R,
verifier: V,
status: Option<Box<dyn StatusCheck>>,
}
impl<R: KeyResolver, V: JwsVerifier> JwtCredentialValidator<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,
jwt: &Jwt,
options: &CredentialValidationOptions,
) -> Result<DecodedCredential, ValidationError> {
let jws = jwt.decode()?;
let decoded = decode(&jws, &options.additional_typ)?;
let issuer = &decoded.credential.issuer;
let (reference, key) = self
.verify(&jws, KeyRole::Issuer, Some(issuer.id().as_str()))
.await?;
check_controller(issuer, &reference, &key, "issuer")?;
options.time.check_claims(&decoded.claims)?;
options.time.check(
Some(decoded.credential.issuance_date),
decoded.credential.expiration_date,
None,
)?;
let status = status_claim(&decoded.claims, options.status)?;
self.check_status(
decoded
.credential
.credential_status
.iter()
.map(CredentialStatus::Vcdm)
.chain(status.iter().map(status_claim_entry)),
options.status,
options.time.now(),
)
.await?;
Ok(decoded)
}
pub async fn validate_v2(
&self,
jwt: &Jwt,
options: &CredentialValidationOptions,
) -> Result<DecodedCredentialV2, ValidationError> {
let jws = jwt.decode()?;
let decoded = decode_v2(&jws, &options.additional_typ)?;
let issuer = &decoded.credential.issuer;
let (reference, key) = self
.verify(&jws, KeyRole::Issuer, Some(issuer.id().as_str()))
.await?;
check_controller(issuer, &reference, &key, "issuer")?;
options.time.check_claims(&decoded.claims)?;
options.time.check(
decoded.credential.valid_from,
decoded.credential.valid_until,
None,
)?;
let status = status_claim(&decoded.claims, options.status)?;
self.check_status(
decoded
.credential
.statuses()
.map(CredentialStatus::Vcdm)
.chain(status.iter().map(status_claim_entry)),
options.status,
options.time.now(),
)
.await?;
Ok(decoded)
}
pub(super) async fn verify(
&self,
jws: &CompactJws,
role: KeyRole,
iss: Option<&str>,
) -> Result<(KeyReference, Jwk), ValidationError> {
let reference = KeyReference::from_jws(jws, iss)?;
let key = self
.resolver
.resolve_key(&reference, role)
.await
.map_err(|source| ValidationError::KeyResolution {
key: Box::new(reference.clone()),
source,
})?;
jws.verify(&self.verifier, &key)?;
Ok((reference, key))
}
pub(super) async fn check_status<'a>(
&self,
statuses: impl IntoIterator<Item = CredentialStatus<'a>>,
policy: StatusPolicy,
now: DateTime<Utc>,
) -> Result<(), ValidationError> {
check_statuses(self.status.as_deref(), statuses, policy, now).await
}
}
impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtCredentialValidator<R, V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("JwtCredentialValidator")
.field("resolver", &self.resolver)
.field("verifier", &self.verifier)
.field("status", &self.status.as_ref().map(|_| "StatusCheck"))
.finish()
}
}
pub fn decode_credential(jwt: &Jwt) -> Result<DecodedCredential, ValidationError> {
decode(&jwt.decode()?, &NO_TYP)
}
pub fn decode_credential_v2(jwt: &Jwt) -> Result<DecodedCredentialV2, ValidationError> {
decode_v2(&jwt.decode()?, &NO_TYP)
}
pub(super) static NO_TYP: BTreeSet<String> = BTreeSet::new();
pub(super) fn decode(
jws: &CompactJws,
additional_typ: &BTreeSet<String>,
) -> Result<DecodedCredential, ValidationError> {
check_typ(jws.header(), "JWT", additional_typ)?;
let claims = plain_claims(jws)?;
let credential = Credential::from_jwt_claims(&claims)?;
Ok(DecodedCredential {
header: jws.header().clone(),
claims,
credential,
})
}
pub(super) fn decode_v2(
jws: &CompactJws,
additional_typ: &BTreeSet<String>,
) -> Result<DecodedCredentialV2, ValidationError> {
check_typ(jws.header(), CredentialV2::JWT_TYP, additional_typ)?;
let claims = plain_claims(jws)?;
let credential = CredentialV2::from_jwt_claims(&claims)?;
Ok(DecodedCredentialV2 {
header: jws.header().clone(),
claims,
credential,
})
}
pub(super) fn plain_claims(jws: &CompactJws) -> Result<JwtClaims, ValidationError> {
let claims: JwtClaims = serde_json::from_slice(jws.payload())
.map_err(|e| ValidationError::Payload(e.to_string()))?;
match claims
.additional
.iter()
.find_map(|(name, value)| sd_jwt_member(name, value))
{
Some(claim) => Err(ValidationError::SdJwtPayload { claim }),
None => Ok(claims),
}
}
const SD_JWT_NAMES: [&str; 2] = ["_sd", "_sd_alg"];
const ELLIPSIS: &str = "...";
fn sd_jwt_member(name: &str, value: &Value) -> Option<&'static str> {
SD_JWT_NAMES
.into_iter()
.find(|reserved| *reserved == name)
.or_else(|| sd_jwt_name_in(value))
}
fn sd_jwt_name_in(value: &Value) -> Option<&'static str> {
match value {
Value::Object(object) => object
.iter()
.find_map(|(name, value)| sd_jwt_member(name, value)),
Value::Array(array) => array.iter().find_map(|element| match element {
Value::Object(object)
if object.len() == 1 && object.get(ELLIPSIS).is_some_and(Value::is_string) =>
{
Some(ELLIPSIS)
}
element => sd_jwt_name_in(element),
}),
_ => None,
}
}
pub(super) fn check_typ(
header: &JwsHeader,
expected: &'static str,
additional: &BTreeSet<String>,
) -> Result<(), ValidationError> {
match header.typ.as_deref() {
Some(found) if found != expected && !additional.contains(found) => {
Err(ValidationError::UnexpectedTyp {
expected,
found: found.to_owned(),
})
}
_ => Ok(()),
}
}
pub(super) fn check_controller(
party: &Party,
reference: &KeyReference,
key: &Jwk,
role: &'static str,
) -> Result<(), ValidationError> {
let id = party.id();
if !reference.binds_to(Some(id.as_str()), key) {
return Err(mismatch(role, id.clone(), reference.clone()));
}
Ok(())
}
fn mismatch(role: &'static str, id: Uri, key: KeyReference) -> ValidationError {
if role == "holder" {
ValidationError::HolderMismatch {
holder: id,
key: Box::new(key),
}
} else {
ValidationError::IssuerMismatch {
issuer: id,
key: Box::new(key),
}
}
}