identity_credential/validator/jpt_credential_validation/
jpt_credential_validator_utils.rs

1// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::credential::Credential;
5use crate::revocation::RevocationDocumentExt;
6use crate::revocation::RevocationTimeframeStatus;
7use std::str::FromStr;
8
9use identity_core::common::Object;
10use identity_core::common::Timestamp;
11use identity_core::convert::FromJson;
12use identity_core::convert::ToJson;
13use identity_did::DID;
14use jsonprooftoken::encoding::SerializationType;
15use jsonprooftoken::jpt::claims::JptClaims;
16use jsonprooftoken::jwp::issued::JwpIssuedDecoder;
17
18use crate::credential::CredentialJwtClaims;
19use crate::credential::Jpt;
20use crate::validator::JwtValidationError;
21use crate::validator::SignerContext;
22
23/// Utility functions for verifying JPT credentials.
24#[derive(Debug)]
25#[non_exhaustive]
26pub struct JptCredentialValidatorUtils;
27
28type ValidationUnitResult<T = ()> = std::result::Result<T, JwtValidationError>;
29
30impl JptCredentialValidatorUtils {
31  /// Utility for extracting the issuer field of a [`Credential`] as a DID.
32  ///
33  /// # Errors
34  ///
35  /// Fails if the issuer field is not a valid DID.
36  pub fn extract_issuer<D, T>(credential: &Credential<T>) -> std::result::Result<D, JwtValidationError>
37  where
38    D: DID,
39    <D as FromStr>::Err: std::error::Error + Send + Sync + 'static,
40  {
41    D::from_str(credential.issuer.url().as_str()).map_err(|err| JwtValidationError::SignerUrl {
42      signer_ctx: SignerContext::Issuer,
43      source: err.into(),
44    })
45  }
46
47  /// Utility for extracting the issuer field of a credential in JPT representation as DID.
48  ///
49  /// # Errors
50  ///
51  /// If the JPT decoding fails or the issuer field is not a valid DID.
52  pub fn extract_issuer_from_issued_jpt<D>(credential: &Jpt) -> std::result::Result<D, JwtValidationError>
53  where
54    D: DID,
55    <D as FromStr>::Err: std::error::Error + Send + Sync + 'static,
56  {
57    let decoded = JwpIssuedDecoder::decode(credential.as_str(), SerializationType::COMPACT)
58      .map_err(JwtValidationError::JwpDecodingError)?;
59    let claims = decoded
60      .get_header()
61      .claims()
62      .ok_or("Claims not present")
63      .map_err(|err| {
64        JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into()))
65      })?;
66    let payloads = decoded.get_payloads();
67    let jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads);
68    let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| {
69      JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into()))
70    })?;
71
72    // Deserialize the raw claims
73    let credential_claims: CredentialJwtClaims<'_, Object> = CredentialJwtClaims::from_json_slice(&jpt_claims_json)
74      .map_err(|err| {
75        JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
76      })?;
77
78    D::from_str(credential_claims.iss.url().as_str()).map_err(|err| JwtValidationError::SignerUrl {
79      signer_ctx: SignerContext::Issuer,
80      source: err.into(),
81    })
82  }
83
84  /// Check timeframe interval in credentialStatus with `RevocationTimeframeStatus`.
85  pub fn check_timeframes_with_validity_timeframe_2024<T>(
86    credential: &Credential<T>,
87    validity_timeframe: Option<Timestamp>,
88    status_check: crate::validator::StatusCheck,
89  ) -> ValidationUnitResult {
90    if status_check == crate::validator::StatusCheck::SkipAll {
91      return Ok(());
92    }
93
94    match &credential.credential_status {
95      None => Ok(()),
96      Some(status) => {
97        if status.type_ == RevocationTimeframeStatus::TYPE {
98          let status: RevocationTimeframeStatus =
99            RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?;
100
101          Self::check_validity_timeframe(status, validity_timeframe)
102        } else {
103          if status_check == crate::validator::StatusCheck::SkipUnsupported {
104            return Ok(());
105          }
106          Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!(
107            "unsupported type '{}'",
108            status.type_
109          ))))
110        }
111      }
112    }
113  }
114
115  pub(crate) fn check_validity_timeframe(
116    status: RevocationTimeframeStatus,
117    validity_timeframe: Option<Timestamp>,
118  ) -> ValidationUnitResult {
119    let timeframe = validity_timeframe.unwrap_or(Timestamp::now_utc());
120
121    let check = timeframe >= status.start_validity_timeframe() && timeframe <= status.end_validity_timeframe();
122
123    if !check {
124      Err(JwtValidationError::OutsideTimeframe)
125    } else {
126      Ok(())
127    }
128  }
129
130  /// Checks whether the credential status has been revoked.
131  ///
132  /// Only supports `RevocationTimeframe2024`.
133  pub fn check_revocation_with_validity_timeframe_2024<
134    DOC: AsRef<identity_document::document::CoreDocument> + ?Sized,
135    T,
136  >(
137    credential: &Credential<T>,
138    issuer: &DOC,
139    status_check: crate::validator::StatusCheck,
140  ) -> ValidationUnitResult {
141    if status_check == crate::validator::StatusCheck::SkipAll {
142      return Ok(());
143    }
144
145    match &credential.credential_status {
146      None => Ok(()),
147      Some(status) => {
148        if status.type_ == RevocationTimeframeStatus::TYPE {
149          let status: RevocationTimeframeStatus =
150            RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?;
151
152          Self::check_revocation_bitmap(issuer, status)
153        } else {
154          if status_check == crate::validator::StatusCheck::SkipUnsupported {
155            return Ok(());
156          }
157          Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!(
158            "unsupported type '{}'",
159            status.type_
160          ))))
161        }
162      }
163    }
164  }
165
166  /// Check the given `status` against the matching [`RevocationBitmap`] service in the issuer's DID Document.
167  fn check_revocation_bitmap<DOC: AsRef<identity_document::document::CoreDocument> + ?Sized>(
168    issuer: &DOC,
169    status: RevocationTimeframeStatus,
170  ) -> ValidationUnitResult {
171    let issuer_service_url: identity_did::DIDUrl = identity_did::DIDUrl::parse(status.id()).map_err(|err| {
172      JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!(
173        "could not convert status id to DIDUrl; {}",
174        err,
175      )))
176    })?;
177
178    // Check whether index is revoked.
179    let revocation_bitmap: crate::revocation::RevocationBitmap = issuer
180      .as_ref()
181      .resolve_revocation_bitmap(issuer_service_url.into())
182      .map_err(|_| JwtValidationError::ServiceLookupError)?;
183
184    if let Some(index) = status.index() {
185      if revocation_bitmap.is_revoked(index) {
186        return Err(JwtValidationError::Revoked);
187      }
188    }
189    Ok(())
190  }
191
192  /// Checks whether the credential status has been revoked or the timeframe interval is INVALID
193  ///
194  /// Only supports `RevocationTimeframe2024`.
195  pub fn check_timeframes_and_revocation_with_validity_timeframe_2024<
196    DOC: AsRef<identity_document::document::CoreDocument> + ?Sized,
197    T,
198  >(
199    credential: &Credential<T>,
200    issuer: &DOC,
201    validity_timeframe: Option<Timestamp>,
202    status_check: crate::validator::StatusCheck,
203  ) -> ValidationUnitResult {
204    if status_check == crate::validator::StatusCheck::SkipAll {
205      return Ok(());
206    }
207
208    match &credential.credential_status {
209      None => Ok(()),
210      Some(status) => {
211        if status.type_ == RevocationTimeframeStatus::TYPE {
212          let status: RevocationTimeframeStatus =
213            RevocationTimeframeStatus::try_from(status).map_err(JwtValidationError::InvalidStatus)?;
214
215          let revocation = std::iter::once_with(|| Self::check_revocation_bitmap(issuer, status.clone()));
216
217          let timeframes = std::iter::once_with(|| Self::check_validity_timeframe(status.clone(), validity_timeframe));
218
219          let checks_iter = revocation.chain(timeframes);
220
221          let checks_error_iter = checks_iter.filter_map(|result| result.err());
222
223          let mut checks_errors: Vec<JwtValidationError> = checks_error_iter.take(1).collect();
224
225          match checks_errors.pop() {
226            Some(err) => Err(err),
227            None => Ok(()),
228          }
229        } else {
230          if status_check == crate::validator::StatusCheck::SkipUnsupported {
231            return Ok(());
232          }
233          Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(format!(
234            "unsupported type '{}'",
235            status.type_
236          ))))
237        }
238      }
239    }
240  }
241}