identity_credential/validator/sd_jwt/
validator.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::credential::CredentialJwtClaims;
5use crate::validator::CompoundCredentialValidationError;
6use crate::validator::DecodedJwtCredential;
7use crate::validator::FailFast;
8use crate::validator::JwtCredentialValidationOptions;
9use crate::validator::JwtCredentialValidator;
10use crate::validator::JwtCredentialValidatorUtils;
11use crate::validator::JwtValidationError;
12use crate::validator::SignerContext;
13use identity_core::common::Timestamp;
14use identity_core::convert::FromJson;
15use identity_did::CoreDID;
16use identity_did::DIDUrl;
17use identity_document::document::CoreDocument;
18use identity_document::verifiable::JwsVerificationOptions;
19use identity_verification::jwk::Jwk;
20use identity_verification::jws::DecodedJws;
21use identity_verification::jws::Decoder;
22use identity_verification::jws::JwsValidationItem;
23use identity_verification::jws::JwsVerifier;
24use itertools::Itertools;
25use sd_jwt_payload::KeyBindingJwtClaims;
26use sd_jwt_payload::SdJwt;
27use sd_jwt_payload::SdObjectDecoder;
28use serde_json::Value;
29
30use super::KeyBindingJWTValidationOptions;
31use super::KeyBindingJwtError;
32
33/// A type for decoding and validating [`SdJwt`]s.
34#[non_exhaustive]
35pub struct SdJwtCredentialValidator<V: JwsVerifier>(V, SdObjectDecoder);
36
37impl<V: JwsVerifier> SdJwtCredentialValidator<V> {
38  /// Creates a new [`SdJwtValidator`]that delegates cryptographic signature verification to the given
39  /// `signature_verifier`.
40  pub fn with_signature_verifier(signature_verifier: V, sd_decoder: SdObjectDecoder) -> Self {
41    Self(signature_verifier, sd_decoder)
42  }
43
44  /// Decodes and validates a [`Credential`] issued as an SD-JWT. A [`DecodedJwtCredential`] is returned upon success.
45  /// The credential is constructed by replacing disclosures following the
46  /// [`Selective Disclosure for JWTs (SD-JWT)`](https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html) standard.
47  ///
48  /// The following properties are validated according to `options`:
49  /// - the issuer's signature on the JWS,
50  /// - the expiration date,
51  /// - the issuance date,
52  /// - the semantic structure.
53  ///
54  /// # Warning
55  /// * The key binding JWT is not validated. If needed, it must be validated separately using
56  ///   `SdJwtValidator::validate_key_binding_jwt`.
57  /// * The lack of an error returned from this method is in of itself not enough to conclude that the credential can be
58  ///   trusted. This section contains more information on additional checks that should be carried out before and after
59  ///   calling this method.
60  ///
61  /// ## The state of the issuer's DID Document
62  /// The caller must ensure that `issuer` represents an up-to-date DID Document.
63  ///
64  /// ## Properties that are not validated
65  ///  There are many properties defined in [The Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) that are **not** validated, such as:
66  /// `proof`, `credentialStatus`, `type`, `credentialSchema`, `refreshService` **and more**.
67  /// These should be manually checked after validation, according to your requirements.
68  ///
69  /// # Errors
70  /// An error is returned whenever a validated condition is not satisfied.
71  pub fn validate_credential<DOC, T>(
72    &self,
73    sd_jwt: &SdJwt,
74    issuer: &DOC,
75    options: &JwtCredentialValidationOptions,
76    fail_fast: FailFast,
77  ) -> Result<DecodedJwtCredential<T>, CompoundCredentialValidationError>
78  where
79    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
80    DOC: AsRef<CoreDocument>,
81  {
82    let issuers = std::slice::from_ref(issuer.as_ref());
83    let credential = self
84      .verify_signature(sd_jwt, issuers, &options.verification_options)
85      .map_err(|err| CompoundCredentialValidationError {
86        validation_errors: [err].into(),
87      })?;
88
89    JwtCredentialValidator::<V>::validate_decoded_credential(credential, issuers, options, fail_fast)
90  }
91
92  /// Decode and verify the JWS signature of a [`Credential`] issued as an SD-JWT using the DID Document of a trusted
93  /// issuer and replaces the disclosures.
94  ///
95  /// A [`DecodedJwtCredential`] is returned upon success.
96  ///
97  /// # Warning
98  /// The caller must ensure that the DID Documents of the trusted issuers are up-to-date.
99  ///
100  /// ## Proofs
101  ///  Only the JWS signature is verified. If the [`Credential`] contains a `proof` property this will not be verified
102  /// by this method.
103  ///
104  /// # Errors
105  /// * If the issuer' URL cannot be parsed.
106  /// * If Signature verification fails.
107  /// * If SD decoding fails.
108  pub fn verify_signature<DOC, T>(
109    &self,
110    credential: &SdJwt,
111    trusted_issuers: &[DOC],
112    options: &JwsVerificationOptions,
113  ) -> Result<DecodedJwtCredential<T>, JwtValidationError>
114  where
115    T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
116    DOC: AsRef<CoreDocument>,
117  {
118    let SdJwt { jwt, disclosures, .. } = credential;
119    let signature = JwtCredentialValidator::<V>::decode(jwt.as_str())?;
120    let (public_key, method_id) = JwtCredentialValidator::<V>::parse_jwk(&signature, trusted_issuers, options)?;
121
122    let DecodedJws { protected, claims, .. } =
123      JwtCredentialValidator::<V>::verify_signature_raw(signature, public_key, &self.0)?;
124
125    let value: Value = serde_json::from_slice(&claims).map_err(|err| {
126      JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
127    })?;
128    let obj = value.as_object().ok_or(JwtValidationError::JwsDecodingError(
129      identity_verification::jose::error::Error::InvalidClaim("sd-jwt claims could not be deserialized"),
130    ))?;
131    let decoded: String = Value::Object(self.1.decode(obj, disclosures).map_err(|e| {
132      let err_str = format!("sd-jwt claims decoding failed, {}", e);
133      let err: &'static str = Box::leak(err_str.into_boxed_str());
134      JwtValidationError::JwsDecodingError(identity_verification::jose::error::Error::InvalidClaim(err))
135    })?)
136    .to_string();
137
138    let claims = CredentialJwtClaims::from_json(&decoded).map_err(|err| {
139      JwtValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into()))
140    })?;
141    let custom_claims = claims.custom.clone();
142    let credential = claims
143      .try_into_credential()
144      .map_err(JwtValidationError::CredentialStructure)?;
145
146    let decoded_credential = DecodedJwtCredential {
147      credential,
148      header: Box::new(protected),
149      custom_claims,
150    };
151
152    // Check that the DID component of the parsed `kid` does indeed correspond to the issuer in the credential before
153    // returning.
154    let issuer_id = JwtCredentialValidatorUtils::extract_issuer::<CoreDID, _>(&decoded_credential.credential)?;
155    if &issuer_id != method_id.did() {
156      return Err(JwtValidationError::IdentifierMismatch {
157        signer_ctx: SignerContext::Issuer,
158      });
159    };
160
161    Ok(decoded_credential)
162  }
163
164  /// Validates a Key Binding JWT (KB-JWT) according to `https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-07.html#name-key-binding-jwt`.
165  /// The Validation process includes:
166  ///   * Signature validation using public key materials defined in the `holder` document.
167  ///   * `typ` value in KB-JWT header.
168  ///   * `sd_hash` claim value in the KB-JWT claim.
169  ///   * Optional `nonce`, `aud` and issuance date validation.
170  pub fn validate_key_binding_jwt<DOC>(
171    &self,
172    sd_jwt: &SdJwt,
173    holder: &DOC,
174    options: &KeyBindingJWTValidationOptions,
175  ) -> Result<KeyBindingJwtClaims, KeyBindingJwtError>
176  where
177    DOC: AsRef<CoreDocument>,
178  {
179    // Check if KB exists in the SD-JWT.
180    let kb_jwt = if let Some(kb_jwt) = &sd_jwt.key_binding_jwt {
181      kb_jwt.clone()
182    } else {
183      return Err(KeyBindingJwtError::MissingKeyBindingJwt);
184    };
185
186    // Calculate the digest from the `sd_jwt.jwt` and the disclosures.
187    let jws_decoder = Decoder::new();
188    let decoded: JwsValidationItem<'_> = jws_decoder
189      .decode_compact_serialization(sd_jwt.jwt.as_bytes(), None)
190      .map_err(|err| KeyBindingJwtError::JwtValidationError(JwtValidationError::JwsDecodingError(err)))?;
191    let sd_jwt_claims: Value = serde_json::from_slice(decoded.claims())
192      .map_err(|_| KeyBindingJwtError::DeserializationError("failed to deserialize sd-jwt claims".to_string()))?;
193    let sd_jwt_claims_object = sd_jwt_claims
194      .as_object()
195      .ok_or(KeyBindingJwtError::DeserializationError(
196        "failed to deserialize sd-jwt claims".to_string(),
197      ))?;
198    let hasher = self.1.determine_hasher(sd_jwt_claims_object)?;
199    let disclosures = sd_jwt.disclosures.iter().join("~");
200    let hash_payload = format!("{}~{}~", sd_jwt.jwt, disclosures);
201    let digest = hasher.encoded_digest(&hash_payload);
202
203    // Verify the signature of the KB-JWT and extract claims.
204    let kb_decoded: JwsValidationItem<'_> = jws_decoder
205      .decode_compact_serialization(kb_jwt.as_bytes(), None)
206      .map_err(JwtValidationError::JwsDecodingError)?;
207    let typ: &str = kb_decoded
208      .protected_header()
209      .ok_or(KeyBindingJwtError::InvalidHeaderTypValue)?
210      .typ()
211      .ok_or(KeyBindingJwtError::InvalidHeaderTypValue)?;
212
213    if typ != KeyBindingJwtClaims::KB_JWT_HEADER_TYP {
214      return Err(KeyBindingJwtError::InvalidHeaderTypValue);
215    }
216    let method_id: DIDUrl = match &options.jws_options.method_id {
217      Some(method_id) => method_id.clone(),
218      None => {
219        let kid: &str = kb_decoded.protected_header().and_then(|header| header.kid()).ok_or(
220          JwtValidationError::MethodDataLookupError {
221            source: None,
222            message: "could not extract kid from protected header",
223            signer_ctx: SignerContext::Holder,
224          },
225        )?;
226
227        // Convert kid to DIDUrl
228        DIDUrl::parse(kid).map_err(|err| JwtValidationError::MethodDataLookupError {
229          source: Some(err.into()),
230          message: "could not parse kid as a DID Url",
231          signer_ctx: SignerContext::Issuer,
232        })?
233      }
234    };
235
236    // Obtain the public key from the holder's DID document
237    let public_key: &Jwk = holder
238      .as_ref()
239      .resolve_method(&method_id, options.jws_options.method_scope)
240      .and_then(|method| method.data().public_key_jwk())
241      .ok_or_else(|| JwtValidationError::MethodDataLookupError {
242        source: None,
243        message: "could not extract JWK from a method identified by kid",
244        signer_ctx: SignerContext::Holder,
245      })?;
246    let decoded: JwsValidationItem<'_> = jws_decoder
247      .decode_compact_serialization(kb_jwt.as_bytes(), None)
248      .map_err(|err| KeyBindingJwtError::JwtValidationError(JwtValidationError::JwsDecodingError(err)))?;
249    let decoded_kb_jws = decoded.verify(&self.0, public_key).unwrap();
250
251    let kb_jwt_claims: KeyBindingJwtClaims = serde_json::from_slice(&decoded_kb_jws.claims)
252      .map_err(|_| KeyBindingJwtError::DeserializationError("failed to deserialize kb-jwt claims".into()))?;
253
254    // Check if the `_sd_hash` matches.
255    if kb_jwt_claims.sd_hash != digest {
256      return Err(KeyBindingJwtError::InvalidDigest);
257    }
258
259    if let Some(nonce) = &options.nonce {
260      if *nonce != kb_jwt_claims.nonce {
261        return Err(KeyBindingJwtError::InvalidNonce);
262      }
263    }
264
265    if let Some(aud) = &options.aud {
266      if *aud != kb_jwt_claims.aud {
267        return Err(KeyBindingJwtError::AudienceMismatch);
268      }
269    }
270
271    let issuance_date = Timestamp::from_unix(kb_jwt_claims.iat)
272      .map_err(|_| KeyBindingJwtError::IssuanceDate("deserialization of `iat` failed".to_string()))?;
273
274    if let Some(earliest_issuance_date) = options.earliest_issuance_date {
275      if issuance_date < earliest_issuance_date {
276        return Err(KeyBindingJwtError::IssuanceDate(
277          "value is earlier than `earliest_issuance_date`".to_string(),
278        ));
279      }
280    }
281
282    if let Some(latest_issuance_date) = options.latest_issuance_date {
283      if issuance_date > latest_issuance_date {
284        return Err(KeyBindingJwtError::IssuanceDate(
285          "value is later than `latest_issuance_date`".to_string(),
286        ));
287      }
288    } else if issuance_date > Timestamp::now_utc() {
289      return Err(KeyBindingJwtError::IssuanceDate("value is in the future".to_string()));
290    }
291
292    Ok(kb_jwt_claims)
293  }
294}