identity_credential/validator/sd_jwt/
validator.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::credential::Credential;
5use crate::credential::CredentialJwtClaims;
6use crate::credential::CredentialV2;
7use crate::validator::FailFast;
8use crate::validator::JwtCredentialValidationOptions;
9use crate::validator::JwtCredentialValidator;
10use crate::validator::JwtCredentialValidatorUtils;
11use crate::validator::JwtValidationError;
12use crate::validator::SignerContext;
13use crate::validator::UnexpectedValue;
14use anyhow::Context as _;
15use identity_core::common::Timestamp;
16use identity_core::convert::FromJson;
17use identity_did::CoreDID;
18use identity_did::DIDUrl;
19use identity_document::document::CoreDocument;
20use identity_document::verifiable::JwsVerificationOptions;
21use identity_verification::jwk::Jwk;
22use identity_verification::jws::Decoder;
23use identity_verification::jws::JwsValidationItem;
24use identity_verification::jws::JwsVerifier;
25use sd_jwt::Hasher;
26use sd_jwt::RequiredKeyBinding;
27use sd_jwt::SdJwt;
28use serde_json::Value;
29
30use super::KeyBindingJwtError;
31use super::KeyBindingJwtValidationOptions;
32
33/// Errors that can occur when validating an SD-JWT credential.
34#[derive(Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum SdJwtCredentialValidatorError {
37  /// The SD-JWT token is valid, but the disclosed claims could not be used to construct a well-formed credential.
38  #[error("failed to construct a well-formed credential from SD-JWT disclosed claims")]
39  CredentialStructure(#[source] Box<dyn std::error::Error + Send + Sync>),
40  /// Failed to verify the JWS signature.
41  #[error(transparent)]
42  JwsVerification(#[from] JwtValidationError),
43  /// SD-JWT specific error like: disclosure processing, or hasher mismatch.
44  #[error(transparent)]
45  SdJwt(#[from] sd_jwt::Error),
46}
47
48/// A type validating [`SdJwt`]s.
49#[non_exhaustive]
50pub struct SdJwtCredentialValidator<V: JwsVerifier>(V, Box<dyn Hasher>);
51
52impl<V: JwsVerifier> SdJwtCredentialValidator<V> {
53  /// Creates a new [`SdJwtCredentialValidator`] that delegates cryptographic signature verification to the given
54  /// `signature_verifier` and SD-JWT decoding to the given `hasher`.
55  pub fn new<H: Hasher + 'static>(signature_verifier: V, hasher: H) -> Self {
56    Self(signature_verifier, Box::new(hasher))
57  }
58
59  /// Decodes and validates a [Credential] issued as an SD-JWT.
60  /// The credential is constructed by replacing disclosures following the
61  /// [Selective Disclosure for JWTs (SD-JWT)](https://www.rfc-editor.org/rfc/rfc9901.html) standard.
62  ///
63  /// The following properties are validated according to `options`:
64  /// - the issuer's signature on the JWS,
65  /// - the expiration date,
66  /// - the issuance date,
67  /// - the semantic structure.
68  ///
69  /// # Warning
70  /// * The key binding JWT is not validated. If needed, it must be validated separately using
71  ///   [SdJwtCredentialValidator::validate_key_binding_jwt].
72  /// * The lack of an error returned from this method is in of itself not enough to conclude that the credential can be
73  ///   trusted. This section contains more information on additional checks that should be carried out before and after
74  ///   calling this method.
75  ///
76  /// ## The state of the issuer's DID Document
77  /// The caller must ensure that `issuer` represents an up-to-date DID Document.
78  ///
79  /// ## Properties that are not validated
80  ///  There are many properties defined in [The Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) that are **not** validated, such as:
81  /// `proof`, `credentialStatus`, `type`, `credentialSchema`, `refreshService` **and more**.
82  /// These should be manually checked after validation, according to your requirements.
83  ///
84  /// # Errors
85  /// An error is returned whenever a validated condition is not satisfied.
86  pub fn validate_credential<DOC, T>(
87    &self,
88    sd_jwt: &SdJwt,
89    trusted_issuers: &[DOC],
90    options: &JwtCredentialValidationOptions,
91  ) -> Result<Credential<T>, SdJwtCredentialValidatorError>
92  where
93    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
94    DOC: AsRef<CoreDocument>,
95  {
96    // Verify the JWS signature.
97    let vm_id = self.verify_signature_impl(&sd_jwt.presentation(), trusted_issuers, &options.verification_options)?;
98    let hasher = self.1.as_ref();
99
100    // Try to construct a credential from the disclosed claims.
101    let disclosed_claims = sd_jwt.clone().into_disclosed_object(hasher)?;
102    let credential_jwt_claims: CredentialJwtClaims<'_, T> = serde_json::from_value(Value::Object(disclosed_claims))
103      .map_err(|e| SdJwtCredentialValidatorError::CredentialStructure(e.into()))?;
104    let credential = credential_jwt_claims
105      .try_into_credential()
106      .map_err(|e| SdJwtCredentialValidatorError::CredentialStructure(e.into()))?;
107    JwtCredentialValidator::<V>::validate_decoded_credential(
108      &credential,
109      trusted_issuers,
110      options,
111      FailFast::FirstError,
112    )
113    .map_err(|mut errs| SdJwtCredentialValidatorError::JwsVerification(errs.validation_errors.swap_remove(0)))?;
114
115    let issuer_id = JwtCredentialValidatorUtils::extract_issuer::<CoreDID, _>(&credential)?;
116    if &issuer_id != vm_id.did() {
117      return Err(
118        JwtValidationError::IdentifierMismatch {
119          signer_ctx: SignerContext::Issuer,
120        }
121        .into(),
122      );
123    }
124
125    Ok(credential)
126  }
127
128  /// Decodes and validates a [CredentialV2] issued as an SD-JWT.
129  /// The credential is constructed by replacing disclosures following the
130  /// [Selective Disclosure for JWTs (SD-JWT)](https://www.rfc-editor.org/rfc/rfc9901.html) standard.
131  ///
132  /// The following properties are validated according to `options`:
133  /// - the issuer's signature on the JWS,
134  /// - the expiration date,
135  /// - the issuance date,
136  /// - the semantic structure.
137  ///
138  /// # Warning
139  /// * The key binding JWT is not validated. If needed, it must be validated separately using
140  ///   [SdJwtCredentialValidator::validate_key_binding_jwt].
141  /// * The lack of an error returned from this method is in of itself not enough to conclude that the credential can be
142  ///   trusted. This section contains more information on additional checks that should be carried out before and after
143  ///   calling this method.
144  ///
145  /// ## The state of the issuer's DID Document
146  /// The caller must ensure that `issuer` represents an up-to-date DID Document.
147  ///
148  /// ## Properties that are not validated
149  /// There are many properties defined in [The Verifiable Credentials Data Model v2](https://www.w3.org/TR/vc-data-model-2.0/)
150  /// that are **not** validated, such as:
151  /// `proof`, `credentialStatus`, `type`, `credentialSchema`, `refreshService` **and more**.
152  /// These should be manually checked after validation, according to your requirements.
153  ///
154  /// # Errors
155  /// An error is returned whenever a validated condition is not satisfied.
156  pub fn validate_credential_v2<DOC, T>(
157    &self,
158    sd_jwt: &SdJwt,
159    trusted_issuers: &[DOC],
160    options: &JwtCredentialValidationOptions,
161  ) -> Result<CredentialV2<T>, SdJwtCredentialValidatorError>
162  where
163    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
164    DOC: AsRef<CoreDocument>,
165  {
166    // Verify the JWS signature.
167    let vm_id = self.verify_signature_impl(&sd_jwt.presentation(), trusted_issuers, &options.verification_options)?;
168    let hasher = self.1.as_ref();
169
170    // Try to construct a credential from the disclosed claims.
171    let disclosed_claims = sd_jwt.clone().into_disclosed_object(hasher)?;
172    let credential = CredentialV2::<T>::from_json_value(Value::Object(disclosed_claims))
173      .map_err(|e| SdJwtCredentialValidatorError::CredentialStructure(e.into()))?;
174    JwtCredentialValidator::<V>::validate_decoded_credential(
175      &credential,
176      trusted_issuers,
177      options,
178      FailFast::FirstError,
179    )
180    .map_err(|mut errs| SdJwtCredentialValidatorError::JwsVerification(errs.validation_errors.swap_remove(0)))?;
181
182    let issuer_id = JwtCredentialValidatorUtils::extract_issuer::<CoreDID, _>(&credential)?;
183    if &issuer_id != vm_id.did() {
184      return Err(
185        JwtValidationError::IdentifierMismatch {
186          signer_ctx: SignerContext::Issuer,
187        }
188        .into(),
189      );
190    }
191
192    Ok(credential)
193  }
194
195  /// Decode and verify the JWS signature of an SD-JWT using the DID Document of a trusted issuer.
196  ///
197  /// # Warning
198  /// The caller must ensure that the DID Documents of the trusted issuers are up-to-date.
199  ///
200  /// # Errors
201  /// An error is returned whenever:
202  /// - The JWS signature is invalid;
203  /// - The issuer's public key could not be determined or is not found within the trusted issuers' documents;
204  pub fn verify_signature<DOC>(
205    &self,
206    sd_jwt: &SdJwt,
207    trusted_issuers: &[DOC],
208    options: &JwsVerificationOptions,
209  ) -> Result<(), JwtValidationError>
210  where
211    DOC: AsRef<CoreDocument>,
212  {
213    let sd_jwt_str = sd_jwt.presentation();
214    let _ = self.verify_signature_impl(&sd_jwt_str, trusted_issuers, options)?;
215
216    Ok(())
217  }
218
219  fn verify_signature_impl<DOC>(
220    &self,
221    sd_jwt: &str,
222    trusted_issuers: &[DOC],
223    options: &JwsVerificationOptions,
224  ) -> Result<DIDUrl, JwtValidationError>
225  where
226    DOC: AsRef<CoreDocument>,
227  {
228    let jwt_str = sd_jwt
229      .split_once('~')
230      .expect("valid SD-JWT contains at least one `~`")
231      .0;
232    let signature = JwtCredentialValidator::<V>::decode(jwt_str).expect("SD-JWT has a valid JWS");
233    let (public_key, method_id) = JwtCredentialValidator::<V>::parse_jwk(&signature, trusted_issuers, options)?;
234
235    JwtCredentialValidator::<V>::verify_signature_raw(signature, public_key, &self.0)?;
236    Ok(method_id)
237  }
238
239  /// Validates a [Key Binding JWT (KB-JWT)](https://www.rfc-editor.org/rfc/rfc9901.html#name-key-binding-jwt)
240  /// according to [RFC9901](https://www.rfc-editor.org/rfc/rfc9901.html#key_binding_security).
241  ///
242  /// The Validation process includes:
243  ///   - Signature validation using public key materials defined in the `holder` document.
244  ///   - `sd_hash` claim value in the KB-JWT claim.
245  ///   - Optional `nonce`, `aud`, and validity period validation.
246  ///
247  /// ## Notes
248  /// If a KB-JWT is not required by the SD-JWT, this method returns successfully early.
249  pub fn validate_key_binding_jwt<DOC>(
250    &self,
251    sd_jwt: &SdJwt,
252    holder_document: &DOC,
253    options: &KeyBindingJwtValidationOptions,
254  ) -> Result<(), KeyBindingJwtError>
255  where
256    DOC: AsRef<CoreDocument>,
257  {
258    // Check if a KB-JWT is required.
259    let Some(required_kb) = sd_jwt.required_key_bind() else {
260      return Ok(());
261    };
262    // Check if KB exists in the SD-JWT.
263    let Some(kb_jwt) = sd_jwt.key_binding_jwt() else {
264      return Err(KeyBindingJwtError::MissingKeyBindingJwt);
265    };
266
267    let hasher = self.1.as_ref();
268    let kb_jwt_str = kb_jwt.to_string();
269    // Determine the holder's public key.
270    let holder_pk = match required_kb {
271      RequiredKeyBinding::Jwk(jwk) => Jwk::from_json_value(Value::Object(jwk.clone()))
272        .context("failed to deserialize 'cnf' JWK")
273        .map_err(|e| KeyBindingJwtError::DeserializationError(e.into()))?,
274      RequiredKeyBinding::Kid(kid) => {
275        let method_id = DIDUrl::parse(kid).map_err(|e| JwtValidationError::MethodDataLookupError {
276          source: Some(e.into()),
277          message: "could not parse kid as a DID Url",
278          signer_ctx: SignerContext::Holder,
279        })?;
280        if holder_document.as_ref().id() != method_id.did() {
281          return Err(KeyBindingJwtError::JwtValidationError(
282            JwtValidationError::DocumentMismatch(SignerContext::Holder),
283          ));
284        }
285        holder_document
286          .as_ref()
287          .resolve_method(&method_id, None)
288          .and_then(|method| method.data().public_key_jwk())
289          .ok_or_else(|| JwtValidationError::MethodDataLookupError {
290            source: None,
291            message: "could not extract JWK from a method identified by kid",
292            signer_ctx: SignerContext::Holder,
293          })?
294          .clone()
295      }
296      _ => return Err(KeyBindingJwtError::UnsupportedCnfMethod),
297    };
298
299    let decoded: JwsValidationItem<'_> = Decoder::new()
300      .decode_compact_serialization(kb_jwt_str.as_bytes(), None)
301      .map_err(|err| KeyBindingJwtError::JwtValidationError(JwtValidationError::JwsDecodingError(err)))?;
302    let _ = decoded.verify(&self.0, &holder_pk).map_err(|e| {
303      KeyBindingJwtError::JwtValidationError(JwtValidationError::Signature {
304        source: e,
305        signer_ctx: SignerContext::Holder,
306      })
307    })?;
308
309    // Make sure the passed Hasher matches the one used in the SD-JWT.
310    if sd_jwt.claims()._sd_alg.as_deref().unwrap_or(sd_jwt::SHA_ALG_NAME) != hasher.alg_name() {
311      return Err(sd_jwt::Error::InvalidHasher(hasher.alg_name().to_owned()).into());
312    }
313
314    let digest = {
315      let sd_jwt_str = sd_jwt.to_string();
316      let last_tilde_index = sd_jwt_str.rfind('~').expect("valid SD-JWT contains at least one `~`");
317      hasher.encoded_digest(&sd_jwt_str[..last_tilde_index + 1])
318    };
319
320    // Check if the `_sd_hash` matches.
321    let sd_hash = kb_jwt.claims().sd_hash.as_str();
322    if sd_hash != digest.as_str() {
323      return Err(KeyBindingJwtError::InvalidDigest(UnexpectedValue {
324        expected: Some(digest.into()),
325        found: sd_hash.into(),
326      }));
327    }
328
329    if let Some(nonce) = options.nonce.as_deref() {
330      if nonce != kb_jwt.claims().nonce {
331        return Err(KeyBindingJwtError::InvalidNonce(UnexpectedValue {
332          expected: Some(nonce.to_owned().into()),
333          found: kb_jwt.claims().nonce.clone().into(),
334        }));
335      }
336    }
337
338    if let Some(aud) = options.aud.as_deref() {
339      if aud != kb_jwt.claims().aud {
340        return Err(KeyBindingJwtError::AudienceMismatch(UnexpectedValue {
341          expected: Some(aud.to_owned().into()),
342          found: kb_jwt.claims().aud.clone().into(),
343        }));
344      }
345    }
346
347    let issuance_date = Timestamp::from_unix(kb_jwt.claims().iat)
348      .map_err(|_| KeyBindingJwtError::IssuanceDate("deserialization of `iat` failed".to_string()))?;
349
350    if let Some(earliest_issuance_date) = options.earliest_issuance_date {
351      if issuance_date < earliest_issuance_date {
352        return Err(KeyBindingJwtError::IssuanceDate(
353          "value is earlier than `earliest_issuance_date`".to_string(),
354        ));
355      }
356    }
357
358    if let Some(latest_issuance_date) = options.latest_issuance_date {
359      if issuance_date > latest_issuance_date {
360        return Err(KeyBindingJwtError::IssuanceDate(
361          "value is later than `latest_issuance_date`".to_string(),
362        ));
363      }
364    } else if issuance_date > Timestamp::now_utc() {
365      return Err(KeyBindingJwtError::IssuanceDate("value is in the future".to_string()));
366    }
367
368    Ok(())
369  }
370}