identity_credential/validator/jwt_credential_validation/
jwt_credential_validator_utils.rs1use std::str::FromStr;
4
5use identity_core::common::Object;
6use identity_core::common::Timestamp;
7use identity_core::common::Url;
8use identity_core::convert::FromJson;
9use identity_did::DID;
10use identity_verification::jws::Decoder;
11
12use super::JwtValidationError;
13use super::SignerContext;
14use crate::credential::Credential;
15use crate::credential::CredentialJwtClaims;
16use crate::credential::CredentialT;
17use crate::credential::CredentialV2;
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: &dyn CredentialT<Properties = T>) -> ValidationUnitResult {
35 match credential.context().get(0) {
37 Some(context) if context == credential.base_context() => {}
38 Some(_) | None => {
39 return Err(JwtValidationError::CredentialStructure(
40 crate::Error::MissingBaseContext,
41 ))
42 }
43 }
44
45 if !credential
47 .type_()
48 .iter()
49 .any(|type_| type_ == Credential::<T>::base_type())
50 {
51 return Err(JwtValidationError::CredentialStructure(crate::Error::MissingBaseType));
52 }
53
54 if credential.subject().is_empty() {
56 return Err(JwtValidationError::CredentialStructure(crate::Error::MissingSubject));
57 }
58
59 for subject in credential.subject().iter() {
61 if subject.id.is_none() && subject.properties.is_empty() {
62 return Err(JwtValidationError::CredentialStructure(crate::Error::InvalidSubject));
63 }
64 }
65
66 Ok(())
67 }
68
69 pub fn check_expires_on_or_after<T>(
71 credential: &dyn CredentialT<Properties = T>,
72 timestamp: Timestamp,
73 ) -> ValidationUnitResult {
74 match credential.valid_until() {
75 Some(exp) if exp < timestamp => Err(JwtValidationError::ExpirationDate),
76 _ => Ok(()),
77 }
78 }
79
80 pub fn check_issued_on_or_before<T>(
82 credential: &dyn CredentialT<Properties = T>,
83 timestamp: Timestamp,
84 ) -> ValidationUnitResult {
85 if credential.valid_from() <= timestamp {
86 Ok(())
87 } else {
88 Err(JwtValidationError::IssuanceDate)
89 }
90 }
91
92 pub fn check_subject_holder_relationship<T>(
95 credential: &dyn CredentialT<Properties = T>,
96 holder: &Url,
97 relationship: SubjectHolderRelationship,
98 ) -> ValidationUnitResult {
99 let url_matches = || {
100 if let [subject] = credential.subject().as_slice() {
101 subject.id.as_ref() == Some(holder)
102 } else {
103 false
104 }
105 };
106
107 let valid = match relationship {
108 SubjectHolderRelationship::AlwaysSubject => url_matches(),
109 SubjectHolderRelationship::SubjectOnNonTransferable => url_matches() || !credential.non_transferable(),
110 SubjectHolderRelationship::Any => true,
111 };
112
113 if valid {
114 Ok(())
115 } else {
116 Err(JwtValidationError::SubjectHolderRelationship)
117 }
118 }
119
120 #[cfg(feature = "status-list-2021")]
124 pub fn check_status_with_status_list_2021<T>(
125 credential: &dyn CredentialT<Properties = T>,
126 status_list_credential: &StatusList2021Credential,
127 status_check: crate::validator::StatusCheck,
128 ) -> ValidationUnitResult {
129 use crate::revocation::status_list_2021::CredentialStatus;
130 use crate::revocation::status_list_2021::StatusList2021Entry;
131
132 if status_check == crate::validator::StatusCheck::SkipAll {
133 return Ok(());
134 }
135
136 let Some(status) = credential.status() else {
137 return Ok(());
138 };
139
140 let status = StatusList2021Entry::try_from(status)
141 .map_err(|e| JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(e.to_string())))?;
142 if Some(status.status_list_credential()) == status_list_credential.id.as_ref()
143 && status.purpose() == status_list_credential.purpose()
144 {
145 let entry_status = status_list_credential
146 .entry(status.index())
147 .map_err(|e| JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(e.to_string())))?;
148 match entry_status {
149 CredentialStatus::Revoked => Err(JwtValidationError::Revoked),
150 CredentialStatus::Suspended => Err(JwtValidationError::Suspended),
151 CredentialStatus::Valid => Ok(()),
152 }
153 } else {
154 Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(
155 "The given statusListCredential doesn't match the credential's status".to_owned(),
156 )))
157 }
158 }
159
160 #[cfg(feature = "revocation-bitmap")]
164 pub fn check_status<DOC: AsRef<identity_document::document::CoreDocument>, T>(
165 credential: &dyn CredentialT<Properties = T>,
166 trusted_issuers: &[DOC],
167 status_check: crate::validator::StatusCheck,
168 ) -> ValidationUnitResult {
169 use identity_did::CoreDID;
170 use identity_document::document::CoreDocument;
171
172 if status_check == crate::validator::StatusCheck::SkipAll {
173 return Ok(());
174 }
175
176 let Some(status) = credential.status() else {
177 return Ok(());
178 };
179
180 if status.type_ != crate::revocation::RevocationBitmap::TYPE {
182 if status_check == crate::validator::StatusCheck::SkipUnsupported {
183 return Ok(());
184 }
185 return Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!(
186 "unsupported type '{}'",
187 status.type_
188 ))));
189 }
190 let status: crate::credential::RevocationBitmapStatus =
191 crate::credential::RevocationBitmapStatus::try_from(status.clone()).map_err(JwtValidationError::InvalidStatus)?;
192
193 let issuer_did: CoreDID = Self::extract_issuer(credential)?;
195 trusted_issuers
196 .iter()
197 .find(|issuer| <CoreDocument>::id(issuer.as_ref()) == &issuer_did)
198 .ok_or(JwtValidationError::DocumentMismatch(SignerContext::Issuer))
199 .and_then(|issuer| Self::check_revocation_bitmap_status(issuer, status))
200 }
201
202 #[cfg(feature = "revocation-bitmap")]
205 pub fn check_revocation_bitmap_status<DOC: AsRef<identity_document::document::CoreDocument> + ?Sized>(
206 issuer: &DOC,
207 status: crate::credential::RevocationBitmapStatus,
208 ) -> ValidationUnitResult {
209 use crate::revocation::RevocationDocumentExt;
210
211 let issuer_service_url: identity_did::DIDUrl = status.id().map_err(JwtValidationError::InvalidStatus)?;
212
213 let revocation_bitmap: crate::revocation::RevocationBitmap = issuer
215 .as_ref()
216 .resolve_revocation_bitmap(issuer_service_url.into())
217 .map_err(|_| JwtValidationError::ServiceLookupError)?;
218 let index: u32 = status.index().map_err(JwtValidationError::InvalidStatus)?;
219 if revocation_bitmap.is_revoked(index) {
220 Err(JwtValidationError::Revoked)
221 } else {
222 Ok(())
223 }
224 }
225
226 pub fn extract_issuer<D, T>(
232 credential: &dyn CredentialT<Properties = T>,
233 ) -> std::result::Result<D, JwtValidationError>
234 where
235 D: DID,
236 <D as FromStr>::Err: std::error::Error + Send + Sync + 'static,
237 {
238 D::from_str(credential.issuer().url().as_str()).map_err(|err| JwtValidationError::SignerUrl {
239 signer_ctx: SignerContext::Issuer,
240 source: err.into(),
241 })
242 }
243
244 pub fn extract_issuer_from_jwt<D>(credential: &impl AsRef<str>) -> std::result::Result<D, JwtValidationError>
250 where
251 D: DID,
252 <D as FromStr>::Err: std::error::Error + Send + Sync + 'static,
253 {
254 let validation_item = Decoder::new()
255 .decode_compact_serialization(credential.as_ref().as_bytes(), None)
256 .map_err(JwtValidationError::JwsDecodingError)?;
257
258 let try_v1 = |payload: &[u8]| {
259 CredentialJwtClaims::<'_, Object>::from_json_slice(payload).map(|claims| claims.iss.url().clone())
260 };
261 let try_v2 =
262 |payload: &[u8]| CredentialV2::<Object>::from_json_slice(payload).map(|cred| cred.issuer().url().clone());
263
264 let issuer_url = try_v1(validation_item.claims())
265 .or_else(|_| try_v2(validation_item.claims()))
266 .map_err(|_| {
267 JwtValidationError::CredentialStructure(crate::error::Error::JwtClaimsSetDeserializationError(
268 "cannot deserialize a Credential V1 or V2".into(),
269 ))
270 })?;
271
272 D::from_str(issuer_url.as_str()).map_err(|err| JwtValidationError::SignerUrl {
273 signer_ctx: SignerContext::Issuer,
274 source: err.into(),
275 })
276 }
277}