Skip to main content

identity_kit/sd_jwt_vc/
validation.rs

1//! Validation of SD-JWT credentials: issuer signature and binding, time claims, key binding,
2//! status.
3//!
4//! [`SdJwtCredentialValidator`] is the SD-JWT counterpart of
5//! [`JwtCredentialValidator`](crate::vc::validation::JwtCredentialValidator), over the same
6//! [`KeyResolver`] and [`JwsVerifier`]. It validates both SD-JWT credential types
7//! ([two SD-JWT credential types](super#two-sd-jwt-credential-types)):
8//! [`validate`](SdJwtCredentialValidator::validate) an SD-JWT VC,
9//! [`validate_v2`](SdJwtCredentialValidator::validate_v2) a VCDM 2.0 credential secured with
10//! SD-JWT. Each checks, in order:
11//!
12//! 1. The SD-JWT parses as an [`SdJwtVc`] (`typ`, disclosures, claims; RFC 9901 §7.1 steps 3
13//!    to 5, SD-JWT VC §2.2) or as an [`SdJwtCredentialV2`] (`typ` `vc+sd-jwt` when present,
14//!    or one of `options.additional_typ`; disclosures; the payload as a
15//!    [`CredentialV2`](crate::vc::CredentialV2) with its registered claims agreeing with it).
16//! 2. The header of the issuer-signed JWT names its key ([`KeyReference::from_jws`], with the
17//!    issuer, `iss` or the credential's `issuer`, as `iss`): a `kid` that is a DID URL of the
18//!    form `did:…#fragment` (a path or DID parameters such as `versionId` are refused, as in
19//!    the JWT validators, ADR 0004), a `#fragment` relative to a DID issuer, a `kid` into the
20//!    JWT VC issuer metadata of an HTTPS issuer (SD-JWT VC §3.5, resolved by `JwksResolver`
21//!    with the `jwks` feature), or a `jwk`, `x5c` or `jku` header. The key is resolved for
22//!    [`KeyRole::Issuer`], verifies the signature, and belongs to the issuer
23//!    ([`KeyReference::binds_to`]).
24//! 3. `nbf`, `exp` and `iat` hold at the validation time, and so do `validFrom` and
25//!    `validUntil` of a VCDM 2.0 credential.
26//! 4. When the options require key binding (RFC 9901 §7.3): the SD-JWT+KB carries a key
27//!    binding JWT with `typ: kb+jwt`, signed by the holder's key, for the expected audience
28//!    and nonce, issued within the window, with an `sd_hash` over the presented SD-JWT. The
29//!    holder's key is `cnf.jwk`, used as it is since the issuer signed it, or else the key
30//!    `cnf.kid` names, an absolute DID URL resolved for [`KeyRole::Subject`]
31//!    (`authentication`); see [`KeyReference::from_confirmation`].
32//! 5. `status` (as [`CredentialStatus::SdJwt`]) and the `credentialStatus` entries of a VCDM
33//!    2.0 credential (as [`CredentialStatus::Vcdm`]) pass the [`StatusCheck`], per the
34//!    [`StatusPolicy`].
35//!
36//! Type metadata is a separate step the application runs when its policy asks for it:
37//! [`TypeMetadata::check_claims`](super::TypeMetadata::check_claims).
38
39use 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/// What a key binding JWT must prove (RFC 9901 §7.3, step 5).
59///
60/// The key binding JWT must be signed by the holder's key the credential's `cnf` names: by
61/// value in `cnf.jwk`, or by reference in `cnf.kid`, an absolute DID URL the validator's
62/// [`KeyResolver`] resolves for [`KeyRole::Subject`] (`authentication`).
63#[derive(Debug, Clone, PartialEq, Eq)]
64#[non_exhaustive]
65pub struct KeyBindingOptions {
66    /// The audience the key binding JWT must name: this verifier.
67    pub audience: String,
68    /// The nonce it must carry: the one of this transaction.
69    pub nonce: String,
70    /// The time its `iat` is checked against, with leeway.
71    pub time: TimeOptions,
72    /// How old it may be, if bounded.
73    pub max_age: Option<Duration>,
74}
75
76impl KeyBindingOptions {
77    /// Key binding for `audience` with `nonce`, checked at the current time with no age
78    /// bound.
79    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    /// These options checking `iat` against `time`.
89    #[must_use]
90    pub fn with_time(mut self, time: TimeOptions) -> Self {
91        self.time = time;
92        self
93    }
94
95    /// These options rejecting a key binding JWT older than `max_age`.
96    #[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/// Options of [`SdJwtCredentialValidator`].
104#[derive(Debug, Clone, Default, PartialEq, Eq)]
105#[non_exhaustive]
106pub struct SdJwtValidationOptions {
107    /// The validation time and leeway of the issuer-signed JWT.
108    pub time: TimeOptions,
109    /// The key binding required, if any; a key binding JWT is not verified otherwise.
110    pub key_binding: Option<KeyBindingOptions>,
111    /// What to do about `status` (and `credentialStatus`).
112    pub status: StatusPolicy,
113    /// Whether to reject the pre-standard `typ: vc+sd-jwt` on an SD-JWT VC.
114    pub reject_legacy_typ: bool,
115    /// Header `typ` values accepted for a VCDM 2.0 credential besides `vc+sd-jwt`; none by
116    /// default. A token without `typ` is always accepted there. An SD-JWT VC's `typ` is fixed
117    /// by its specification and not affected.
118    pub additional_typ: BTreeSet<String>,
119}
120
121impl SdJwtValidationOptions {
122    /// Default options: validate now without leeway, no key binding required,
123    /// [`StatusPolicy::Strict`], `vc+sd-jwt` accepted.
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// These options with `time`.
129    #[must_use]
130    pub fn with_time(mut self, time: TimeOptions) -> Self {
131        self.time = time;
132        self
133    }
134
135    /// These options requiring `key_binding`.
136    #[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    /// These options with `status`.
143    #[must_use]
144    pub fn with_status(mut self, status: StatusPolicy) -> Self {
145        self.status = status;
146        self
147    }
148
149    /// These options rejecting `typ: vc+sd-jwt` on an SD-JWT VC.
150    #[must_use]
151    pub fn rejecting_legacy_typ(mut self) -> Self {
152        self.reject_legacy_typ = true;
153        self
154    }
155
156    /// These options also accepting a VCDM 2.0 credential whose header `typ` is `typ`.
157    #[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/// Why an SD-JWT credential did not validate.
165#[derive(Debug, thiserror::Error)]
166#[non_exhaustive]
167pub enum SdJwtValidationError {
168    /// The SD-JWT is not a well-formed SD-JWT VC.
169    #[error(transparent)]
170    Credential(#[from] SdJwtVcError),
171    /// The SD-JWT is not a well-formed VCDM 2.0 credential secured with SD-JWT.
172    #[error(transparent)]
173    CredentialV2(#[from] SdJwtCredentialV2Error),
174    /// The issuer-signed JWT has the pre-standard `typ: vc+sd-jwt`, which the options reject.
175    #[error("header `typ` is the pre-standard `vc+sd-jwt`")]
176    LegacyTyp,
177    /// A check shared with the JWT validators failed: `kid`, key resolution, signature,
178    /// issuer binding, time, audience, nonce, status.
179    #[error(transparent)]
180    Validation(#[from] ValidationError),
181    /// Key binding is required and the SD-JWT carries no key binding JWT.
182    #[error("key binding is required, but the SD-JWT has no key binding JWT")]
183    KeyBindingRequired,
184    /// Key binding is required and `cnf` names no key this crate can look for: there is no
185    /// `cnf`, or [`KeyReference::from_confirmation`] refuses it.
186    #[error("key binding is required, but `cnf` carries no usable key reference: {0}")]
187    NoHolderKey(KeyReferenceError),
188    /// The key binding JWT is older than allowed.
189    #[error("key binding JWT issued at {} is older than {max_age:?} (now is {})", issued_at.to_rfc3339(), now.to_rfc3339())]
190    KeyBindingTooOld {
191        /// Its `iat`.
192        issued_at: DateTime<Utc>,
193        /// The validation time.
194        now: DateTime<Utc>,
195        /// The bound.
196        max_age: Duration,
197    },
198    /// The key binding JWT's `sd_hash` is not the digest of the presented SD-JWT.
199    #[error("key binding `sd_hash` {found:?} is not the digest {expected:?} of the SD-JWT")]
200    SdHashMismatch {
201        /// The digest of the SD-JWT.
202        expected: String,
203        /// The `sd_hash` found.
204        found: Option<String>,
205    },
206}
207
208/// A validated SD-JWT VC.
209#[derive(Debug, Clone)]
210#[non_exhaustive]
211pub struct ValidatedSdJwtVc {
212    /// The credential, with the presented disclosures applied.
213    pub credential: SdJwtVc,
214    /// Where the issuer-signed JWT says its key is.
215    pub issuer_key: KeyReference,
216    /// The claims of the key binding JWT, when key binding was required and verified.
217    pub key_binding: Option<JwtClaims>,
218    /// The holder's key that verified the key binding JWT, and where `cnf` said it is (a DID
219    /// URL to compare with the presentation's holder or the credential subject, say), when key
220    /// binding was required and verified.
221    pub holder_key: Option<(KeyReference, Jwk)>,
222}
223
224/// A validated VCDM 2.0 credential secured with SD-JWT.
225#[derive(Debug, Clone)]
226#[non_exhaustive]
227pub struct ValidatedSdJwtCredentialV2 {
228    /// The credential, with the presented disclosures applied.
229    pub credential: SdJwtCredentialV2,
230    /// Where the issuer-signed JWT says its key is.
231    pub issuer_key: KeyReference,
232    /// The claims of the key binding JWT, when key binding was required and verified.
233    pub key_binding: Option<JwtClaims>,
234    /// The holder's key that verified the key binding JWT, and where `cnf` said it is, when key
235    /// binding was required and verified.
236    pub holder_key: Option<(KeyReference, Jwk)>,
237}
238
239/// Validates SD-JWT credentials; see the [module documentation](self) for the checks.
240pub 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    /// A validator resolving keys with `resolver` and verifying signatures with `verifier`,
248    /// without a status check.
249    pub fn new(resolver: R, verifier: V) -> Self {
250        Self {
251            resolver,
252            verifier,
253            status: None,
254        }
255    }
256
257    /// This validator checking the `status` claim and, for a VCDM 2.0 credential, the
258    /// `credentialStatus` entries with `check`.
259    #[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    /// The key resolver.
266    pub fn resolver(&self) -> &R {
267        &self.resolver
268    }
269
270    /// The signature verifier.
271    pub fn verifier(&self) -> &V {
272        &self.verifier
273    }
274
275    /// Whether a status check is configured.
276    pub fn has_status_check(&self) -> bool {
277        self.status.is_some()
278    }
279
280    /// Validates `sd_jwt` as an SD-JWT VC.
281    ///
282    /// # Errors
283    ///
284    /// The first [`SdJwtValidationError`] found, in the order the [module
285    /// documentation](self) lists the checks.
286    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    /// Validates the parsed `credential`.
296    ///
297    /// # Errors
298    ///
299    /// As [`validate`](Self::validate).
300    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    /// Validates `sd_jwt` as a VCDM 2.0 credential secured with SD-JWT (VC-JOSE-COSE §3.2.1).
347    ///
348    /// # Errors
349    ///
350    /// The first [`SdJwtValidationError`] found, in the order the [module
351    /// documentation](self) lists the checks.
352    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    /// Validates the parsed `credential`.
363    ///
364    /// # Errors
365    ///
366    /// As [`validate_v2`](Self::validate_v2).
367    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    /// Steps 2 to 4 of [`validate_credential_v2`](Self::validate_credential_v2): everything
389    /// but the status checks, which the presentation validator runs with its own check.
390    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    /// Resolves the key the issuer-signed JWT names, `issuer` being its issuer, for
421    /// [`KeyRole::Issuer`] and verifies the signature; the reference and the key.
422    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    /// The holder's key `cnf` names: `cnf.jwk` as it is, since the issuer signed it, or the
442    /// key a `cnf.kid` resolves to for [`KeyRole::Subject`].
443    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    /// RFC 9901 §7.3, step 5: the key binding JWT of `sd_jwt`, signed by the key `cnf` names;
467    /// its claims and that key.
468    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            // A `max_age` beyond what `chrono` represents bounds nothing.
523            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    /// Checks every status entry with this validator's check, per the policy, at the
548    /// validation time.
549    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
564/// Checks that `key`, which `reference` resolved to, belongs to `issuer`
565/// ([`KeyReference::binds_to`]).
566fn 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    /// A [`StatusCheck`] for the Token Status List: for the `status` claim of an SD-JWT
602    /// credential or of a JWT credential ([`CredentialStatus::SdJwt`]), fetches the status
603    /// list token from the referenced URI with an [`HttpClient`], verifies it was signed by
604    /// the key its header names ([`KeyReference::from_jws`] with the token's `iss`), resolved
605    /// for [`KeyRole::Issuer`], and reads the status at the referenced index (Token Status
606    /// List §8).
607    ///
608    /// When the token has an `iss`, the key must belong to it ([`KeyReference::binds_to`]);
609    /// without one, only a key named by a DID URL is accepted, since a key by value or a key
610    /// set could be anybody's. Beyond that the check trusts the key the reference resolves
611    /// to; whether that key may speak for the list is the application's policy to enforce
612    /// around it, by restricting the resolver, say.
613    ///
614    /// The status list URI comes from the credential, so the client fetches whatever an issuer
615    /// names: `client` must enforce the protections `ReqwestClient` (feature `reqwest`) does
616    /// (HTTPS only, no redirects, no non-public addresses after name resolution, a body limit
617    /// and a timeout), or the status check becomes a request forgery primitive.
618    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        /// A check fetching with `client`, resolving the status issuer's key with `resolver`
626        /// and verifying with `verifier`.
627        pub fn new(client: C, resolver: R, verifier: V) -> Self {
628            Self {
629                client,
630                resolver,
631                verifier,
632            }
633        }
634
635        /// The status list token at `uri`, fetched and verified.
636        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        /// Checks the Token Status List reference of a `status` claim; a `credentialStatus`
687        /// entry, or a `status` claim without one, is [`StatusError::Unsupported`].
688        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}