1use core::time::Duration;
40use std::collections::BTreeSet;
41
42use chrono::{DateTime, Utc};
43
44use super::{
45 Confirmation, SdJwt, SdJwtCredentialV2, SdJwtCredentialV2Error, SdJwtVc, SdJwtVcError,
46};
47use crate::did::Uri;
48use crate::jose::{DefaultVerifier, Jwk, JwsVerifier};
49use crate::resolver::{KeyReference, KeyReferenceError, KeyResolver, KeyRole};
50use crate::vc::validation::{
51 CredentialStatus, StatusCheck, StatusPolicy, TimeOptions, ValidationError, check_statuses,
52};
53use crate::vc::{CredentialError, JwtClaims};
54
55#[cfg(feature = "did-web")]
57#[cfg_attr(docsrs, doc(cfg(feature = "did-web")))]
58#[deprecated(since = "0.3.0", note = "moved to `status_list::StatusListCheck`")]
59pub type StatusListCheck<C, R, V = DefaultVerifier> = crate::status_list::StatusListCheck<C, R, V>;
60
61#[derive(Debug, Clone, PartialEq, Eq)]
67#[non_exhaustive]
68pub struct KeyBindingOptions {
69 pub audience: String,
71 pub nonce: String,
73 pub time: TimeOptions,
75 pub max_age: Option<Duration>,
77}
78
79impl KeyBindingOptions {
80 pub fn new(audience: impl Into<String>, nonce: impl Into<String>) -> Self {
83 Self {
84 audience: audience.into(),
85 nonce: nonce.into(),
86 time: TimeOptions::default(),
87 max_age: None,
88 }
89 }
90
91 #[must_use]
93 pub fn with_time(mut self, time: TimeOptions) -> Self {
94 self.time = time;
95 self
96 }
97
98 #[must_use]
100 pub fn with_max_age(mut self, max_age: Duration) -> Self {
101 self.max_age = Some(max_age);
102 self
103 }
104}
105
106#[derive(Debug, Clone, Default, PartialEq, Eq)]
108#[non_exhaustive]
109pub struct SdJwtValidationOptions {
110 pub time: TimeOptions,
112 pub key_binding: Option<KeyBindingOptions>,
114 pub status: StatusPolicy,
116 pub reject_legacy_typ: bool,
118 pub additional_typ: BTreeSet<String>,
122}
123
124impl SdJwtValidationOptions {
125 pub fn new() -> Self {
128 Self::default()
129 }
130
131 #[must_use]
133 pub fn with_time(mut self, time: TimeOptions) -> Self {
134 self.time = time;
135 self
136 }
137
138 #[must_use]
140 pub fn with_key_binding(mut self, key_binding: KeyBindingOptions) -> Self {
141 self.key_binding = Some(key_binding);
142 self
143 }
144
145 #[must_use]
147 pub fn with_status(mut self, status: StatusPolicy) -> Self {
148 self.status = status;
149 self
150 }
151
152 #[must_use]
154 pub fn rejecting_legacy_typ(mut self) -> Self {
155 self.reject_legacy_typ = true;
156 self
157 }
158
159 #[must_use]
161 pub fn with_additional_typ(mut self, typ: impl Into<String>) -> Self {
162 self.additional_typ.insert(typ.into());
163 self
164 }
165}
166
167#[derive(Debug, thiserror::Error)]
169#[non_exhaustive]
170pub enum SdJwtValidationError {
171 #[error(transparent)]
173 Credential(#[from] SdJwtVcError),
174 #[error(transparent)]
176 CredentialV2(#[from] SdJwtCredentialV2Error),
177 #[error("header `typ` is the pre-standard `vc+sd-jwt`")]
179 LegacyTyp,
180 #[error(transparent)]
183 Validation(#[from] ValidationError),
184 #[error("key binding is required, but the SD-JWT has no key binding JWT")]
186 KeyBindingRequired,
187 #[error("key binding is required, but `cnf` carries no usable key reference: {0}")]
190 NoHolderKey(KeyReferenceError),
191 #[error("key binding JWT issued at {} is older than {max_age:?} (now is {})", issued_at.to_rfc3339(), now.to_rfc3339())]
193 KeyBindingTooOld {
194 issued_at: DateTime<Utc>,
196 now: DateTime<Utc>,
198 max_age: Duration,
200 },
201 #[error("key binding `sd_hash` {found:?} is not the digest {expected:?} of the SD-JWT")]
203 SdHashMismatch {
204 expected: String,
206 found: Option<String>,
208 },
209}
210
211#[derive(Debug, Clone)]
213#[non_exhaustive]
214pub struct ValidatedSdJwtVc {
215 pub credential: SdJwtVc,
217 pub issuer_key: KeyReference,
219 pub key_binding: Option<JwtClaims>,
221 pub holder_key: Option<(KeyReference, Jwk)>,
225}
226
227#[derive(Debug, Clone)]
229#[non_exhaustive]
230pub struct ValidatedSdJwtCredentialV2 {
231 pub credential: SdJwtCredentialV2,
233 pub issuer_key: KeyReference,
235 pub key_binding: Option<JwtClaims>,
237 pub holder_key: Option<(KeyReference, Jwk)>,
240}
241
242pub struct SdJwtCredentialValidator<R, V = DefaultVerifier> {
244 resolver: R,
245 verifier: V,
246 status: Option<Box<dyn StatusCheck>>,
247}
248
249impl<R: KeyResolver, V: JwsVerifier> SdJwtCredentialValidator<R, V> {
250 pub fn new(resolver: R, verifier: V) -> Self {
253 Self {
254 resolver,
255 verifier,
256 status: None,
257 }
258 }
259
260 #[must_use]
263 pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
264 self.status = Some(Box::new(check));
265 self
266 }
267
268 pub fn resolver(&self) -> &R {
270 &self.resolver
271 }
272
273 pub fn verifier(&self) -> &V {
275 &self.verifier
276 }
277
278 pub fn has_status_check(&self) -> bool {
280 self.status.is_some()
281 }
282
283 pub async fn validate(
290 &self,
291 sd_jwt: &SdJwt,
292 options: &SdJwtValidationOptions,
293 ) -> Result<ValidatedSdJwtVc, SdJwtValidationError> {
294 let credential = SdJwtVc::from_sd_jwt(sd_jwt.clone())?;
295 self.validate_credential(credential, options).await
296 }
297
298 pub async fn validate_credential(
304 &self,
305 credential: SdJwtVc,
306 options: &SdJwtValidationOptions,
307 ) -> Result<ValidatedSdJwtVc, SdJwtValidationError> {
308 if options.reject_legacy_typ && credential.has_legacy_typ() {
309 return Err(SdJwtValidationError::LegacyTyp);
310 }
311 let claims = credential.claims();
312 let issuer = claims
313 .iss
314 .as_deref()
315 .ok_or(CredentialError::MissingClaim("iss"))
316 .map_err(ValidationError::from)?;
317 let (issuer_key, key) = self
318 .verify_issuer_signature(credential.sd_jwt(), issuer)
319 .await?;
320 check_issuer(issuer, &issuer_key, &key)?;
321
322 let mut time_claims = JwtClaims::new();
323 time_claims.nbf = claims.nbf;
324 time_claims.exp = claims.exp;
325 time_claims.iat = claims.iat;
326 options.time.check_claims(&time_claims)?;
327
328 let (key_binding, holder_key) = match &options.key_binding {
329 Some(required) => {
330 let (claims, holder_key) = self
331 .check_key_binding(credential.sd_jwt(), credential.confirmation(), required)
332 .await?;
333 (Some(claims), Some(holder_key))
334 }
335 None => (None, None),
336 };
337
338 self.check_status(
339 claims
340 .status
341 .iter()
342 .map(CredentialStatus::StatusClaim)
343 .collect(),
344 options,
345 )
346 .await?;
347
348 Ok(ValidatedSdJwtVc {
349 credential,
350 issuer_key,
351 key_binding,
352 holder_key,
353 })
354 }
355
356 pub async fn validate_v2(
363 &self,
364 sd_jwt: &SdJwt,
365 options: &SdJwtValidationOptions,
366 ) -> Result<ValidatedSdJwtCredentialV2, SdJwtValidationError> {
367 let credential =
368 SdJwtCredentialV2::from_sd_jwt_with(sd_jwt.clone(), &options.additional_typ)?;
369 self.validate_credential_v2(credential, options).await
370 }
371
372 pub async fn validate_credential_v2(
378 &self,
379 credential: SdJwtCredentialV2,
380 options: &SdJwtValidationOptions,
381 ) -> Result<ValidatedSdJwtCredentialV2, SdJwtValidationError> {
382 let validated = self
383 .validate_credential_v2_unchecked_status(credential, options)
384 .await?;
385 let credential = &validated.credential;
386 self.check_status(
387 credential
388 .credential()
389 .statuses()
390 .map(CredentialStatus::Vcdm)
391 .chain(credential.status().map(CredentialStatus::StatusClaim))
392 .collect(),
393 options,
394 )
395 .await?;
396 Ok(validated)
397 }
398
399 pub(crate) async fn validate_credential_v2_unchecked_status(
402 &self,
403 credential: SdJwtCredentialV2,
404 options: &SdJwtValidationOptions,
405 ) -> Result<ValidatedSdJwtCredentialV2, SdJwtValidationError> {
406 let vc = credential.credential();
407 let issuer = vc.issuer.id().as_str();
408 let (issuer_key, key) = self
409 .verify_issuer_signature(credential.sd_jwt(), issuer)
410 .await?;
411 check_issuer(issuer, &issuer_key, &key)?;
412 options.time.check_claims(credential.claims())?;
413 options.time.check(vc.valid_from, vc.valid_until, None)?;
414 let (key_binding, holder_key) = match &options.key_binding {
415 Some(required) => {
416 let (claims, holder_key) = self
417 .check_key_binding(credential.sd_jwt(), credential.confirmation(), required)
418 .await?;
419 (Some(claims), Some(holder_key))
420 }
421 None => (None, None),
422 };
423 Ok(ValidatedSdJwtCredentialV2 {
424 credential,
425 issuer_key,
426 key_binding,
427 holder_key,
428 })
429 }
430
431 async fn verify_issuer_signature(
434 &self,
435 sd_jwt: &SdJwt,
436 issuer: &str,
437 ) -> Result<(KeyReference, Jwk), ValidationError> {
438 let jws = sd_jwt.jwt().decode()?;
439 let reference = KeyReference::from_jws(&jws, Some(issuer))?;
440 let key = self
441 .resolver
442 .resolve_key(&reference, KeyRole::Issuer)
443 .await
444 .map_err(|source| ValidationError::KeyResolution {
445 key: Box::new(reference.clone()),
446 source,
447 })?;
448 jws.verify(&self.verifier, &key)?;
449 Ok((reference, key))
450 }
451
452 async fn holder_key(
455 &self,
456 cnf: Option<&Confirmation>,
457 ) -> Result<(KeyReference, Jwk), SdJwtValidationError> {
458 let cnf = cnf.ok_or(SdJwtValidationError::NoHolderKey(
459 KeyReferenceError::Missing,
460 ))?;
461 let reference =
462 KeyReference::from_confirmation(cnf).map_err(SdJwtValidationError::NoHolderKey)?;
463 let key = match &reference {
464 KeyReference::Jwk(jwk) => jwk.public(),
465 reference => self
466 .resolver
467 .resolve_key(reference, KeyRole::Subject)
468 .await
469 .map_err(|source| ValidationError::KeyResolution {
470 key: Box::new(reference.clone()),
471 source,
472 })?,
473 };
474 Ok((reference, key))
475 }
476
477 async fn check_key_binding(
480 &self,
481 sd_jwt: &SdJwt,
482 cnf: Option<&Confirmation>,
483 required: &KeyBindingOptions,
484 ) -> Result<(JwtClaims, (KeyReference, Jwk)), SdJwtValidationError> {
485 let kb_jwt = sd_jwt
486 .key_binding_jwt()
487 .ok_or(SdJwtValidationError::KeyBindingRequired)?;
488 let holder_key = self.holder_key(cnf).await?;
489 let jws = kb_jwt.decode().map_err(ValidationError::from)?;
490 match jws.header().typ.as_deref() {
491 Some(super::KB_JWT_TYP) => {}
492 found => {
493 return Err(ValidationError::UnexpectedTyp {
494 expected: super::KB_JWT_TYP,
495 found: found.unwrap_or("").to_owned(),
496 }
497 .into());
498 }
499 }
500 jws.verify(&self.verifier, &holder_key.1)
501 .map_err(ValidationError::from)?;
502 let claims: JwtClaims =
503 serde_json::from_slice(jws.payload()).map_err(ValidationError::Payload)?;
504
505 if !claims.audiences().any(|aud| aud == required.audience) {
506 return Err(ValidationError::AudienceMismatch {
507 expected: required.audience.clone(),
508 found: claims.aud.clone(),
509 }
510 .into());
511 }
512 let nonce = claims.additional.get("nonce").and_then(|v| v.as_str());
513 if nonce != Some(required.nonce.as_str()) {
514 return Err(ValidationError::NonceMismatch {
515 expected: required.nonce.clone(),
516 found: nonce.map(str::to_owned),
517 }
518 .into());
519 }
520 let iat = claims
521 .iat
522 .ok_or(CredentialError::MissingClaim("iat"))
523 .map_err(ValidationError::from)?;
524 let issued_at = JwtClaims::timestamp(iat).ok_or_else(|| {
525 ValidationError::from(CredentialError::InvalidClaim {
526 claim: "iat",
527 reason: format!("{iat} is out of range"),
528 })
529 })?;
530 required.time.check(None, None, Some(issued_at))?;
531 if let Some(max_age) = required.max_age {
532 let now = required.time.now();
533 let oldest = chrono::TimeDelta::from_std(max_age)
535 .ok()
536 .and_then(|delta| now.checked_sub_signed(delta))
537 .unwrap_or(DateTime::<Utc>::MIN_UTC);
538 if issued_at < oldest {
539 return Err(SdJwtValidationError::KeyBindingTooOld {
540 issued_at,
541 now,
542 max_age,
543 });
544 }
545 }
546 let alg = sd_jwt.hash_algorithm().map_err(SdJwtVcError::from)?;
547 let expected = sd_jwt.sd_hash(alg);
548 let found = claims.additional.get("sd_hash").and_then(|v| v.as_str());
549 if found != Some(expected.as_str()) {
550 return Err(SdJwtValidationError::SdHashMismatch {
551 expected,
552 found: found.map(str::to_owned),
553 });
554 }
555 Ok((claims, holder_key))
556 }
557
558 async fn check_status(
561 &self,
562 statuses: Vec<CredentialStatus<'_>>,
563 options: &SdJwtValidationOptions,
564 ) -> Result<(), SdJwtValidationError> {
565 Ok(check_statuses(
566 self.status.as_deref(),
567 statuses,
568 options.status,
569 options.time.now(),
570 )
571 .await?)
572 }
573}
574
575fn check_issuer(issuer: &str, reference: &KeyReference, key: &Jwk) -> Result<(), ValidationError> {
578 if !reference.binds_to(Some(issuer), key) {
579 return Err(ValidationError::IssuerMismatch {
580 issuer: Uri::parse(issuer).map_err(|e| CredentialError::InvalidClaim {
581 claim: "iss",
582 reason: e.to_string(),
583 })?,
584 key: Box::new(reference.clone()),
585 });
586 }
587 Ok(())
588}
589
590impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for SdJwtCredentialValidator<R, V> {
591 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
592 f.debug_struct("SdJwtCredentialValidator")
593 .field("resolver", &self.resolver)
594 .field("verifier", &self.verifier)
595 .field("status", &self.status.as_ref().map(|_| "StatusCheck"))
596 .finish()
597 }
598}