identity_credential/validator/sd_jwt/
validator.rs1use crate::credential::Credential;
5use crate::credential::CredentialJwtClaims;
6use crate::credential::CredentialV2;
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 crate::validator::UnexpectedValue;
14use anyhow::Context as _;
15use identity_core::common::Timestamp;
16use identity_core::convert::FromJson;
17use identity_did::CoreDID;
18use identity_did::DIDUrl;
19use identity_document::document::CoreDocument;
20use identity_document::verifiable::JwsVerificationOptions;
21use identity_verification::jwk::Jwk;
22use identity_verification::jws::Decoder;
23use identity_verification::jws::JwsValidationItem;
24use identity_verification::jws::JwsVerifier;
25use sd_jwt::Hasher;
26use sd_jwt::RequiredKeyBinding;
27use sd_jwt::SdJwt;
28use serde_json::Value;
29
30use super::KeyBindingJwtError;
31use super::KeyBindingJwtValidationOptions;
32
33#[derive(Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum SdJwtCredentialValidatorError {
37 #[error("failed to construct a well-formed credential from SD-JWT disclosed claims")]
39 CredentialStructure(#[source] Box<dyn std::error::Error + Send + Sync>),
40 #[error(transparent)]
42 JwsVerification(#[from] JwtValidationError),
43 #[error(transparent)]
45 SdJwt(#[from] sd_jwt::Error),
46}
47
48#[non_exhaustive]
50pub struct SdJwtCredentialValidator<V: JwsVerifier>(V, Box<dyn Hasher>);
51
52impl<V: JwsVerifier> SdJwtCredentialValidator<V> {
53 pub fn new<H: Hasher + 'static>(signature_verifier: V, hasher: H) -> Self {
56 Self(signature_verifier, Box::new(hasher))
57 }
58
59 pub fn validate_credential<DOC, T>(
87 &self,
88 sd_jwt: &SdJwt,
89 trusted_issuers: &[DOC],
90 options: &JwtCredentialValidationOptions,
91 ) -> Result<Credential<T>, SdJwtCredentialValidatorError>
92 where
93 T: Clone + serde::Serialize + serde::de::DeserializeOwned,
94 DOC: AsRef<CoreDocument>,
95 {
96 let vm_id = self.verify_signature_impl(&sd_jwt.presentation(), trusted_issuers, &options.verification_options)?;
98 let hasher = self.1.as_ref();
99
100 let disclosed_claims = sd_jwt.clone().into_disclosed_object(hasher)?;
102 let credential_jwt_claims: CredentialJwtClaims<'_, T> = serde_json::from_value(Value::Object(disclosed_claims))
103 .map_err(|e| SdJwtCredentialValidatorError::CredentialStructure(e.into()))?;
104 let credential = credential_jwt_claims
105 .try_into_credential()
106 .map_err(|e| SdJwtCredentialValidatorError::CredentialStructure(e.into()))?;
107 JwtCredentialValidator::<V>::validate_decoded_credential(
108 &credential,
109 trusted_issuers,
110 options,
111 FailFast::FirstError,
112 )
113 .map_err(|mut errs| SdJwtCredentialValidatorError::JwsVerification(errs.validation_errors.swap_remove(0)))?;
114
115 let issuer_id = JwtCredentialValidatorUtils::extract_issuer::<CoreDID, _>(&credential)?;
116 if &issuer_id != vm_id.did() {
117 return Err(
118 JwtValidationError::IdentifierMismatch {
119 signer_ctx: SignerContext::Issuer,
120 }
121 .into(),
122 );
123 }
124
125 Ok(credential)
126 }
127
128 pub fn validate_credential_v2<DOC, T>(
157 &self,
158 sd_jwt: &SdJwt,
159 trusted_issuers: &[DOC],
160 options: &JwtCredentialValidationOptions,
161 ) -> Result<CredentialV2<T>, SdJwtCredentialValidatorError>
162 where
163 T: Clone + serde::Serialize + serde::de::DeserializeOwned,
164 DOC: AsRef<CoreDocument>,
165 {
166 let vm_id = self.verify_signature_impl(&sd_jwt.presentation(), trusted_issuers, &options.verification_options)?;
168 let hasher = self.1.as_ref();
169
170 let disclosed_claims = sd_jwt.clone().into_disclosed_object(hasher)?;
172 let credential = CredentialV2::<T>::from_json_value(Value::Object(disclosed_claims))
173 .map_err(|e| SdJwtCredentialValidatorError::CredentialStructure(e.into()))?;
174 JwtCredentialValidator::<V>::validate_decoded_credential(
175 &credential,
176 trusted_issuers,
177 options,
178 FailFast::FirstError,
179 )
180 .map_err(|mut errs| SdJwtCredentialValidatorError::JwsVerification(errs.validation_errors.swap_remove(0)))?;
181
182 let issuer_id = JwtCredentialValidatorUtils::extract_issuer::<CoreDID, _>(&credential)?;
183 if &issuer_id != vm_id.did() {
184 return Err(
185 JwtValidationError::IdentifierMismatch {
186 signer_ctx: SignerContext::Issuer,
187 }
188 .into(),
189 );
190 }
191
192 Ok(credential)
193 }
194
195 pub fn verify_signature<DOC>(
205 &self,
206 sd_jwt: &SdJwt,
207 trusted_issuers: &[DOC],
208 options: &JwsVerificationOptions,
209 ) -> Result<(), JwtValidationError>
210 where
211 DOC: AsRef<CoreDocument>,
212 {
213 let sd_jwt_str = sd_jwt.presentation();
214 let _ = self.verify_signature_impl(&sd_jwt_str, trusted_issuers, options)?;
215
216 Ok(())
217 }
218
219 fn verify_signature_impl<DOC>(
220 &self,
221 sd_jwt: &str,
222 trusted_issuers: &[DOC],
223 options: &JwsVerificationOptions,
224 ) -> Result<DIDUrl, JwtValidationError>
225 where
226 DOC: AsRef<CoreDocument>,
227 {
228 let jwt_str = sd_jwt
229 .split_once('~')
230 .expect("valid SD-JWT contains at least one `~`")
231 .0;
232 let signature = JwtCredentialValidator::<V>::decode(jwt_str).expect("SD-JWT has a valid JWS");
233 let (public_key, method_id) = JwtCredentialValidator::<V>::parse_jwk(&signature, trusted_issuers, options)?;
234
235 JwtCredentialValidator::<V>::verify_signature_raw(signature, public_key, &self.0)?;
236 Ok(method_id)
237 }
238
239 pub fn validate_key_binding_jwt<DOC>(
250 &self,
251 sd_jwt: &SdJwt,
252 holder_document: &DOC,
253 options: &KeyBindingJwtValidationOptions,
254 ) -> Result<(), KeyBindingJwtError>
255 where
256 DOC: AsRef<CoreDocument>,
257 {
258 let Some(required_kb) = sd_jwt.required_key_bind() else {
260 return Ok(());
261 };
262 let Some(kb_jwt) = sd_jwt.key_binding_jwt() else {
264 return Err(KeyBindingJwtError::MissingKeyBindingJwt);
265 };
266
267 let hasher = self.1.as_ref();
268 let kb_jwt_str = kb_jwt.to_string();
269 let holder_pk = match required_kb {
271 RequiredKeyBinding::Jwk(jwk) => Jwk::from_json_value(Value::Object(jwk.clone()))
272 .context("failed to deserialize 'cnf' JWK")
273 .map_err(|e| KeyBindingJwtError::DeserializationError(e.into()))?,
274 RequiredKeyBinding::Kid(kid) => {
275 let method_id = DIDUrl::parse(kid).map_err(|e| JwtValidationError::MethodDataLookupError {
276 source: Some(e.into()),
277 message: "could not parse kid as a DID Url",
278 signer_ctx: SignerContext::Holder,
279 })?;
280 if holder_document.as_ref().id() != method_id.did() {
281 return Err(KeyBindingJwtError::JwtValidationError(
282 JwtValidationError::DocumentMismatch(SignerContext::Holder),
283 ));
284 }
285 holder_document
286 .as_ref()
287 .resolve_method(&method_id, None)
288 .and_then(|method| method.data().public_key_jwk())
289 .ok_or_else(|| JwtValidationError::MethodDataLookupError {
290 source: None,
291 message: "could not extract JWK from a method identified by kid",
292 signer_ctx: SignerContext::Holder,
293 })?
294 .clone()
295 }
296 _ => return Err(KeyBindingJwtError::UnsupportedCnfMethod),
297 };
298
299 let decoded: JwsValidationItem<'_> = Decoder::new()
300 .decode_compact_serialization(kb_jwt_str.as_bytes(), None)
301 .map_err(|err| KeyBindingJwtError::JwtValidationError(JwtValidationError::JwsDecodingError(err)))?;
302 let _ = decoded.verify(&self.0, &holder_pk).map_err(|e| {
303 KeyBindingJwtError::JwtValidationError(JwtValidationError::Signature {
304 source: e,
305 signer_ctx: SignerContext::Holder,
306 })
307 })?;
308
309 if sd_jwt.claims()._sd_alg.as_deref().unwrap_or(sd_jwt::SHA_ALG_NAME) != hasher.alg_name() {
311 return Err(sd_jwt::Error::InvalidHasher(hasher.alg_name().to_owned()).into());
312 }
313
314 let digest = {
315 let sd_jwt_str = sd_jwt.to_string();
316 let last_tilde_index = sd_jwt_str.rfind('~').expect("valid SD-JWT contains at least one `~`");
317 hasher.encoded_digest(&sd_jwt_str[..last_tilde_index + 1])
318 };
319
320 let sd_hash = kb_jwt.claims().sd_hash.as_str();
322 if sd_hash != digest.as_str() {
323 return Err(KeyBindingJwtError::InvalidDigest(UnexpectedValue {
324 expected: Some(digest.into()),
325 found: sd_hash.into(),
326 }));
327 }
328
329 if let Some(nonce) = options.nonce.as_deref() {
330 if nonce != kb_jwt.claims().nonce {
331 return Err(KeyBindingJwtError::InvalidNonce(UnexpectedValue {
332 expected: Some(nonce.to_owned().into()),
333 found: kb_jwt.claims().nonce.clone().into(),
334 }));
335 }
336 }
337
338 if let Some(aud) = options.aud.as_deref() {
339 if aud != kb_jwt.claims().aud {
340 return Err(KeyBindingJwtError::AudienceMismatch(UnexpectedValue {
341 expected: Some(aud.to_owned().into()),
342 found: kb_jwt.claims().aud.clone().into(),
343 }));
344 }
345 }
346
347 let issuance_date = Timestamp::from_unix(kb_jwt.claims().iat)
348 .map_err(|_| KeyBindingJwtError::IssuanceDate("deserialization of `iat` failed".to_string()))?;
349
350 if let Some(earliest_issuance_date) = options.earliest_issuance_date {
351 if issuance_date < earliest_issuance_date {
352 return Err(KeyBindingJwtError::IssuanceDate(
353 "value is earlier than `earliest_issuance_date`".to_string(),
354 ));
355 }
356 }
357
358 if let Some(latest_issuance_date) = options.latest_issuance_date {
359 if issuance_date > latest_issuance_date {
360 return Err(KeyBindingJwtError::IssuanceDate(
361 "value is later than `latest_issuance_date`".to_string(),
362 ));
363 }
364 } else if issuance_date > Timestamp::now_utc() {
365 return Err(KeyBindingJwtError::IssuanceDate("value is in the future".to_string()));
366 }
367
368 Ok(())
369 }
370}