identity_credential/validator/jwt_presentation_validation/
jwt_presentation_validator.rs

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