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