identity_credential/validator/jpt_credential_validation/
jpt_credential_validator.rs

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