identity_credential/validator/jpt_presentation_validation/
jpt_presentation_validator.rs

1// Copyright 2020-2024 IOTA Stiftung, Fondazione Links
2// SPDX-License-Identifier: Apache-2.0
3
4use std::str::FromStr;
5
6use identity_core::common::Url;
7use identity_core::convert::FromJson;
8use identity_core::convert::ToJson;
9use identity_did::CoreDID;
10use identity_did::DIDUrl;
11use identity_document::document::CoreDocument;
12use jsonprooftoken::encoding::SerializationType;
13use jsonprooftoken::jpt::claims::JptClaims;
14use jsonprooftoken::jwk::key::Jwk as JwkExt;
15use jsonprooftoken::jwp::presented::JwpPresentedDecoder;
16
17use crate::credential::Credential;
18use crate::credential::CredentialJwtClaims;
19use crate::credential::Jpt;
20use crate::validator::CompoundCredentialValidationError;
21use crate::validator::FailFast;
22use crate::validator::JwtCredentialValidatorUtils;
23use crate::validator::JwtValidationError;
24use crate::validator::SignerContext;
25
26use super::DecodedJptPresentation;
27use super::JptPresentationValidationOptions;
28
29/// A type for decoding and validating Presented [`Credential`]s in JPT format.
30#[non_exhaustive]
31pub struct JptPresentationValidator;
32
33impl JptPresentationValidator {
34  /// Decodes and validates a Presented [`Credential`] issued as a JPT (JWP Presented Form). A
35  /// [`DecodedJptPresentation`] is returned upon success.
36  ///
37  /// The following properties are validated according to `options`:
38  /// - the holder's proof on the JWP,
39  /// - the expiration date,
40  /// - the issuance date,
41  /// - the semantic structure.
42  pub fn validate<DOC, T>(
43    presentation_jpt: &Jpt,
44    issuer: &DOC,
45    options: &JptPresentationValidationOptions,
46    fail_fast: FailFast,
47  ) -> Result<DecodedJptPresentation<T>, CompoundCredentialValidationError>
48  where
49    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
50    DOC: AsRef<CoreDocument>,
51  {
52    // First verify the JWP proof and decode the result into a presented credential token, then apply all other
53    // validations.
54    let presented_credential_token =
55      Self::verify_proof(presentation_jpt, issuer, options).map_err(|err| CompoundCredentialValidationError {
56        validation_errors: [err].into(),
57      })?;
58
59    let credential: &Credential<T> = &presented_credential_token.credential;
60
61    Self::validate_presented_credential::<T>(credential, fail_fast)?;
62
63    Ok(presented_credential_token)
64  }
65
66  pub(crate) fn validate_presented_credential<T>(
67    credential: &Credential<T>,
68    fail_fast: FailFast,
69  ) -> Result<(), CompoundCredentialValidationError>
70  where
71    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
72  {
73    let structure_validation = std::iter::once_with(|| JwtCredentialValidatorUtils::check_structure(credential));
74
75    let validation_units_iter = structure_validation;
76
77    let validation_units_error_iter = validation_units_iter.filter_map(|result| result.err());
78    let validation_errors: Vec<JwtValidationError> = match fail_fast {
79      FailFast::FirstError => validation_units_error_iter.take(1).collect(),
80      FailFast::AllErrors => validation_units_error_iter.collect(),
81    };
82
83    if validation_errors.is_empty() {
84      Ok(())
85    } else {
86      Err(CompoundCredentialValidationError { validation_errors })
87    }
88  }
89
90  /// Proof verification function
91  fn verify_proof<DOC, T>(
92    presentation_jpt: &Jpt,
93    issuer: &DOC,
94    options: &JptPresentationValidationOptions,
95  ) -> Result<DecodedJptPresentation<T>, JwtValidationError>
96  where
97    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
98    DOC: AsRef<CoreDocument>,
99  {
100    let decoded: JwpPresentedDecoder =
101      JwpPresentedDecoder::decode(presentation_jpt.as_str(), SerializationType::COMPACT)
102        .map_err(JwtValidationError::JwpDecodingError)?;
103
104    let nonce: Option<&String> = options.nonce.as_ref();
105    // Validate the nonce
106    if decoded.get_presentation_header().nonce() != nonce {
107      return Err(JwtValidationError::JwsDecodingError(
108        identity_verification::jose::error::Error::InvalidParam("invalid nonce value"),
109      ));
110    }
111
112    // If no method_url is set, parse the `kid` to a DID Url which should be the identifier
113    // of a verification method in a trusted issuer's DID document.
114    let method_id: DIDUrl = match &options.verification_options.method_id {
115      Some(method_id) => method_id.clone(),
116      None => {
117        let kid: &str = decoded
118          .get_issuer_header()
119          .kid()
120          .ok_or(JwtValidationError::MethodDataLookupError {
121            source: None,
122            message: "could not extract kid from protected header",
123            signer_ctx: SignerContext::Issuer,
124          })?;
125
126        // Convert kid to DIDUrl
127        DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError {
128          source: Some(err.into()),
129          message: "could not parse kid as a DID Url",
130          signer_ctx: SignerContext::Issuer,
131        })?
132      }
133    };
134
135    // check issuer
136    let issuer: &CoreDocument = issuer.as_ref();
137
138    if issuer.id() != method_id.did() {
139      return Err(JwtValidationError::DocumentMismatch(SignerContext::Issuer));
140    }
141
142    // Obtain the public key from the issuer's DID document
143    let public_key: JwkExt = issuer
144      .resolve_method(&method_id, options.verification_options.method_scope)
145      .and_then(|method| method.data().public_key_jwk())
146      .and_then(|k| k.try_into().ok()) //Conversio into jsonprooftoken::Jwk type
147      .ok_or_else(|| JwtValidationError::MethodDataLookupError {
148        source: None,
149        message: "could not extract JWK from a method identified by kid",
150        signer_ctx: SignerContext::Issuer,
151      })?;
152
153    let credential_token = Self::verify_decoded_jwp(decoded, &public_key)?;
154
155    // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before
156    // returning.
157    let issuer_id: CoreDID = JwtCredentialValidatorUtils::extract_issuer(&credential_token.credential)?;
158    if &issuer_id != method_id.did() {
159      return Err(JwtValidationError::IdentifierMismatch {
160        signer_ctx: SignerContext::Issuer,
161      });
162    };
163    Ok(credential_token)
164  }
165
166  /// Verify the decoded presented JWP proof using the given `public_key`.
167  fn verify_decoded_jwp<T>(
168    decoded: JwpPresentedDecoder,
169    public_key: &JwkExt,
170  ) -> Result<DecodedJptPresentation<T>, JwtValidationError>
171  where
172    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
173  {
174    // Verify Jwp proof
175    let decoded_jwp = decoded
176      .verify(public_key)
177      .map_err(JwtValidationError::JwpProofVerificationError)?;
178
179    let claims = decoded_jwp.get_claims().ok_or("Claims not present").map_err(|err| {
180      JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into()))
181    })?;
182    let payloads = decoded_jwp.get_payloads();
183    let mut jpt_claims = JptClaims::from_claims_and_payloads(claims, payloads);
184    // if not set the deserializatioon will throw an error since even the iat is not set, so we set this to 0
185    jpt_claims.nbf.map_or_else(
186      || {
187        jpt_claims.set_nbf(0);
188      },
189      |_| (),
190    );
191
192    let jpt_claims_json = jpt_claims.to_json_vec().map_err(|err| {
193      JwtValidationError::CredentialStructure(crate::Error::JptClaimsSetDeserializationError(err.into()))
194    })?;
195
196    // Deserialize the raw claims
197    let credential_claims: CredentialJwtClaims<'_, T> = CredentialJwtClaims::from_json_slice(&jpt_claims_json)
198      .map_err(|err| {
199        JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
200      })?;
201
202    let custom_claims = credential_claims.custom.clone();
203
204    // Construct the credential token containing the credential and the protected header.
205    let credential: Credential<T> = credential_claims
206      .try_into_credential()
207      .map_err(JwtValidationError::CredentialStructure)?;
208
209    let aud: Option<Url> = decoded_jwp.get_presentation_protected_header().aud().and_then(|aud| {
210      Url::from_str(aud)
211        .map_err(|_| {
212          JwtValidationError::JwsDecodingError(identity_verification::jose::error::Error::InvalidParam(
213            "invalid audience value",
214          ))
215        })
216        .ok()
217    });
218
219    Ok(DecodedJptPresentation {
220      credential,
221      aud,
222      custom_claims,
223      decoded_jwp,
224    })
225  }
226}