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::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/// 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: &Credential<T>) -> ValidationUnitResult {
35    credential
36      .check_structure()
37      .map_err(JwtValidationError::CredentialStructure)
38  }
39
40  /// Validate that the [`Credential`] expires on or after the specified [`Timestamp`].
41  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  /// Validate that the [`Credential`] is issued on or before the specified [`Timestamp`].
49  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  /// Validate that the relationship between the `holder` and the credential subjects is in accordance with
56  /// `relationship`.
57  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        // need to check the case where the Many variant holds a vector of exactly one subject
66        if let [credential_subject] = subjects.as_slice() {
67          credential_subject.id.as_ref() == Some(holder)
68        } else {
69          // zero or > 1 subjects is interpreted to mean that the holder is not the subject
70          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  /// Checks whether the status specified in `credentialStatus` has been set by the issuer.
88  ///
89  /// Only supports `StatusList2021`.
90  #[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  /// Checks whether the credential status has been revoked.
128  ///
129  /// Only supports `RevocationBitmap2022`.
130  #[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        // Check status is supported.
147        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        // Check the credential index against the issuer's DID Document.
161        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  /// Check the given `status` against the matching [`RevocationBitmap`] service in the
172  /// issuer's DID Document.
173  #[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    // Check whether index is revoked.
183    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  /// Utility for extracting the issuer field of a [`Credential`] as a DID.
196  ///
197  /// # Errors
198  ///
199  /// Fails if the issuer field is not a valid DID.
200  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  /// Utility for extracting the issuer field of a credential in JWT representation as DID.
212  ///
213  /// # Errors
214  ///
215  /// If the JWT decoding fails or the issuer field is not a valid DID.
216  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}