identity_credential/validator/jwt_presentation_validation/
jwt_presentation_validator_hybrid.rs

1// Copyright 2020-2025 IOTA Stiftung, Fondazione Links
2// SPDX-License-Identifier: Apache-2.0
3
4use identity_core::common::Object;
5use identity_core::common::Timestamp;
6use identity_core::common::Url;
7use identity_core::convert::FromJson;
8use identity_did::CoreDID;
9use identity_document::document::CoreDocument;
10use identity_verification::jws::DecodedJws;
11use identity_verification::jws::JwsVerifier;
12use std::str::FromStr;
13
14use crate::credential::Jwt;
15use crate::presentation::Presentation;
16use crate::presentation::PresentationJwtClaims;
17use crate::validator::jwt_credential_validation::JwtValidationError;
18use crate::validator::jwt_credential_validation::SignerContext;
19
20use super::CompoundJwtPresentationValidationError;
21use super::DecodedJwtPresentation;
22use super::JwtPresentationValidationOptions;
23
24/// Struct for validating [`Presentation`] signed with a PQ/T signature.
25#[derive(Debug, Clone)]
26#[non_exhaustive]
27pub struct JwtPresentationValidatorHybrid<TRV: JwsVerifier, PQV: JwsVerifier>(TRV, PQV);
28
29impl<TRV, PQV> JwtPresentationValidatorHybrid<TRV, PQV>
30where
31  TRV: JwsVerifier,
32  PQV: JwsVerifier,
33{
34  /// Creates a new [`JwtPresentationValidatorHybrid`] using a specific traditional [`JwsVerifier`] and a specific PQ
35  /// [`JwsVerifier`].
36  pub fn with_signature_verifiers(traditional_signature_verifier: TRV, pq_signature_verifier: PQV) -> Self {
37    Self(traditional_signature_verifier, pq_signature_verifier)
38  }
39
40  /// Validates a [`Presentation`] signed with a PQ/T signature.
41  ///
42  /// The following properties are validated according to `options`:
43  /// - the JWT can be decoded into a semantically valid presentation.
44  /// - the expiration and issuance date contained in the JWT claims.
45  /// - the holder's PQ/T signature.
46  ///
47  /// Validation is done with respect to the properties set in `options`.
48  ///
49  /// # Warning
50  ///
51  /// * This method does NOT validate the constituent credentials and therefore also not the relationship between the
52  ///   credentials' subjects and the presentation holder. This can be done with
53  ///   [`JwtCredentialValidationOptions`](crate::validator::JwtCredentialValidationOptions).
54  /// * The lack of an error returned from this method is in of itself not enough to conclude that the presentation can
55  ///   be trusted. This section contains more information on additional checks that should be carried out before and
56  ///   after calling this method.
57  ///
58  /// ## The state of the supplied DID Documents.
59  ///
60  /// The caller must ensure that the DID Documents in `holder` and `issuers` are up-to-date.
61  ///
62  /// # Errors
63  ///
64  /// An error is returned whenever a validated condition is not satisfied or when decoding fails.
65  pub fn validate<HDOC, CRED, T>(
66    &self,
67    presentation: &Jwt,
68    holder: &HDOC,
69    options: &JwtPresentationValidationOptions,
70  ) -> Result<DecodedJwtPresentation<CRED, T>, CompoundJwtPresentationValidationError>
71  where
72    HDOC: AsRef<CoreDocument> + ?Sized,
73    T: Clone + serde::Serialize + serde::de::DeserializeOwned,
74    CRED: Clone + serde::Serialize + serde::de::DeserializeOwned + Clone,
75  {
76    // Verify JWS.
77    let decoded_jws: DecodedJws<'_> = holder
78      .as_ref()
79      .verify_jws_hybrid(
80        presentation.as_str(),
81        None,
82        &self.0,
83        &self.1,
84        &options.presentation_verifier_options,
85      )
86      .map_err(|err| {
87        CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationJwsError(err))
88      })?;
89
90    let claims: PresentationJwtClaims<'_, CRED, T> = PresentationJwtClaims::from_json_slice(&decoded_jws.claims)
91      .map_err(|err| {
92        CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationStructure(
93          crate::Error::JwtClaimsSetDeserializationError(err.into()),
94        ))
95      })?;
96
97    // Verify that holder document matches holder in presentation.
98    let holder_did: CoreDID = CoreDID::from_str(claims.iss.as_str()).map_err(|err| {
99      CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::SignerUrl {
100        signer_ctx: SignerContext::Holder,
101        source: err.into(),
102      })
103    })?;
104
105    if &holder_did != <CoreDocument>::id(holder.as_ref()) {
106      return Err(CompoundJwtPresentationValidationError::one_presentation_error(
107        JwtValidationError::DocumentMismatch(SignerContext::Holder),
108      ));
109    }
110
111    // Check the expiration date.
112    let expiration_date: Option<Timestamp> = claims
113      .exp
114      .map(|exp| {
115        Timestamp::from_unix(exp).map_err(|err| {
116          CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationStructure(
117            crate::Error::JwtClaimsSetDeserializationError(err.into()),
118          ))
119        })
120      })
121      .transpose()?;
122
123    if expiration_date.is_some_and(|exp| exp < options.earliest_expiry_date.unwrap_or_default()) {
124      return Err(CompoundJwtPresentationValidationError::one_presentation_error(
125        JwtValidationError::ExpirationDate,
126      ));
127    }
128    // Check issuance date.
129    let issuance_date: Option<Timestamp> = match claims.issuance_date {
130      Some(iss) => {
131        if iss.iat.is_some() || iss.nbf.is_some() {
132          Some(iss.to_issuance_date().map_err(|err| {
133            CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationStructure(
134              crate::Error::JwtClaimsSetDeserializationError(err.into()),
135            ))
136          })?)
137        } else {
138          None
139        }
140      }
141      None => None,
142    };
143
144    if issuance_date.is_some_and(|iss| iss > options.latest_issuance_date.unwrap_or_default()) {
145      return Err(CompoundJwtPresentationValidationError::one_presentation_error(
146        JwtValidationError::IssuanceDate,
147      ));
148    }
149
150    let aud: Option<Url> = claims.aud.clone();
151    let custom_claims: Option<Object> = claims.custom.clone();
152
153    let presentation: Presentation<CRED, T> = claims.try_into_presentation().map_err(|err| {
154      CompoundJwtPresentationValidationError::one_presentation_error(JwtValidationError::PresentationStructure(err))
155    })?;
156
157    let decoded_jwt_presentation: DecodedJwtPresentation<CRED, T> = DecodedJwtPresentation {
158      presentation,
159      header: Box::new(decoded_jws.protected),
160      expiration_date,
161      issuance_date,
162      aud,
163      custom_claims,
164    };
165
166    Ok(decoded_jwt_presentation)
167  }
168}