identity_credential/validator/jwt_credential_validation/
jwt_credential_validator.rsuse identity_core::convert::FromJson;
use identity_did::CoreDID;
use identity_did::DIDUrl;
use identity_document::document::CoreDocument;
use identity_document::verifiable::JwsVerificationOptions;
use identity_verification::jwk::Jwk;
use identity_verification::jws::DecodedJws;
use identity_verification::jws::Decoder;
use identity_verification::jws::JwsValidationItem;
use identity_verification::jws::JwsVerifier;
use super::CompoundCredentialValidationError;
use super::DecodedJwtCredential;
use super::JwtCredentialValidationOptions;
use super::JwtCredentialValidatorUtils;
use super::JwtValidationError;
use super::SignerContext;
use crate::credential::Credential;
use crate::credential::CredentialJwtClaims;
use crate::credential::Jwt;
use crate::validator::FailFast;
#[non_exhaustive]
pub struct JwtCredentialValidator<V: JwsVerifier>(V);
impl<V: JwsVerifier> JwtCredentialValidator<V> {
pub fn with_signature_verifier(signature_verifier: V) -> Self {
Self(signature_verifier)
}
pub fn validate<DOC, T>(
&self,
credential_jwt: &Jwt,
issuer: &DOC,
options: &JwtCredentialValidationOptions,
fail_fast: FailFast,
) -> Result<DecodedJwtCredential<T>, CompoundCredentialValidationError>
where
T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
DOC: AsRef<CoreDocument>,
{
let credential_token = self
.verify_signature(
credential_jwt,
std::slice::from_ref(issuer.as_ref()),
&options.verification_options,
)
.map_err(|err| CompoundCredentialValidationError {
validation_errors: [err].into(),
})?;
Self::validate_decoded_credential::<CoreDocument, T>(
credential_token,
std::slice::from_ref(issuer.as_ref()),
options,
fail_fast,
)
}
pub fn verify_signature<DOC, T>(
&self,
credential: &Jwt,
trusted_issuers: &[DOC],
options: &JwsVerificationOptions,
) -> Result<DecodedJwtCredential<T>, JwtValidationError>
where
T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
DOC: AsRef<CoreDocument>,
{
Self::verify_signature_with_verifier(&self.0, credential, trusted_issuers, options)
}
pub(crate) fn validate_decoded_credential<DOC, T>(
credential_token: DecodedJwtCredential<T>,
issuers: &[DOC],
options: &JwtCredentialValidationOptions,
fail_fast: FailFast,
) -> Result<DecodedJwtCredential<T>, CompoundCredentialValidationError>
where
T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
DOC: AsRef<CoreDocument>,
{
let credential: &Credential<T> = &credential_token.credential;
let expiry_date_validation = std::iter::once_with(|| {
JwtCredentialValidatorUtils::check_expires_on_or_after(
&credential_token.credential,
options.earliest_expiry_date.unwrap_or_default(),
)
});
let issuance_date_validation = std::iter::once_with(|| {
JwtCredentialValidatorUtils::check_issued_on_or_before(
credential,
options.latest_issuance_date.unwrap_or_default(),
)
});
let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential));
let subject_holder_validation = std::iter::once_with(|| {
options
.subject_holder_relationship
.as_ref()
.map(|(holder, relationship)| {
JwtCredentialValidatorUtils::check_subject_holder_relationship(credential, holder, *relationship)
})
.unwrap_or(Ok(()))
});
let validation_units_iter = issuance_date_validation
.chain(expiry_date_validation)
.chain(structure_validation)
.chain(subject_holder_validation);
#[cfg(feature = "revocation-bitmap")]
let validation_units_iter = {
let revocation_validation =
std::iter::once_with(|| JwtCredentialValidatorUtils::check_status(credential, issuers, options.status));
validation_units_iter.chain(revocation_validation)
};
let validation_units_error_iter = validation_units_iter.filter_map(|result| result.err());
let validation_errors: Vec<JwtValidationError> = match fail_fast {
FailFast::FirstError => validation_units_error_iter.take(1).collect(),
FailFast::AllErrors => validation_units_error_iter.collect(),
};
if validation_errors.is_empty() {
Ok(credential_token)
} else {
Err(CompoundCredentialValidationError { validation_errors })
}
}
pub(crate) fn parse_jwk<'a, 'i, DOC>(
jws: &JwsValidationItem<'a>,
trusted_issuers: &'i [DOC],
options: &JwsVerificationOptions,
) -> Result<(&'a Jwk, DIDUrl), JwtValidationError>
where
DOC: AsRef<CoreDocument>,
'i: 'a,
{
let nonce: Option<&str> = options.nonce.as_deref();
if jws.nonce() != nonce {
return Err(JwtValidationError::JwsDecodingError(
identity_verification::jose::error::Error::InvalidParam("invalid nonce value"),
));
}
let method_id: DIDUrl =
match &options.method_id {
Some(method_id) => method_id.clone(),
None => {
let kid: &str = jws.protected_header().and_then(|header| header.kid()).ok_or(
JwtValidationError::MethodDataLookupError {
source: None,
message: "could not extract kid from protected header",
signer_ctx: SignerContext::Issuer,
},
)?;
DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError {
source: Some(err.into()),
message: "could not parse kid as a DID Url",
signer_ctx: SignerContext::Issuer,
})?
}
};
let issuer: &CoreDocument = trusted_issuers
.iter()
.map(AsRef::as_ref)
.find(|issuer_doc| <CoreDocument>::id(issuer_doc) == method_id.did())
.ok_or(JwtValidationError::DocumentMismatch(SignerContext::Issuer))?;
issuer
.resolve_method(&method_id, options.method_scope)
.and_then(|method| method.data().public_key_jwk())
.ok_or_else(|| JwtValidationError::MethodDataLookupError {
source: None,
message: "could not extract JWK from a method identified by kid",
signer_ctx: SignerContext::Issuer,
})
.map(move |jwk| (jwk, method_id))
}
fn verify_signature_with_verifier<DOC, S, T>(
signature_verifier: &S,
credential: &Jwt,
trusted_issuers: &[DOC],
options: &JwsVerificationOptions,
) -> Result<DecodedJwtCredential<T>, JwtValidationError>
where
T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
DOC: AsRef<CoreDocument>,
S: JwsVerifier,
{
let decoded: JwsValidationItem<'_> = Self::decode(credential.as_str())?;
let (public_key, method_id) = Self::parse_jwk(&decoded, trusted_issuers, options)?;
let credential_token = Self::verify_decoded_signature(decoded, public_key, signature_verifier)?;
let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?;
if &issuer_id != method_id.did() {
return Err(JwtValidationError::IdentifierMismatch {
signer_ctx: SignerContext::Issuer,
});
};
Ok(credential_token)
}
pub(crate) fn decode(credential_jws: &str) -> Result<JwsValidationItem<'_>, JwtValidationError> {
let decoder: Decoder = Decoder::new();
decoder
.decode_compact_serialization(credential_jws.as_bytes(), None)
.map_err(JwtValidationError::JwsDecodingError)
}
pub(crate) fn verify_signature_raw<'a, S: JwsVerifier>(
decoded: JwsValidationItem<'a>,
public_key: &Jwk,
signature_verifier: &S,
) -> Result<DecodedJws<'a>, JwtValidationError> {
decoded
.verify(signature_verifier, public_key)
.map_err(|err| JwtValidationError::Signature {
source: err,
signer_ctx: SignerContext::Issuer,
})
}
fn verify_decoded_signature<S: JwsVerifier, T>(
decoded: JwsValidationItem<'_>,
public_key: &Jwk,
signature_verifier: &S,
) -> Result<DecodedJwtCredential<T>, JwtValidationError>
where
T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
{
let DecodedJws { protected, claims, .. } = Self::verify_signature_raw(decoded, public_key, signature_verifier)?;
let credential_claims: CredentialJwtClaims<'_, T> =
CredentialJwtClaims::from_json_slice(&claims).map_err(|err| {
JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
})?;
let custom_claims = credential_claims.custom.clone();
let credential: Credential<T> = credential_claims
.try_into_credential()
.map_err(JwtValidationError::CredentialStructure)?;
Ok(DecodedJwtCredential {
credential,
header: Box::new(protected),
custom_claims,
})
}
}
#[cfg(test)]
mod tests {
use crate::credential::Subject;
use crate::validator::SubjectHolderRelationship;
use identity_core::common::Duration;
use identity_core::common::Url;
use once_cell::sync::Lazy;
use super::*;
use identity_core::common::Object;
use identity_core::common::Timestamp;
use proptest::proptest;
const LAST_RFC3339_COMPATIBLE_UNIX_TIMESTAMP: i64 = 253402300799; const FIRST_RFC3999_COMPATIBLE_UNIX_TIMESTAMP: i64 = -62167219200; const SIMPLE_CREDENTIAL_JSON: &str = r#"{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/3732",
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "https://example.edu/issuers/14",
"issuanceDate": "2010-01-01T19:23:24Z",
"expirationDate": "2020-01-01T19:23:24Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}"#;
static SIMPLE_CREDENTIAL: Lazy<Credential> =
Lazy::new(|| Credential::<Object>::from_json(SIMPLE_CREDENTIAL_JSON).unwrap());
#[test]
fn issued_on_or_before() {
assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(
&SIMPLE_CREDENTIAL,
SIMPLE_CREDENTIAL
.issuance_date
.checked_sub(Duration::minutes(1))
.unwrap()
)
.is_err());
assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(
&SIMPLE_CREDENTIAL,
SIMPLE_CREDENTIAL
.issuance_date
.checked_add(Duration::minutes(1))
.unwrap()
)
.is_ok());
}
#[test]
fn check_subject_holder_relationship() {
let mut credential: Credential = SIMPLE_CREDENTIAL.clone();
let actual_holder_url = credential.credential_subject.first().unwrap().id.clone().unwrap();
assert_eq!(credential.credential_subject.len(), 1);
credential.non_transferable = Some(true);
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential,
&actual_holder_url,
SubjectHolderRelationship::AlwaysSubject
)
.is_ok());
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential,
&actual_holder_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_ok());
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential,
&actual_holder_url,
SubjectHolderRelationship::Any
)
.is_ok());
let issuer_url = Url::parse("did:core:0x1234567890").unwrap();
assert!(actual_holder_url != issuer_url);
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential,
&issuer_url,
SubjectHolderRelationship::AlwaysSubject
)
.is_err());
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential,
&issuer_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_err());
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential,
&issuer_url,
SubjectHolderRelationship::Any
)
.is_ok());
let mut credential_transferable = credential.clone();
credential_transferable.non_transferable = Some(false);
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential_transferable,
&issuer_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_ok());
credential_transferable.non_transferable = None;
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential_transferable,
&issuer_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_ok());
let mut credential_duplicated_holder = credential;
credential_duplicated_holder
.credential_subject
.push(Subject::with_id(actual_holder_url));
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential_duplicated_holder,
&issuer_url,
SubjectHolderRelationship::AlwaysSubject
)
.is_err());
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential_duplicated_holder,
&issuer_url,
SubjectHolderRelationship::SubjectOnNonTransferable
)
.is_err());
assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
&credential_duplicated_holder,
&issuer_url,
SubjectHolderRelationship::Any
)
.is_ok());
}
#[test]
fn simple_expires_on_or_after_with_expiration_date() {
let later_than_expiration_date = SIMPLE_CREDENTIAL
.expiration_date
.unwrap()
.checked_add(Duration::minutes(1))
.unwrap();
assert!(
JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, later_than_expiration_date).is_err()
);
let earlier_date = Timestamp::parse("2019-12-27T11:35:30Z").unwrap();
assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, earlier_date).is_ok());
}
proptest! {
#[test]
fn property_based_expires_after_with_expiration_date(seconds in 0..1_000_000_000_u32) {
let after_expiration_date = SIMPLE_CREDENTIAL.expiration_date.unwrap().checked_add(Duration::seconds(seconds)).unwrap();
let before_expiration_date = SIMPLE_CREDENTIAL.expiration_date.unwrap().checked_sub(Duration::seconds(seconds)).unwrap();
assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, after_expiration_date).is_err());
assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, before_expiration_date).is_ok());
}
}
proptest! {
#[test]
fn property_based_expires_after_no_expiration_date(seconds in FIRST_RFC3999_COMPATIBLE_UNIX_TIMESTAMP..LAST_RFC3339_COMPATIBLE_UNIX_TIMESTAMP) {
let mut credential = SIMPLE_CREDENTIAL.clone();
credential.expiration_date = None;
assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&credential, Timestamp::from_unix(seconds).unwrap()).is_ok());
}
}
proptest! {
#[test]
fn property_based_issued_before(seconds in 0 ..1_000_000_000_u32) {
let earlier_than_issuance_date = SIMPLE_CREDENTIAL.issuance_date.checked_sub(Duration::seconds(seconds)).unwrap();
let later_than_issuance_date = SIMPLE_CREDENTIAL.issuance_date.checked_add(Duration::seconds(seconds)).unwrap();
assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(&SIMPLE_CREDENTIAL, earlier_than_issuance_date).is_err());
assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(&SIMPLE_CREDENTIAL, later_than_issuance_date).is_ok());
}
}
}