identity_credential/validator/jwt_credential_validation/
jwt_credential_validator_utils.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3use 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/// Utility functions for verifying JWT credentials.
23#[derive(Debug)]
24#[non_exhaustive]
25pub struct JwtCredentialValidatorUtils;
26
27type ValidationUnitResult<T = ()> = std::result::Result<T, JwtValidationError>;
28
29impl JwtCredentialValidatorUtils {
30  /// Validates the semantic structure of the [`Credential`].
31  ///
32  /// # Warning
33  /// This does not validate against the credential's schema nor the structure of the subject claims.
34  pub fn check_structure<T>(credential: &dyn CredentialT<Properties = T>) -> ValidationUnitResult {
35    // Ensure the base context is present and in the correct location
36    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    // The set of types MUST contain the base type
46    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    // Credentials MUST have at least one subject
55    if credential.subject().is_empty() {
56      return Err(JwtValidationError::CredentialStructure(crate::Error::MissingSubject));
57    }
58
59    // Each subject is defined as one or more properties - no empty objects
60    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  /// Validate that the [`Credential`] expires after the specified [`Timestamp`].
70  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  /// Validate that the [`Credential`] is issued on or before the specified [`Timestamp`].
81  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  /// Validate that the relationship between the `holder` and the credential subjects is in accordance with
93  /// `relationship`.
94  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  /// Checks whether the status specified in `credentialStatus` has been set by the issuer.
121  ///
122  /// Only supports `StatusList2021`.
123  #[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  /// Checks whether the credential status has been revoked.
161  ///
162  /// Only supports `RevocationBitmap2022`.
163  #[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    // Check status is supported.
181    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    // Check the credential index against the issuer's DID Document.
194    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  /// Check the given `status` against the matching [`RevocationBitmap`] service in the
203  /// issuer's DID Document.
204  #[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    // Check whether index is revoked.
214    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  /// Utility for extracting the issuer field of a [`Credential`] as a DID.
227  ///
228  /// # Errors
229  ///
230  /// Fails if the issuer field is not a valid DID.
231  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  /// Utility for extracting the issuer field of a credential in JWT representation as DID.
245  ///
246  /// # Errors
247  ///
248  /// If the JWT decoding fails or the issuer field is not a valid DID.
249  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}