identity_credential/validator/jwt_credential_validation/
jwt_credential_validator.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use identity_core::convert::FromJson;
5use identity_did::CoreDID;
6use identity_did::DIDUrl;
7use identity_document::document::CoreDocument;
8use identity_document::verifiable::JwsVerificationOptions;
9use identity_verification::jwk::Jwk;
10use identity_verification::jws::DecodedJws;
11use identity_verification::jws::Decoder;
12use identity_verification::jws::JwsValidationItem;
13use identity_verification::jws::JwsVerifier;
14
15use super::CompoundCredentialValidationError;
16use super::DecodedJwtCredential;
17use super::JwtCredentialValidationOptions;
18use super::JwtCredentialValidatorUtils;
19use super::JwtValidationError;
20use super::SignerContext;
21use crate::credential::Credential;
22use crate::credential::CredentialJwtClaims;
23use crate::credential::Jwt;
24use crate::validator::FailFast;
25
26/// A type for decoding and validating [`Credential`]s.
27#[non_exhaustive]
28pub struct JwtCredentialValidator<V: JwsVerifier>(V);
29
30impl<V: JwsVerifier> JwtCredentialValidator<V> {
31  /// Create a new [`JwtCredentialValidator`] that delegates cryptographic signature verification to the given
32  /// `signature_verifier`.
33  pub fn with_signature_verifier(signature_verifier: V) -> Self {
34    Self(signature_verifier)
35  }
36
37  /// Decodes and validates a [`Credential`] issued as a JWT. A [`DecodedJwtCredential`] is returned upon success.
38  ///
39  /// The following properties are validated according to `options`:
40  /// - the issuer's signature on the JWS,
41  /// - the expiration date,
42  /// - the issuance date,
43  /// - the semantic structure.
44  ///
45  /// # Warning
46  /// The lack of an error returned from this method is in of itself not enough to conclude that the credential can be
47  /// trusted. This section contains more information on additional checks that should be carried out before and after
48  /// calling this method.
49  ///
50  /// ## The state of the issuer's DID Document
51  /// The caller must ensure that `issuer` represents an up-to-date DID Document.
52  ///
53  /// ## Properties that are not validated
54  ///  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:
55  /// `proof`, `credentialStatus`, `type`, `credentialSchema`, `refreshService` **and more**.
56  /// These should be manually checked after validation, according to your requirements.
57  ///
58  /// # Errors
59  /// An error is returned whenever a validated condition is not satisfied.
60  pub fn validate<DOC, T>(
61    &self,
62    credential_jwt: &Jwt,
63    issuer: &DOC,
64    options: &JwtCredentialValidationOptions,
65    fail_fast: FailFast,
66  ) -> Result<DecodedJwtCredential<T>, CompoundCredentialValidationError>
67  where
68    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
69    DOC: AsRef<CoreDocument>,
70  {
71    let credential_token = self
72      .verify_signature(
73        credential_jwt,
74        std::slice::from_ref(issuer.as_ref()),
75        &options.verification_options,
76      )
77      .map_err(|err| CompoundCredentialValidationError {
78        validation_errors: [err].into(),
79      })?;
80
81    Self::validate_decoded_credential::<CoreDocument, T>(
82      credential_token,
83      std::slice::from_ref(issuer.as_ref()),
84      options,
85      fail_fast,
86    )
87  }
88
89  /// Decode and verify the JWS signature of a [`Credential`] issued as a JWT using the DID Document of a trusted
90  /// issuer.
91  ///
92  /// A [`DecodedJwtCredential`] is returned upon success.
93  ///
94  /// # Warning
95  /// The caller must ensure that the DID Documents of the trusted issuers are up-to-date.
96  ///
97  /// ## Proofs
98  ///  Only the JWS signature is verified. If the [`Credential`] contains a `proof` property this will not be verified
99  /// by this method.
100  ///
101  /// # Errors
102  /// This method immediately returns an error if
103  /// the credential issuer' url cannot be parsed to a DID belonging to one of the trusted issuers. Otherwise an attempt
104  /// to verify the credential's signature will be made and an error is returned upon failure.
105  pub fn verify_signature<DOC, T>(
106    &self,
107    credential: &Jwt,
108    trusted_issuers: &[DOC],
109    options: &JwsVerificationOptions,
110  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
111  where
112    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
113    DOC: AsRef<CoreDocument>,
114  {
115    Self::verify_signature_with_verifier(&self.0, credential, trusted_issuers, options)
116  }
117
118  // This method takes a slice of issuer's instead of a single issuer in order to better accommodate presentation
119  // validation. It also validates the relationship between a holder and the credential subjects when
120  // `relationship_criterion` is Some.
121  pub(crate) fn validate_decoded_credential<DOC, T>(
122    credential_token: DecodedJwtCredential<T>,
123    issuers: &[DOC],
124    options: &JwtCredentialValidationOptions,
125    fail_fast: FailFast,
126  ) -> Result<DecodedJwtCredential<T>, CompoundCredentialValidationError>
127  where
128    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
129    DOC: AsRef<CoreDocument>,
130  {
131    let credential: &Credential<T> = &credential_token.credential;
132    // Run all single concern Credential validations in turn and fail immediately if `fail_fast` is true.
133
134    let expiry_date_validation = std::iter::once_with(|| {
135      JwtCredentialValidatorUtils::check_expires_on_or_after(
136        &credential_token.credential,
137        options.earliest_expiry_date.unwrap_or_default(),
138      )
139    });
140
141    let issuance_date_validation = std::iter::once_with(|| {
142      JwtCredentialValidatorUtils::check_issued_on_or_before(
143        credential,
144        options.latest_issuance_date.unwrap_or_default(),
145      )
146    });
147
148    let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential));
149
150    let subject_holder_validation = std::iter::once_with(|| {
151      options
152        .subject_holder_relationship
153        .as_ref()
154        .map(|(holder, relationship)| {
155          JwtCredentialValidatorUtils::check_subject_holder_relationship(credential, holder, *relationship)
156        })
157        .unwrap_or(Ok(()))
158    });
159
160    let validation_units_iter = issuance_date_validation
161      .chain(expiry_date_validation)
162      .chain(structure_validation)
163      .chain(subject_holder_validation);
164
165    #[cfg(feature = "revocation-bitmap")]
166    let validation_units_iter = {
167      let revocation_validation =
168        std::iter::once_with(|| JwtCredentialValidatorUtils::check_status(credential, issuers, options.status));
169      validation_units_iter.chain(revocation_validation)
170    };
171
172    let validation_units_error_iter = validation_units_iter.filter_map(|result| result.err());
173    let validation_errors: Vec<JwtValidationError> = match fail_fast {
174      FailFast::FirstError => validation_units_error_iter.take(1).collect(),
175      FailFast::AllErrors => validation_units_error_iter.collect(),
176    };
177
178    if validation_errors.is_empty() {
179      Ok(credential_token)
180    } else {
181      Err(CompoundCredentialValidationError { validation_errors })
182    }
183  }
184
185  pub(crate) fn parse_jwk<'a, 'i, DOC>(
186    jws: &JwsValidationItem<'a>,
187    trusted_issuers: &'i [DOC],
188    options: &JwsVerificationOptions,
189  ) -> Result<(&'a Jwk, DIDUrl), JwtValidationError>
190  where
191    DOC: AsRef<CoreDocument>,
192    'i: 'a,
193  {
194    let nonce: Option<&str> = options.nonce.as_deref();
195    // Validate the nonce
196    if jws.nonce() != nonce {
197      return Err(JwtValidationError::JwsDecodingError(
198        identity_verification::jose::error::Error::InvalidParam("invalid nonce value"),
199      ));
200    }
201
202    // If no method_url is set, parse the `kid` to a DID Url which should be the identifier
203    // of a verification method in a trusted issuer's DID document.
204    let method_id: DIDUrl =
205      match &options.method_id {
206        Some(method_id) => method_id.clone(),
207        None => {
208          let kid: &str = jws.protected_header().and_then(|header| header.kid()).ok_or(
209            JwtValidationError::MethodDataLookupError {
210              source: None,
211              message: "could not extract kid from protected header",
212              signer_ctx: SignerContext::Issuer,
213            },
214          )?;
215
216          // Convert kid to DIDUrl
217          DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError {
218            source: Some(err.into()),
219            message: "could not parse kid as a DID Url",
220            signer_ctx: SignerContext::Issuer,
221          })?
222        }
223      };
224
225    // locate the corresponding issuer
226    let issuer: &CoreDocument = trusted_issuers
227      .iter()
228      .map(AsRef::as_ref)
229      .find(|issuer_doc| <CoreDocument>::id(issuer_doc) == method_id.did())
230      .ok_or(JwtValidationError::DocumentMismatch(SignerContext::Issuer))?;
231
232    // Obtain the public key from the issuer's DID document
233    issuer
234      .resolve_method(&method_id, options.method_scope)
235      .and_then(|method| method.data().public_key_jwk())
236      .ok_or_else(|| JwtValidationError::MethodDataLookupError {
237        source: None,
238        message: "could not extract JWK from a method identified by kid",
239        signer_ctx: SignerContext::Issuer,
240      })
241      .map(move |jwk| (jwk, method_id))
242  }
243
244  /// Stateless version of [`Self::verify_signature`]
245  fn verify_signature_with_verifier<DOC, S, T>(
246    signature_verifier: &S,
247    credential: &Jwt,
248    trusted_issuers: &[DOC],
249    options: &JwsVerificationOptions,
250  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
251  where
252    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
253    DOC: AsRef<CoreDocument>,
254    S: JwsVerifier,
255  {
256    // Note the below steps are necessary because `CoreDocument::verify_jws` decodes the JWS and then searches for a
257    // method with a fragment (or full DID Url) matching `kid` in the given document. We do not want to carry out
258    // that process for potentially every document in `trusted_issuers`.
259
260    // Start decoding the credential
261    let decoded: JwsValidationItem<'_> = Self::decode(credential.as_str())?;
262    let (public_key, method_id) = Self::parse_jwk(&decoded, trusted_issuers, options)?;
263
264    let credential_token = Self::verify_decoded_signature(decoded, public_key, signature_verifier)?;
265
266    // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before
267    // returning.
268    let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?;
269    if &issuer_id != method_id.did() {
270      return Err(JwtValidationError::IdentifierMismatch {
271        signer_ctx: SignerContext::Issuer,
272      });
273    };
274    Ok(credential_token)
275  }
276
277  /// Decode the credential into a [`JwsValidationItem`].
278  pub(crate) fn decode(credential_jws: &str) -> Result<JwsValidationItem<'_>, JwtValidationError> {
279    let decoder: Decoder = Decoder::new();
280
281    decoder
282      .decode_compact_serialization(credential_jws.as_bytes(), None)
283      .map_err(JwtValidationError::JwsDecodingError)
284  }
285
286  pub(crate) fn verify_signature_raw<'a, S: JwsVerifier>(
287    decoded: JwsValidationItem<'a>,
288    public_key: &Jwk,
289    signature_verifier: &S,
290  ) -> Result<DecodedJws<'a>, JwtValidationError> {
291    decoded
292      .verify(signature_verifier, public_key)
293      .map_err(|err| JwtValidationError::Signature {
294        source: err,
295        signer_ctx: SignerContext::Issuer,
296      })
297  }
298
299  /// Verify the signature using the given `public_key` and `signature_verifier`.
300  pub(crate) fn verify_decoded_signature<S: JwsVerifier, T>(
301    decoded: JwsValidationItem<'_>,
302    public_key: &Jwk,
303    signature_verifier: &S,
304  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
305  where
306    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
307  {
308    // Verify the JWS signature and obtain the decoded token containing the protected header and raw claims
309    let DecodedJws { protected, claims, .. } = Self::verify_signature_raw(decoded, public_key, signature_verifier)?;
310
311    let credential_claims: CredentialJwtClaims<'_, T> =
312      CredentialJwtClaims::from_json_slice(&claims).map_err(|err| {
313        JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
314      })?;
315
316    let custom_claims = credential_claims.custom.clone();
317
318    // Construct the credential token containing the credential and the protected header.
319    let credential: Credential<T> = credential_claims
320      .try_into_credential()
321      .map_err(JwtValidationError::CredentialStructure)?;
322
323    Ok(DecodedJwtCredential {
324      credential,
325      header: Box::new(protected),
326      custom_claims,
327    })
328  }
329}
330
331#[cfg(test)]
332mod tests {
333  use crate::credential::Subject;
334  use crate::validator::SubjectHolderRelationship;
335  use identity_core::common::Duration;
336  use identity_core::common::Url;
337  use once_cell::sync::Lazy;
338
339  // All tests here are essentially adaptations of the old JwtCredentialValidator tests.
340  use super::*;
341  use identity_core::common::Object;
342  use identity_core::common::Timestamp;
343  use proptest::proptest;
344  const LAST_RFC3339_COMPATIBLE_UNIX_TIMESTAMP: i64 = 253402300799; // 9999-12-31T23:59:59Z
345  const FIRST_RFC3999_COMPATIBLE_UNIX_TIMESTAMP: i64 = -62167219200; // 0000-01-01T00:00:00Z
346
347  const SIMPLE_CREDENTIAL_JSON: &str = r#"{
348    "@context": [
349      "https://www.w3.org/2018/credentials/v1",
350      "https://www.w3.org/2018/credentials/examples/v1"
351    ],
352    "id": "http://example.edu/credentials/3732",
353    "type": ["VerifiableCredential", "UniversityDegreeCredential"],
354    "issuer": "https://example.edu/issuers/14",
355    "issuanceDate": "2010-01-01T19:23:24Z",
356    "expirationDate": "2020-01-01T19:23:24Z",
357    "credentialSubject": {
358      "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
359      "degree": {
360        "type": "BachelorDegree",
361        "name": "Bachelor of Science in Mechanical Engineering"
362      }
363    }
364  }"#;
365
366  /// A simple credential shared by some of the tests in this module
367  static SIMPLE_CREDENTIAL: Lazy<Credential> =
368    Lazy::new(|| Credential::<Object>::from_json(SIMPLE_CREDENTIAL_JSON).unwrap());
369
370  #[test]
371  fn issued_on_or_before() {
372    assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(
373      &SIMPLE_CREDENTIAL,
374      SIMPLE_CREDENTIAL
375        .issuance_date
376        .checked_sub(Duration::minutes(1))
377        .unwrap()
378    )
379    .is_err());
380
381    // and now with a later timestamp
382    assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(
383      &SIMPLE_CREDENTIAL,
384      SIMPLE_CREDENTIAL
385        .issuance_date
386        .checked_add(Duration::minutes(1))
387        .unwrap()
388    )
389    .is_ok());
390  }
391
392  #[test]
393  fn check_subject_holder_relationship() {
394    let mut credential: Credential = SIMPLE_CREDENTIAL.clone();
395
396    // first ensure that holder_url is the subject and set the nonTransferable property
397    let actual_holder_url = credential.credential_subject.first().unwrap().id.clone().unwrap();
398    assert_eq!(credential.credential_subject.len(), 1);
399    credential.non_transferable = Some(true);
400
401    // checking with holder = subject passes for all defined subject holder relationships:
402    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
403      &credential,
404      &actual_holder_url,
405      SubjectHolderRelationship::AlwaysSubject
406    )
407    .is_ok());
408
409    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
410      &credential,
411      &actual_holder_url,
412      SubjectHolderRelationship::SubjectOnNonTransferable
413    )
414    .is_ok());
415
416    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
417      &credential,
418      &actual_holder_url,
419      SubjectHolderRelationship::Any
420    )
421    .is_ok());
422
423    // check with a holder different from the subject of the credential:
424    let issuer_url = Url::parse("did:core:0x1234567890").unwrap();
425    assert!(actual_holder_url != issuer_url);
426
427    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
428      &credential,
429      &issuer_url,
430      SubjectHolderRelationship::AlwaysSubject
431    )
432    .is_err());
433
434    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
435      &credential,
436      &issuer_url,
437      SubjectHolderRelationship::SubjectOnNonTransferable
438    )
439    .is_err());
440
441    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
442      &credential,
443      &issuer_url,
444      SubjectHolderRelationship::Any
445    )
446    .is_ok());
447
448    let mut credential_transferable = credential.clone();
449
450    credential_transferable.non_transferable = Some(false);
451
452    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
453      &credential_transferable,
454      &issuer_url,
455      SubjectHolderRelationship::SubjectOnNonTransferable
456    )
457    .is_ok());
458
459    credential_transferable.non_transferable = None;
460
461    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
462      &credential_transferable,
463      &issuer_url,
464      SubjectHolderRelationship::SubjectOnNonTransferable
465    )
466    .is_ok());
467
468    // two subjects (even when they are both the holder) should fail for all defined values except "Any"
469    let mut credential_duplicated_holder = credential;
470    credential_duplicated_holder
471      .credential_subject
472      .push(Subject::with_id(actual_holder_url));
473
474    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
475      &credential_duplicated_holder,
476      &issuer_url,
477      SubjectHolderRelationship::AlwaysSubject
478    )
479    .is_err());
480
481    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
482      &credential_duplicated_holder,
483      &issuer_url,
484      SubjectHolderRelationship::SubjectOnNonTransferable
485    )
486    .is_err());
487
488    assert!(JwtCredentialValidatorUtils::check_subject_holder_relationship(
489      &credential_duplicated_holder,
490      &issuer_url,
491      SubjectHolderRelationship::Any
492    )
493    .is_ok());
494  }
495
496  #[test]
497  fn simple_expires_on_or_after_with_expiration_date() {
498    let later_than_expiration_date = SIMPLE_CREDENTIAL
499      .expiration_date
500      .unwrap()
501      .checked_add(Duration::minutes(1))
502      .unwrap();
503    assert!(
504      JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, later_than_expiration_date).is_err()
505    );
506    // and now with an earlier date
507    let earlier_date = Timestamp::parse("2019-12-27T11:35:30Z").unwrap();
508    assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, earlier_date).is_ok());
509  }
510
511  // test with a few timestamps that should be RFC3339 compatible
512  proptest! {
513    #[test]
514    fn property_based_expires_after_with_expiration_date(seconds in 0..1_000_000_000_u32) {
515      let after_expiration_date = SIMPLE_CREDENTIAL.expiration_date.unwrap().checked_add(Duration::seconds(seconds)).unwrap();
516      let before_expiration_date = SIMPLE_CREDENTIAL.expiration_date.unwrap().checked_sub(Duration::seconds(seconds)).unwrap();
517      assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, after_expiration_date).is_err());
518      assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&SIMPLE_CREDENTIAL, before_expiration_date).is_ok());
519    }
520  }
521
522  proptest! {
523    #[test]
524    fn property_based_expires_after_no_expiration_date(seconds in FIRST_RFC3999_COMPATIBLE_UNIX_TIMESTAMP..LAST_RFC3339_COMPATIBLE_UNIX_TIMESTAMP) {
525      let mut credential = SIMPLE_CREDENTIAL.clone();
526      credential.expiration_date = None;
527      // expires after whatever the timestamp may be because the expires_after field is None.
528      assert!(JwtCredentialValidatorUtils::check_expires_on_or_after(&credential, Timestamp::from_unix(seconds).unwrap()).is_ok());
529    }
530  }
531
532  proptest! {
533    #[test]
534    fn property_based_issued_before(seconds in 0 ..1_000_000_000_u32) {
535
536      let earlier_than_issuance_date = SIMPLE_CREDENTIAL.issuance_date.checked_sub(Duration::seconds(seconds)).unwrap();
537      let later_than_issuance_date = SIMPLE_CREDENTIAL.issuance_date.checked_add(Duration::seconds(seconds)).unwrap();
538      assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(&SIMPLE_CREDENTIAL, earlier_than_issuance_date).is_err());
539      assert!(JwtCredentialValidatorUtils::check_issued_on_or_before(&SIMPLE_CREDENTIAL, later_than_issuance_date).is_ok());
540    }
541  }
542}