identity_credential/validator/jwt_credential_validation/
jwt_credential_validator_hybrid.rs

1// Copyright 2020-2025 IOTA Stiftung, Fondazione Links
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::CompositeJwk;
10use identity_verification::jwk::PostQuantumJwk;
11use identity_verification::jwk::TraditionalJwk;
12use identity_verification::jws::DecodedJws;
13use identity_verification::jws::JwsValidationItem;
14use identity_verification::jws::JwsVerifier;
15
16use super::CompoundCredentialValidationError;
17use super::DecodedJwtCredential;
18use super::JwtCredentialValidationOptions;
19use super::JwtCredentialValidatorUtils;
20use super::JwtValidationError;
21use super::SignerContext;
22use crate::credential::Credential;
23use crate::credential::CredentialJwtClaims;
24use crate::credential::Jwt;
25use crate::validator::FailFast;
26use crate::validator::JwtCredentialValidator;
27
28/// A type for decoding and validating [`Credential`]s signed with a PQ/T signature.
29pub struct JwtCredentialValidatorHybrid<TRV, PQV>(TRV, PQV);
30
31impl<TRV: JwsVerifier, PQV: JwsVerifier> JwtCredentialValidatorHybrid<TRV, PQV> {
32  /// Create a new [`JwtCredentialValidatorHybrid`] that delegates cryptographic signature verification to the given
33  /// traditional [`JwsVerifier`] and PQ [`JwsVerifier`].
34  pub fn with_signature_verifiers(traditional_signature_verifier: TRV, pq_signature_verifier: PQV) -> Self {
35    Self(traditional_signature_verifier, pq_signature_verifier)
36  }
37
38  /// Decodes and validates a [`Credential`] issued as a JWT. A [`DecodedJwtCredential`] is returned upon success.
39  ///
40  /// The following properties are validated according to `options`:
41  /// - the issuer's PQ/T signature on the JWS,
42  /// - the expiration date,
43  /// - the issuance date,
44  /// - the semantic structure.
45  ///
46  /// # Warning
47  /// The lack of an error returned from this method is in of itself not enough to conclude that the credential can be
48  /// trusted. This section contains more information on additional checks that should be carried out before and after
49  /// calling this method.
50  ///
51  /// ## The state of the issuer's DID Document
52  /// The caller must ensure that `issuer` represents an up-to-date DID Document.
53  ///
54  /// ## Properties that are not validated
55  ///  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:
56  /// `proof`, `credentialStatus`, `type`, `credentialSchema`, `refreshService` **and more**.
57  /// These should be manually checked after validation, according to your requirements.
58  ///
59  /// # Errors
60  /// An error is returned whenever a validated condition is not satisfied.
61  pub fn validate<DOC, T>(
62    &self,
63    credential_jwt: &Jwt,
64    issuer: &DOC,
65    options: &JwtCredentialValidationOptions,
66    fail_fast: FailFast,
67  ) -> Result<DecodedJwtCredential<T>, CompoundCredentialValidationError>
68  where
69    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
70    DOC: AsRef<CoreDocument>,
71  {
72    let credential_token = self
73      .verify_signature(
74        credential_jwt,
75        std::slice::from_ref(issuer.as_ref()),
76        &options.verification_options,
77      )
78      .map_err(|err| CompoundCredentialValidationError {
79        validation_errors: [err].into(),
80      })?;
81
82    JwtCredentialValidator::<TRV>::validate_decoded_credential(
83      credential_token,
84      std::slice::from_ref(issuer.as_ref()),
85      options,
86      fail_fast,
87    )
88  }
89
90  /// Decode and verify the PQ/T JWS signature of a [`Credential`] issued as a JWT using the DID Document of a trusted
91  /// issuer.
92  ///
93  /// A [`DecodedJwtCredential`] is returned upon success.
94  ///
95  /// # Warning
96  /// The caller must ensure that the DID Documents of the trusted issuers are up-to-date.
97  ///
98  /// ## Proofs
99  ///  Only the PQ/T JWS signature is verified. If the [`Credential`] contains a `proof` property this will not be
100  /// verified by this method.
101  ///
102  /// # Errors
103  /// This method immediately returns an error if
104  /// the credential issuer' url cannot be parsed to a DID belonging to one of the trusted issuers. Otherwise an attempt
105  /// to verify the credential's signature will be made and an error is returned upon failure.
106  pub fn verify_signature<DOC, T>(
107    &self,
108    credential: &Jwt,
109    trusted_issuers: &[DOC],
110    options: &JwsVerificationOptions,
111  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
112  where
113    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
114    DOC: AsRef<CoreDocument>,
115  {
116    Self::verify_signature_with_verifiers(&self.0, &self.1, credential, trusted_issuers, options)
117  }
118
119  pub(crate) fn parse_composite_pk<'a, 'i, DOC>(
120    jws: &JwsValidationItem<'a>,
121    trusted_issuers: &'i [DOC],
122    options: &JwsVerificationOptions,
123  ) -> Result<(&'a CompositeJwk, DIDUrl), JwtValidationError>
124  where
125    DOC: AsRef<CoreDocument>,
126    'i: 'a,
127  {
128    let nonce: Option<&str> = options.nonce.as_deref();
129    // Validate the nonce
130    if jws.nonce() != nonce {
131      return Err(JwtValidationError::JwsDecodingError(
132        identity_verification::jose::error::Error::InvalidParam("invalid nonce value"),
133      ));
134    }
135
136    // If no method_url is set, parse the `kid` to a DID Url which should be the identifier
137    // of a verification method in a trusted issuer's DID document.
138    let method_id: DIDUrl =
139      match &options.method_id {
140        Some(method_id) => method_id.clone(),
141        None => {
142          let kid: &str = jws.protected_header().and_then(|header| header.kid()).ok_or(
143            JwtValidationError::MethodDataLookupError {
144              source: None,
145              message: "could not extract kid from protected header",
146              signer_ctx: SignerContext::Issuer,
147            },
148          )?;
149
150          // Convert kid to DIDUrl
151          DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError {
152            source: Some(err.into()),
153            message: "could not parse kid as a DID Url",
154            signer_ctx: SignerContext::Issuer,
155          })?
156        }
157      };
158
159    // locate the corresponding issuer
160    let issuer: &CoreDocument = trusted_issuers
161      .iter()
162      .map(AsRef::as_ref)
163      .find(|issuer_doc| <CoreDocument>::id(issuer_doc) == method_id.did())
164      .ok_or(JwtValidationError::DocumentMismatch(SignerContext::Issuer))?;
165
166    // Obtain the public key from the issuer's DID document
167    issuer
168      .resolve_method(&method_id, options.method_scope)
169      .and_then(|method| method.data().composite_public_key())
170      .ok_or_else(|| JwtValidationError::MethodDataLookupError {
171        source: None,
172        message: "could not extract CompositePublicKey from a method identified by kid",
173        signer_ctx: SignerContext::Issuer,
174      })
175      .map(move |c: &CompositeJwk| (c, method_id))
176  }
177
178  /// Stateless version of [`Self::verify_signature`].
179  fn verify_signature_with_verifiers<DOC, T>(
180    traditional_signature_verifier: &TRV,
181    pq_signature_verifier: &PQV,
182    credential: &Jwt,
183    trusted_issuers: &[DOC],
184    options: &JwsVerificationOptions,
185  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
186  where
187    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
188    DOC: AsRef<CoreDocument>,
189  {
190    // Note the below steps are necessary because `CoreDocument::verify_jws` decodes the JWS and then searches for a
191    // method with a fragment (or full DID Url) matching `kid` in the given document. We do not want to carry out
192    // that process for potentially every document in `trusted_issuers`.
193
194    // Start decoding the credential
195    let decoded: JwsValidationItem<'_> = JwtCredentialValidator::<TRV>::decode(credential.as_str())?;
196
197    let (composite, method_id) = Self::parse_composite_pk(&decoded, trusted_issuers, options)?;
198
199    let credential_token = Self::verify_decoded_signature(
200      decoded,
201      composite.traditional_public_key(),
202      composite.pq_public_key(),
203      traditional_signature_verifier,
204      pq_signature_verifier,
205    )?;
206
207    // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before
208    // returning.
209    let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?;
210    if &issuer_id != method_id.did() {
211      return Err(JwtValidationError::IdentifierMismatch {
212        signer_ctx: SignerContext::Issuer,
213      });
214    };
215    Ok(credential_token)
216  }
217
218  pub(crate) fn verify_signature_raw<'a>(
219    decoded: JwsValidationItem<'a>,
220    traditional_pk: &TraditionalJwk,
221    pq_pk: &PostQuantumJwk,
222    traditional_verifier: &TRV,
223    pq_verifier: &PQV,
224  ) -> Result<DecodedJws<'a>, JwtValidationError> {
225    decoded
226      .verify_hybrid(traditional_verifier, pq_verifier, traditional_pk, pq_pk)
227      .map_err(|err| JwtValidationError::Signature {
228        source: err,
229        signer_ctx: SignerContext::Issuer,
230      })
231  }
232
233  /// Verify the signature using the given the `traditional_pk`, `pq_pk`,  `traditional_verifier` and `pq_verifier`.
234  fn verify_decoded_signature<T>(
235    decoded: JwsValidationItem<'_>,
236    traditional_pk: &TraditionalJwk,
237    pq_pk: &PostQuantumJwk,
238    traditional_verifier: &TRV,
239    pq_verifier: &PQV,
240  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
241  where
242    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
243  {
244    // Verify the JWS signature and obtain the decoded token containing the protected header and raw claims
245    let DecodedJws { protected, claims, .. } =
246      Self::verify_signature_raw(decoded, traditional_pk, pq_pk, traditional_verifier, pq_verifier)?;
247
248    let credential_claims: CredentialJwtClaims<'_, T> =
249      CredentialJwtClaims::from_json_slice(&claims).map_err(|err| {
250        JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
251      })?;
252
253    let custom_claims = credential_claims.custom.clone();
254
255    // Construct the credential token containing the credential and the protected header.
256    let credential: Credential<T> = credential_claims
257      .try_into_credential()
258      .map_err(JwtValidationError::CredentialStructure)?;
259
260    Ok(DecodedJwtCredential {
261      credential,
262      header: Box::new(protected),
263      custom_claims,
264    })
265  }
266}