use crate::did::{Did, DidUrl, Uri, VerificationRelationship};
use crate::jose::{CompactJws, DefaultVerifier, JwsHeader, JwsVerifier};
use crate::vc::{Credential, CredentialV2, Jwt, JwtClaims, Party, TypedObject};
use super::{
CredentialValidationOptions, KeyResolver, StatusCheck, StatusError, StatusPolicy,
ValidationError,
};
#[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)?;
let kid = self
.verify(&jws, VerificationRelationship::AssertionMethod)
.await?;
check_controller(&decoded.credential.issuer, &kid, "issuer")?;
options.time.check_claims(&decoded.claims)?;
options.time.check(
Some(decoded.credential.issuance_date),
decoded.credential.expiration_date,
None,
)?;
self.check_status(decoded.credential.credential_status.iter(), options.status)
.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)?;
let kid = self
.verify(&jws, VerificationRelationship::AssertionMethod)
.await?;
check_controller(&decoded.credential.issuer, &kid, "issuer")?;
options.time.check_claims(&decoded.claims)?;
options.time.check(
decoded.credential.valid_from,
decoded.credential.valid_until,
None,
)?;
self.check_status(decoded.credential.statuses(), options.status)
.await?;
Ok(decoded)
}
pub(super) async fn verify(
&self,
jws: &CompactJws,
relationship: VerificationRelationship,
) -> Result<DidUrl, ValidationError> {
let kid = jws.kid().ok_or(ValidationError::MissingKid)?;
let kid = DidUrl::parse(kid).map_err(|source| ValidationError::InvalidKid {
kid: kid.to_owned(),
source,
})?;
if kid.path().is_some() || kid.query().is_some() {
return Err(ValidationError::KidWithParameters(kid));
}
let key = self
.resolver
.resolve_key(&kid, relationship)
.await
.map_err(|source| ValidationError::KeyResolution {
kid: kid.clone(),
source,
})?;
jws.verify(&self.verifier, &key)?;
Ok(kid)
}
async fn check_status<'a>(
&self,
statuses: impl Iterator<Item = &'a TypedObject>,
policy: StatusPolicy,
) -> Result<(), ValidationError> {
if policy == StatusPolicy::SkipAll {
return Ok(());
}
for status in statuses {
let result = match &self.status {
Some(check) => check.check(status).await,
None => Err(StatusError::Unsupported(status.object_type().to_owned())),
};
match result {
Ok(()) => {}
Err(StatusError::Unsupported(_)) if policy == StatusPolicy::SkipUnsupported => {}
Err(StatusError::Unsupported(status_type)) => {
return Err(ValidationError::StatusUnchecked(status_type));
}
Err(error) => return Err(ValidationError::Status(error)),
}
}
Ok(())
}
}
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()?)
}
pub fn decode_credential_v2(jwt: &Jwt) -> Result<DecodedCredentialV2, ValidationError> {
decode_v2(&jwt.decode()?)
}
pub(super) fn decode(jws: &CompactJws) -> Result<DecodedCredential, ValidationError> {
check_typ(jws.header(), "JWT")?;
let claims: JwtClaims = serde_json::from_slice(jws.payload())
.map_err(|e| ValidationError::Payload(e.to_string()))?;
let credential = Credential::from_jwt_claims(&claims)?;
Ok(DecodedCredential {
header: jws.header().clone(),
claims,
credential,
})
}
fn decode_v2(jws: &CompactJws) -> Result<DecodedCredentialV2, ValidationError> {
check_typ(jws.header(), CredentialV2::JWT_TYP)?;
let claims: JwtClaims = serde_json::from_slice(jws.payload())
.map_err(|e| ValidationError::Payload(e.to_string()))?;
let credential = CredentialV2::from_jwt_claims(&claims)?;
Ok(DecodedCredentialV2 {
header: jws.header().clone(),
claims,
credential,
})
}
pub(super) fn check_typ(header: &JwsHeader, expected: &'static str) -> Result<(), ValidationError> {
match header.typ.as_deref() {
Some(found) if found != expected => Err(ValidationError::UnexpectedTyp {
expected,
found: found.to_owned(),
}),
_ => Ok(()),
}
}
pub(super) fn check_controller(
party: &Party,
kid: &DidUrl,
role: &'static str,
) -> Result<(), ValidationError> {
let id = party.id();
let did = Did::parse(id.as_str()).map_err(|_| ValidationError::NotDid {
role,
id: id.to_string(),
})?;
if did != *kid.did() {
return Err(mismatch(role, id.clone(), kid.clone()));
}
Ok(())
}
fn mismatch(role: &'static str, id: Uri, kid: DidUrl) -> ValidationError {
if role == "holder" {
ValidationError::HolderMismatch { holder: id, kid }
} else {
ValidationError::IssuerMismatch { issuer: id, kid }
}
}