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")]
56pub use status_list::StatusListCheck;
57
58#[derive(Debug, Clone, PartialEq, Eq)]
64#[non_exhaustive]
65pub struct KeyBindingOptions {
66 pub audience: String,
68 pub nonce: String,
70 pub time: TimeOptions,
72 pub max_age: Option<Duration>,
74}
75
76impl KeyBindingOptions {
77 pub fn new(audience: impl Into<String>, nonce: impl Into<String>) -> Self {
80 Self {
81 audience: audience.into(),
82 nonce: nonce.into(),
83 time: TimeOptions::default(),
84 max_age: None,
85 }
86 }
87
88 #[must_use]
90 pub fn with_time(mut self, time: TimeOptions) -> Self {
91 self.time = time;
92 self
93 }
94
95 #[must_use]
97 pub fn with_max_age(mut self, max_age: Duration) -> Self {
98 self.max_age = Some(max_age);
99 self
100 }
101}
102
103#[derive(Debug, Clone, Default, PartialEq, Eq)]
105#[non_exhaustive]
106pub struct SdJwtValidationOptions {
107 pub time: TimeOptions,
109 pub key_binding: Option<KeyBindingOptions>,
111 pub status: StatusPolicy,
113 pub reject_legacy_typ: bool,
115 pub additional_typ: BTreeSet<String>,
119}
120
121impl SdJwtValidationOptions {
122 pub fn new() -> Self {
125 Self::default()
126 }
127
128 #[must_use]
130 pub fn with_time(mut self, time: TimeOptions) -> Self {
131 self.time = time;
132 self
133 }
134
135 #[must_use]
137 pub fn with_key_binding(mut self, key_binding: KeyBindingOptions) -> Self {
138 self.key_binding = Some(key_binding);
139 self
140 }
141
142 #[must_use]
144 pub fn with_status(mut self, status: StatusPolicy) -> Self {
145 self.status = status;
146 self
147 }
148
149 #[must_use]
151 pub fn rejecting_legacy_typ(mut self) -> Self {
152 self.reject_legacy_typ = true;
153 self
154 }
155
156 #[must_use]
158 pub fn with_additional_typ(mut self, typ: impl Into<String>) -> Self {
159 self.additional_typ.insert(typ.into());
160 self
161 }
162}
163
164#[derive(Debug, thiserror::Error)]
166#[non_exhaustive]
167pub enum SdJwtValidationError {
168 #[error(transparent)]
170 Credential(#[from] SdJwtVcError),
171 #[error(transparent)]
173 CredentialV2(#[from] SdJwtCredentialV2Error),
174 #[error("header `typ` is the pre-standard `vc+sd-jwt`")]
176 LegacyTyp,
177 #[error(transparent)]
180 Validation(#[from] ValidationError),
181 #[error("key binding is required, but the SD-JWT has no key binding JWT")]
183 KeyBindingRequired,
184 #[error("key binding is required, but `cnf` carries no usable key reference: {0}")]
187 NoHolderKey(KeyReferenceError),
188 #[error("key binding JWT issued at {} is older than {max_age:?} (now is {})", issued_at.to_rfc3339(), now.to_rfc3339())]
190 KeyBindingTooOld {
191 issued_at: DateTime<Utc>,
193 now: DateTime<Utc>,
195 max_age: Duration,
197 },
198 #[error("key binding `sd_hash` {found:?} is not the digest {expected:?} of the SD-JWT")]
200 SdHashMismatch {
201 expected: String,
203 found: Option<String>,
205 },
206}
207
208#[derive(Debug, Clone)]
210#[non_exhaustive]
211pub struct ValidatedSdJwtVc {
212 pub credential: SdJwtVc,
214 pub issuer_key: KeyReference,
216 pub key_binding: Option<JwtClaims>,
218 pub holder_key: Option<(KeyReference, Jwk)>,
222}
223
224#[derive(Debug, Clone)]
226#[non_exhaustive]
227pub struct ValidatedSdJwtCredentialV2 {
228 pub credential: SdJwtCredentialV2,
230 pub issuer_key: KeyReference,
232 pub key_binding: Option<JwtClaims>,
234 pub holder_key: Option<(KeyReference, Jwk)>,
237}
238
239pub struct SdJwtCredentialValidator<R, V = DefaultVerifier> {
241 resolver: R,
242 verifier: V,
243 status: Option<Box<dyn StatusCheck>>,
244}
245
246impl<R: KeyResolver, V: JwsVerifier> SdJwtCredentialValidator<R, V> {
247 pub fn new(resolver: R, verifier: V) -> Self {
250 Self {
251 resolver,
252 verifier,
253 status: None,
254 }
255 }
256
257 #[must_use]
260 pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
261 self.status = Some(Box::new(check));
262 self
263 }
264
265 pub fn resolver(&self) -> &R {
267 &self.resolver
268 }
269
270 pub fn verifier(&self) -> &V {
272 &self.verifier
273 }
274
275 pub fn has_status_check(&self) -> bool {
277 self.status.is_some()
278 }
279
280 pub async fn validate(
287 &self,
288 sd_jwt: &SdJwt,
289 options: &SdJwtValidationOptions,
290 ) -> Result<ValidatedSdJwtVc, SdJwtValidationError> {
291 let credential = SdJwtVc::from_sd_jwt(sd_jwt.clone())?;
292 self.validate_credential(credential, options).await
293 }
294
295 pub async fn validate_credential(
301 &self,
302 credential: SdJwtVc,
303 options: &SdJwtValidationOptions,
304 ) -> Result<ValidatedSdJwtVc, SdJwtValidationError> {
305 if options.reject_legacy_typ && credential.has_legacy_typ() {
306 return Err(SdJwtValidationError::LegacyTyp);
307 }
308 let claims = credential.claims();
309 let issuer = claims
310 .iss
311 .as_deref()
312 .ok_or(CredentialError::MissingClaim("iss"))
313 .map_err(ValidationError::from)?;
314 let (issuer_key, key) = self
315 .verify_issuer_signature(credential.sd_jwt(), issuer)
316 .await?;
317 check_issuer(issuer, &issuer_key, &key)?;
318
319 let mut time_claims = JwtClaims::new();
320 time_claims.nbf = claims.nbf;
321 time_claims.exp = claims.exp;
322 time_claims.iat = claims.iat;
323 options.time.check_claims(&time_claims)?;
324
325 let (key_binding, holder_key) = match &options.key_binding {
326 Some(required) => {
327 let (claims, holder_key) = self
328 .check_key_binding(credential.sd_jwt(), credential.confirmation(), required)
329 .await?;
330 (Some(claims), Some(holder_key))
331 }
332 None => (None, None),
333 };
334
335 self.check_status(claims.status.iter().map(CredentialStatus::SdJwt), options)
336 .await?;
337
338 Ok(ValidatedSdJwtVc {
339 credential,
340 issuer_key,
341 key_binding,
342 holder_key,
343 })
344 }
345
346 pub async fn validate_v2(
353 &self,
354 sd_jwt: &SdJwt,
355 options: &SdJwtValidationOptions,
356 ) -> Result<ValidatedSdJwtCredentialV2, SdJwtValidationError> {
357 let credential =
358 SdJwtCredentialV2::from_sd_jwt_with(sd_jwt.clone(), &options.additional_typ)?;
359 self.validate_credential_v2(credential, options).await
360 }
361
362 pub async fn validate_credential_v2(
368 &self,
369 credential: SdJwtCredentialV2,
370 options: &SdJwtValidationOptions,
371 ) -> Result<ValidatedSdJwtCredentialV2, SdJwtValidationError> {
372 let validated = self
373 .validate_credential_v2_unchecked_status(credential, options)
374 .await?;
375 let credential = &validated.credential;
376 self.check_status(
377 credential
378 .credential()
379 .statuses()
380 .map(CredentialStatus::Vcdm)
381 .chain(credential.status().map(CredentialStatus::SdJwt)),
382 options,
383 )
384 .await?;
385 Ok(validated)
386 }
387
388 pub(crate) async fn validate_credential_v2_unchecked_status(
391 &self,
392 credential: SdJwtCredentialV2,
393 options: &SdJwtValidationOptions,
394 ) -> Result<ValidatedSdJwtCredentialV2, SdJwtValidationError> {
395 let vc = credential.credential();
396 let issuer = vc.issuer.id().as_str();
397 let (issuer_key, key) = self
398 .verify_issuer_signature(credential.sd_jwt(), issuer)
399 .await?;
400 check_issuer(issuer, &issuer_key, &key)?;
401 options.time.check_claims(credential.claims())?;
402 options.time.check(vc.valid_from, vc.valid_until, None)?;
403 let (key_binding, holder_key) = match &options.key_binding {
404 Some(required) => {
405 let (claims, holder_key) = self
406 .check_key_binding(credential.sd_jwt(), credential.confirmation(), required)
407 .await?;
408 (Some(claims), Some(holder_key))
409 }
410 None => (None, None),
411 };
412 Ok(ValidatedSdJwtCredentialV2 {
413 credential,
414 issuer_key,
415 key_binding,
416 holder_key,
417 })
418 }
419
420 async fn verify_issuer_signature(
423 &self,
424 sd_jwt: &SdJwt,
425 issuer: &str,
426 ) -> Result<(KeyReference, Jwk), ValidationError> {
427 let jws = sd_jwt.jwt().decode()?;
428 let reference = KeyReference::from_jws(&jws, Some(issuer))?;
429 let key = self
430 .resolver
431 .resolve_key(&reference, KeyRole::Issuer)
432 .await
433 .map_err(|source| ValidationError::KeyResolution {
434 key: Box::new(reference.clone()),
435 source,
436 })?;
437 jws.verify(&self.verifier, &key)?;
438 Ok((reference, key))
439 }
440
441 async fn holder_key(
444 &self,
445 cnf: Option<&Confirmation>,
446 ) -> Result<(KeyReference, Jwk), SdJwtValidationError> {
447 let cnf = cnf.ok_or(SdJwtValidationError::NoHolderKey(
448 KeyReferenceError::Missing,
449 ))?;
450 let reference =
451 KeyReference::from_confirmation(cnf).map_err(SdJwtValidationError::NoHolderKey)?;
452 let key = match &reference {
453 KeyReference::Jwk(jwk) => jwk.public(),
454 reference => self
455 .resolver
456 .resolve_key(reference, KeyRole::Subject)
457 .await
458 .map_err(|source| ValidationError::KeyResolution {
459 key: Box::new(reference.clone()),
460 source,
461 })?,
462 };
463 Ok((reference, key))
464 }
465
466 async fn check_key_binding(
469 &self,
470 sd_jwt: &SdJwt,
471 cnf: Option<&Confirmation>,
472 required: &KeyBindingOptions,
473 ) -> Result<(JwtClaims, (KeyReference, Jwk)), SdJwtValidationError> {
474 let kb_jwt = sd_jwt
475 .key_binding_jwt()
476 .ok_or(SdJwtValidationError::KeyBindingRequired)?;
477 let holder_key = self.holder_key(cnf).await?;
478 let jws = kb_jwt.decode().map_err(ValidationError::from)?;
479 match jws.header().typ.as_deref() {
480 Some(super::KB_JWT_TYP) => {}
481 found => {
482 return Err(ValidationError::UnexpectedTyp {
483 expected: super::KB_JWT_TYP,
484 found: found.unwrap_or("").to_owned(),
485 }
486 .into());
487 }
488 }
489 jws.verify(&self.verifier, &holder_key.1)
490 .map_err(ValidationError::from)?;
491 let claims: JwtClaims = serde_json::from_slice(jws.payload())
492 .map_err(|e| ValidationError::Payload(e.to_string()))?;
493
494 if !claims.audiences().any(|aud| aud == required.audience) {
495 return Err(ValidationError::AudienceMismatch {
496 expected: required.audience.clone(),
497 found: claims.aud.clone(),
498 }
499 .into());
500 }
501 let nonce = claims.additional.get("nonce").and_then(|v| v.as_str());
502 if nonce != Some(required.nonce.as_str()) {
503 return Err(ValidationError::NonceMismatch {
504 expected: required.nonce.clone(),
505 found: nonce.map(str::to_owned),
506 }
507 .into());
508 }
509 let iat = claims
510 .iat
511 .ok_or(CredentialError::MissingClaim("iat"))
512 .map_err(ValidationError::from)?;
513 let issued_at = JwtClaims::timestamp(iat).ok_or_else(|| {
514 ValidationError::from(CredentialError::InvalidClaim {
515 claim: "iat",
516 reason: format!("{iat} is out of range"),
517 })
518 })?;
519 required.time.check(None, None, Some(issued_at))?;
520 if let Some(max_age) = required.max_age {
521 let now = required.time.now();
522 let oldest = chrono::TimeDelta::from_std(max_age)
524 .ok()
525 .and_then(|delta| now.checked_sub_signed(delta))
526 .unwrap_or(DateTime::<Utc>::MIN_UTC);
527 if issued_at < oldest {
528 return Err(SdJwtValidationError::KeyBindingTooOld {
529 issued_at,
530 now,
531 max_age,
532 });
533 }
534 }
535 let alg = sd_jwt.hash_algorithm().map_err(SdJwtVcError::from)?;
536 let expected = sd_jwt.sd_hash(alg);
537 let found = claims.additional.get("sd_hash").and_then(|v| v.as_str());
538 if found != Some(expected.as_str()) {
539 return Err(SdJwtValidationError::SdHashMismatch {
540 expected,
541 found: found.map(str::to_owned),
542 });
543 }
544 Ok((claims, holder_key))
545 }
546
547 async fn check_status<'a>(
550 &self,
551 statuses: impl IntoIterator<Item = CredentialStatus<'a>>,
552 options: &SdJwtValidationOptions,
553 ) -> Result<(), SdJwtValidationError> {
554 Ok(check_statuses(
555 self.status.as_deref(),
556 statuses,
557 options.status,
558 options.time.now(),
559 )
560 .await?)
561 }
562}
563
564fn check_issuer(issuer: &str, reference: &KeyReference, key: &Jwk) -> Result<(), ValidationError> {
567 if !reference.binds_to(Some(issuer), key) {
568 return Err(ValidationError::IssuerMismatch {
569 issuer: Uri::parse(issuer).map_err(|e| CredentialError::InvalidClaim {
570 claim: "iss",
571 reason: e.to_string(),
572 })?,
573 key: Box::new(reference.clone()),
574 });
575 }
576 Ok(())
577}
578
579impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for SdJwtCredentialValidator<R, V> {
580 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
581 f.debug_struct("SdJwtCredentialValidator")
582 .field("resolver", &self.resolver)
583 .field("verifier", &self.verifier)
584 .field("status", &self.status.as_ref().map(|_| "StatusCheck"))
585 .finish()
586 }
587}
588
589#[cfg(feature = "did-web")]
590mod status_list {
591 use chrono::{DateTime, Utc};
592 use url::Url;
593
594 use crate::jose::{DefaultVerifier, JwsVerifier};
595 use crate::methods::web::HttpClient;
596 use crate::resolver::{KeyReference, KeyResolver, KeyRole};
597 use crate::sd_jwt_vc::{StatusListToken, StatusMechanism};
598 use crate::vc::Jwt;
599 use crate::vc::validation::{CredentialStatus, StatusCheck, StatusError};
600
601 pub struct StatusListCheck<C, R, V = DefaultVerifier> {
619 client: C,
620 resolver: R,
621 verifier: V,
622 }
623
624 impl<C: HttpClient, R: KeyResolver, V: JwsVerifier> StatusListCheck<C, R, V> {
625 pub fn new(client: C, resolver: R, verifier: V) -> Self {
628 Self {
629 client,
630 resolver,
631 verifier,
632 }
633 }
634
635 async fn fetch(&self, uri: &str) -> Result<StatusListToken, StatusError> {
637 let url = Url::parse(uri)
638 .map_err(|e| StatusError::Unavailable(format!("`uri` {uri:?}: {e}")))?;
639 let response = self
640 .client
641 .get(&url)
642 .await
643 .map_err(|e| StatusError::Unavailable(format!("fetching {uri}: {e}")))?;
644 if !(200..300).contains(&response.status) {
645 return Err(StatusError::Unavailable(format!(
646 "fetching {uri}: HTTP {}",
647 response.status
648 )));
649 }
650 let body = String::from_utf8(response.body)
651 .map_err(|_| StatusError::Unavailable(format!("{uri}: body is not UTF-8")))?;
652 let jwt = Jwt::new(body.trim())
653 .map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
654 let (_, token) = StatusListToken::decode(&jwt)
655 .map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
656 let jws = jwt
657 .decode()
658 .map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
659 let iss = token.additional.get("iss").and_then(|iss| iss.as_str());
660 let reference = KeyReference::from_jws(&jws, iss)
661 .map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
662 let key = self
663 .resolver
664 .resolve_key(&reference, KeyRole::Issuer)
665 .await
666 .map_err(|e| {
667 StatusError::Unavailable(format!("{uri}: resolving key {reference}: {e}"))
668 })?;
669 jws.verify(&self.verifier, &key)
670 .map_err(|e| StatusError::Unavailable(format!("{uri}: signature: {e}")))?;
671 let bound = match iss {
672 Some(iss) => reference.binds_to(Some(iss), &key),
673 None => matches!(reference, KeyReference::DidUrl(_)),
674 };
675 if !bound {
676 return Err(StatusError::Unavailable(format!(
677 "{uri}: key {reference} does not belong to the issuer {iss:?}"
678 )));
679 }
680 Ok(token)
681 }
682 }
683
684 #[async_trait::async_trait]
685 impl<C: HttpClient, R: KeyResolver, V: JwsVerifier> StatusCheck for StatusListCheck<C, R, V> {
686 async fn check(
689 &self,
690 status: CredentialStatus<'_>,
691 now: DateTime<Utc>,
692 ) -> Result<(), StatusError> {
693 let CredentialStatus::SdJwt(status) = status else {
694 return Err(StatusError::Unsupported(status.mechanism()));
695 };
696 let Some(reference) = &status.status_list else {
697 let names: Vec<&str> = status
698 .mechanisms()
699 .filter_map(|m| match m {
700 StatusMechanism::Other { name, .. } => Some(name),
701 _ => None,
702 })
703 .collect();
704 return Err(StatusError::Unsupported(names.join(", ")));
705 };
706 let token = self.fetch(&reference.uri).await?;
707 let status_type = token
708 .status_of(reference, now)
709 .map_err(|e| StatusError::Unavailable(e.to_string()))?;
710 if status_type.is_valid() {
711 Ok(())
712 } else {
713 Err(StatusError::Invalid(status_type.to_string()))
714 }
715 }
716 }
717
718 impl<C: core::fmt::Debug, R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug
719 for StatusListCheck<C, R, V>
720 {
721 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
722 f.debug_struct("StatusListCheck")
723 .field("client", &self.client)
724 .field("resolver", &self.resolver)
725 .field("verifier", &self.verifier)
726 .finish()
727 }
728 }
729}