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.credential,
84      std::slice::from_ref(issuer.as_ref()),
85      options,
86      fail_fast,
87    )?;
88
89    Ok(credential_token)
90  }
91
92  /// Decode and verify the PQ/T JWS signature of a [`Credential`] issued as a JWT using the DID Document of a trusted
93  /// issuer.
94  ///
95  /// A [`DecodedJwtCredential`] is returned upon success.
96  ///
97  /// # Warning
98  /// The caller must ensure that the DID Documents of the trusted issuers are up-to-date.
99  ///
100  /// ## Proofs
101  ///  Only the PQ/T JWS signature is verified. If the [`Credential`] contains a `proof` property this will not be
102  /// verified by this method.
103  ///
104  /// # Errors
105  /// This method immediately returns an error if
106  /// the credential issuer' url cannot be parsed to a DID belonging to one of the trusted issuers. Otherwise an attempt
107  /// to verify the credential's signature will be made and an error is returned upon failure.
108  pub fn verify_signature<DOC, T>(
109    &self,
110    credential: &Jwt,
111    trusted_issuers: &[DOC],
112    options: &JwsVerificationOptions,
113  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
114  where
115    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
116    DOC: AsRef<CoreDocument>,
117  {
118    Self::verify_signature_with_verifiers(&self.0, &self.1, credential, trusted_issuers, options)
119  }
120
121  pub(crate) fn parse_composite_pk<'a, 'i, DOC>(
122    jws: &JwsValidationItem<'a>,
123    trusted_issuers: &'i [DOC],
124    options: &JwsVerificationOptions,
125  ) -> Result<(&'a CompositeJwk, DIDUrl), JwtValidationError>
126  where
127    DOC: AsRef<CoreDocument>,
128    'i: 'a,
129  {
130    let nonce: Option<&str> = options.nonce.as_deref();
131    // Validate the nonce
132    if jws.nonce() != nonce {
133      return Err(JwtValidationError::JwsDecodingError(
134        identity_verification::jose::error::Error::InvalidParam("invalid nonce value"),
135      ));
136    }
137
138    // If no method_url is set, parse the `kid` to a DID Url which should be the identifier
139    // of a verification method in a trusted issuer's DID document.
140    let method_id: DIDUrl =
141      match &options.method_id {
142        Some(method_id) => method_id.clone(),
143        None => {
144          let kid: &str = jws.protected_header().and_then(|header| header.kid()).ok_or(
145            JwtValidationError::MethodDataLookupError {
146              source: None,
147              message: "could not extract kid from protected header",
148              signer_ctx: SignerContext::Issuer,
149            },
150          )?;
151
152          // Convert kid to DIDUrl
153          DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError {
154            source: Some(err.into()),
155            message: "could not parse kid as a DID Url",
156            signer_ctx: SignerContext::Issuer,
157          })?
158        }
159      };
160
161    // locate the corresponding issuer
162    let issuer: &CoreDocument = trusted_issuers
163      .iter()
164      .map(AsRef::as_ref)
165      .find(|issuer_doc| <CoreDocument>::id(issuer_doc) == method_id.did())
166      .ok_or(JwtValidationError::DocumentMismatch(SignerContext::Issuer))?;
167
168    // Obtain the public key from the issuer's DID document
169    issuer
170      .resolve_method(&method_id, options.method_scope)
171      .and_then(|method| method.data().composite_public_key())
172      .ok_or_else(|| JwtValidationError::MethodDataLookupError {
173        source: None,
174        message: "could not extract CompositePublicKey from a method identified by kid",
175        signer_ctx: SignerContext::Issuer,
176      })
177      .map(move |c: &CompositeJwk| (c, method_id))
178  }
179
180  /// Stateless version of [`Self::verify_signature`].
181  fn verify_signature_with_verifiers<DOC, T>(
182    traditional_signature_verifier: &TRV,
183    pq_signature_verifier: &PQV,
184    credential: &Jwt,
185    trusted_issuers: &[DOC],
186    options: &JwsVerificationOptions,
187  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
188  where
189    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
190    DOC: AsRef<CoreDocument>,
191  {
192    // Note the below steps are necessary because `CoreDocument::verify_jws` decodes the JWS and then searches for a
193    // method with a fragment (or full DID Url) matching `kid` in the given document. We do not want to carry out
194    // that process for potentially every document in `trusted_issuers`.
195
196    // Start decoding the credential
197    let decoded: JwsValidationItem<'_> = JwtCredentialValidator::<TRV>::decode(credential.as_str())?;
198
199    let (composite, method_id) = Self::parse_composite_pk(&decoded, trusted_issuers, options)?;
200
201    let credential_token = Self::verify_decoded_signature(
202      decoded,
203      composite.traditional_public_key(),
204      composite.pq_public_key(),
205      traditional_signature_verifier,
206      pq_signature_verifier,
207    )?;
208
209    // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before
210    // returning.
211    let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?;
212    if &issuer_id != method_id.did() {
213      return Err(JwtValidationError::IdentifierMismatch {
214        signer_ctx: SignerContext::Issuer,
215      });
216    };
217    Ok(credential_token)
218  }
219
220  pub(crate) fn verify_signature_raw<'a>(
221    decoded: JwsValidationItem<'a>,
222    traditional_pk: &TraditionalJwk,
223    pq_pk: &PostQuantumJwk,
224    traditional_verifier: &TRV,
225    pq_verifier: &PQV,
226  ) -> Result<DecodedJws<'a>, JwtValidationError> {
227    decoded
228      .verify_hybrid(traditional_verifier, pq_verifier, traditional_pk, pq_pk)
229      .map_err(|err| JwtValidationError::Signature {
230        source: err,
231        signer_ctx: SignerContext::Issuer,
232      })
233  }
234
235  /// Verify the signature using the given the `traditional_pk`, `pq_pk`,  `traditional_verifier` and `pq_verifier`.
236  fn verify_decoded_signature<T>(
237    decoded: JwsValidationItem<'_>,
238    traditional_pk: &TraditionalJwk,
239    pq_pk: &PostQuantumJwk,
240    traditional_verifier: &TRV,
241    pq_verifier: &PQV,
242  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
243  where
244    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
245  {
246    // Verify the JWS signature and obtain the decoded token containing the protected header and raw claims
247    let DecodedJws { protected, claims, .. } =
248      Self::verify_signature_raw(decoded, traditional_pk, pq_pk, traditional_verifier, pq_verifier)?;
249
250    let credential_claims: CredentialJwtClaims<'_, T> =
251      CredentialJwtClaims::from_json_slice(&claims).map_err(|err| {
252        JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
253      })?;
254
255    let custom_claims = credential_claims.custom.clone();
256
257    // Construct the credential token containing the credential and the protected header.
258    let credential: Credential<T> = credential_claims
259      .try_into_credential()
260      .map_err(JwtValidationError::CredentialStructure)?;
261
262    Ok(DecodedJwtCredential {
263      credential,
264      header: Box::new(protected),
265      custom_claims,
266    })
267  }
268}