identity_credential/validator/jwt_credential_validation/
jwt_credential_validator_utils.rs1use std::str::FromStr;
4
5use identity_core::common::Object;
6use identity_core::common::OneOrMany;
7use identity_core::common::Timestamp;
8use identity_core::common::Url;
9use identity_core::convert::FromJson;
10use identity_did::DID;
11use identity_verification::jws::Decoder;
12
13use super::JwtValidationError;
14use super::SignerContext;
15use crate::credential::Credential;
16use crate::credential::CredentialJwtClaims;
17use crate::credential::Jwt;
18#[cfg(feature = "status-list-2021")]
19use crate::revocation::status_list_2021::StatusList2021Credential;
20use crate::validator::SubjectHolderRelationship;
21
22#[derive(Debug)]
24#[non_exhaustive]
25pub struct JwtCredentialValidatorUtils;
26
27type ValidationUnitResult<T = ()> = std::result::Result<T, JwtValidationError>;
28
29impl JwtCredentialValidatorUtils {
30 pub fn check_structure<T>(credential: &Credential<T>) -> ValidationUnitResult {
35 credential
36 .check_structure()
37 .map_err(JwtValidationError::CredentialStructure)
38 }
39
40 pub fn check_expires_on_or_after<T>(credential: &Credential<T>, timestamp: Timestamp) -> ValidationUnitResult {
42 let expiration_date: Option<Timestamp> = credential.expiration_date;
43 (expiration_date.is_none() || expiration_date >= Some(timestamp))
44 .then_some(())
45 .ok_or(JwtValidationError::ExpirationDate)
46 }
47
48 pub fn check_issued_on_or_before<T>(credential: &Credential<T>, timestamp: Timestamp) -> ValidationUnitResult {
50 (credential.issuance_date <= timestamp)
51 .then_some(())
52 .ok_or(JwtValidationError::IssuanceDate)
53 }
54
55 pub fn check_subject_holder_relationship<T>(
58 credential: &Credential<T>,
59 holder: &Url,
60 relationship: SubjectHolderRelationship,
61 ) -> ValidationUnitResult {
62 let url_matches: bool = match &credential.credential_subject {
63 OneOrMany::One(ref credential_subject) => credential_subject.id.as_ref() == Some(holder),
64 OneOrMany::Many(subjects) => {
65 if let [credential_subject] = subjects.as_slice() {
67 credential_subject.id.as_ref() == Some(holder)
68 } else {
69 false
71 }
72 }
73 };
74
75 Some(relationship)
76 .filter(|relationship| match relationship {
77 SubjectHolderRelationship::AlwaysSubject => url_matches,
78 SubjectHolderRelationship::SubjectOnNonTransferable => {
79 url_matches || !credential.non_transferable.unwrap_or(false)
80 }
81 SubjectHolderRelationship::Any => true,
82 })
83 .map(|_| ())
84 .ok_or(JwtValidationError::SubjectHolderRelationship)
85 }
86
87 #[cfg(feature = "status-list-2021")]
91 pub fn check_status_with_status_list_2021<T>(
92 credential: &Credential<T>,
93 status_list_credential: &StatusList2021Credential,
94 status_check: crate::validator::StatusCheck,
95 ) -> ValidationUnitResult {
96 use crate::revocation::status_list_2021::CredentialStatus;
97 use crate::revocation::status_list_2021::StatusList2021Entry;
98
99 if status_check == crate::validator::StatusCheck::SkipAll {
100 return Ok(());
101 }
102
103 match &credential.credential_status {
104 None => Ok(()),
105 Some(status) => {
106 let status = StatusList2021Entry::try_from(status)
107 .map_err(|e| JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(e.to_string())))?;
108 if Some(status.status_list_credential()) == status_list_credential.id.as_ref()
109 && status.purpose() == status_list_credential.purpose()
110 {
111 let entry_status = status_list_credential
112 .entry(status.index())
113 .map_err(|e| JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(e.to_string())))?;
114 match entry_status {
115 CredentialStatus::Revoked => Err(JwtValidationError::Revoked),
116 CredentialStatus::Suspended => Err(JwtValidationError::Suspended),
117 CredentialStatus::Valid => Ok(()),
118 }
119 } else {
120 Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(
121 "The given statusListCredential doesn't match the credential's status".to_owned(),
122 )))
123 }
124 }
125 }
126 }
127 #[cfg(feature = "revocation-bitmap")]
131 pub fn check_status<DOC: AsRef<identity_document::document::CoreDocument>, T>(
132 credential: &Credential<T>,
133 trusted_issuers: &[DOC],
134 status_check: crate::validator::StatusCheck,
135 ) -> ValidationUnitResult {
136 use identity_did::CoreDID;
137 use identity_document::document::CoreDocument;
138
139 if status_check == crate::validator::StatusCheck::SkipAll {
140 return Ok(());
141 }
142
143 match &credential.credential_status {
144 None => Ok(()),
145 Some(status) => {
146 if status.type_ != crate::revocation::RevocationBitmap::TYPE {
148 if status_check == crate::validator::StatusCheck::SkipUnsupported {
149 return Ok(());
150 }
151 return Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!(
152 "unsupported type '{}'",
153 status.type_
154 ))));
155 }
156 let status: crate::credential::RevocationBitmapStatus =
157 crate::credential::RevocationBitmapStatus::try_from(status.clone())
158 .map_err(JwtValidationError::InvalidStatus)?;
159
160 let issuer_did: CoreDID = Self::extract_issuer(credential)?;
162 trusted_issuers
163 .iter()
164 .find(|issuer| <CoreDocument>::id(issuer.as_ref()) == &issuer_did)
165 .ok_or(JwtValidationError::DocumentMismatch(SignerContext::Issuer))
166 .and_then(|issuer| Self::check_revocation_bitmap_status(issuer, status))
167 }
168 }
169 }
170
171 #[cfg(feature = "revocation-bitmap")]
174 pub fn check_revocation_bitmap_status<DOC: AsRef<identity_document::document::CoreDocument> + ?Sized>(
175 issuer: &DOC,
176 status: crate::credential::RevocationBitmapStatus,
177 ) -> ValidationUnitResult {
178 use crate::revocation::RevocationDocumentExt;
179
180 let issuer_service_url: identity_did::DIDUrl = status.id().map_err(JwtValidationError::InvalidStatus)?;
181
182 let revocation_bitmap: crate::revocation::RevocationBitmap = issuer
184 .as_ref()
185 .resolve_revocation_bitmap(issuer_service_url.into())
186 .map_err(|_| JwtValidationError::ServiceLookupError)?;
187 let index: u32 = status.index().map_err(JwtValidationError::InvalidStatus)?;
188 if revocation_bitmap.is_revoked(index) {
189 Err(JwtValidationError::Revoked)
190 } else {
191 Ok(())
192 }
193 }
194
195 pub fn extract_issuer<D, T>(credential: &Credential<T>) -> std::result::Result<D, JwtValidationError>
201 where
202 D: DID,
203 <D as FromStr>::Err: std::error::Error + Send + Sync + 'static,
204 {
205 D::from_str(credential.issuer.url().as_str()).map_err(|err| JwtValidationError::SignerUrl {
206 signer_ctx: SignerContext::Issuer,
207 source: err.into(),
208 })
209 }
210
211 pub fn extract_issuer_from_jwt<D>(credential: &Jwt) -> std::result::Result<D, JwtValidationError>
217 where
218 D: DID,
219 <D as FromStr>::Err: std::error::Error + Send + Sync + 'static,
220 {
221 let validation_item = Decoder::new()
222 .decode_compact_serialization(credential.as_str().as_bytes(), None)
223 .map_err(JwtValidationError::JwsDecodingError)?;
224
225 let claims: CredentialJwtClaims<'_, Object> = CredentialJwtClaims::from_json_slice(&validation_item.claims())
226 .map_err(|err| {
227 JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
228 })?;
229
230 D::from_str(claims.iss.url().as_str()).map_err(|err| JwtValidationError::SignerUrl {
231 signer_ctx: SignerContext::Issuer,
232 source: err.into(),
233 })
234 }
235}