Skip to main content

identity_kit/vc/validation/
mod.rs

1//! Validation of credential and presentation JWTs: signature, binding to the issuer or holder,
2//! validity period, audience, nonce and status.
3//!
4//! The validators take two extension points: a [`KeyResolver`] (from
5//! [`resolver`](crate::resolver), re-exported here), which turns the [`KeyReference`] of a JWT
6//! into the public key of its signer, and a [`JwsVerifier`](crate::jose::JwsVerifier), which
7//! checks the signature and is [`DefaultVerifier`](crate::jose::DefaultVerifier) unless an
8//! application substitutes its own. The crate's [`Resolver`](crate::resolver::Resolver)
9//! resolves DID URLs; a [`CompositeKeyResolver`](crate::resolver::CompositeKeyResolver) adds
10//! keys by value ([`JwkResolver`](crate::resolver::JwkResolver)) or the key sets of HTTPS
11//! issuers (`JwksResolver`, feature `jwks`). A [`StatusCheck`] is a third, optional extension
12//! point, for `credentialStatus` and the `status` claim, shared with the SD-JWT validators.
13//!
14//! # What is checked
15//!
16//! For a credential ([`JwtCredentialValidator`]):
17//!
18//! 1. The token is a compact JWS whose header names its key
19//!    ([`KeyReference::from_jws`], with the issuer as `iss`): a `kid` that is a DID URL of the
20//!    form `did:…#fragment`, a `#fragment` relative to a DID issuer, a `kid` into the key set of
21//!    an HTTPS issuer, or a `jwk`, `x5c` or `jku` header. A path or DID parameters such as
22//!    `versionId` in a DID URL are refused, so the signer cannot pick the document version its
23//!    key is looked up in (ADR 0004). When `typ` is present it names the profile: `JWT` for
24//!    VCDM 1.1, `vc+jwt` for VCDM 2.0, or one of the values the options add
25//!    (`CredentialValidationOptions::additional_typ`).
26//! 2. The payload is a claims set that maps to a [`Credential`](crate::vc::Credential) (or a
27//!    [`CredentialV2`](crate::vc::CredentialV2)), with its registered claims agreeing with the
28//!    credential. A payload with `_sd` or `_sd_alg` as a member name anywhere, or an array
29//!    element `{"...": "<digest>"}`, is the issuer-signed JWT of an SD-JWT and is refused
30//!    ([`ValidationError::SdJwtPayload`]), typed or not: read as a plain JWT, its digests would
31//!    be properties and its concealed claims would look absent. The `typ` check alone does not
32//!    catch it, since an untyped token is accepted; `SdJwtCredentialValidator` of `sd_jwt_vc`
33//!    validates it.
34//! 3. The key the reference names is resolved for [`KeyRole::Issuer`] (`assertionMethod` in a
35//!    DID document) and verifies the signature.
36//! 4. The key belongs to the issuer ([`KeyReference::binds_to`]): the issuer is the DID of the
37//!    DID URL, the `did:jwk` or thumbprint of a key by value, or the HTTPS issuer of the key
38//!    set.
39//! 5. `nbf`, `exp` and `iat`, and the credential's own validity period, hold at the validation
40//!    time, within the leeway.
41//! 6. `credentialStatus` passes the [`StatusCheck`], per the [`StatusPolicy`], and so does a
42//!    `status` claim, which the Token Status List defines for any JWT (§6.1), as
43//!    `CredentialStatus::SdJwt` after the `credentialStatus` entries. With the `sd-jwt`
44//!    feature a `status` claim that does not parse, or names no status mechanism, is
45//!    refused; without it the claim cannot be checked, so it fails under
46//!    [`StatusPolicy::Strict`] and is ignored otherwise.
47//!
48//! For a presentation ([`JwtPresentationValidator`]): the same for the presentation JWT, with
49//! the key resolved for [`KeyRole::Subject`] (`authentication`) and bound to the holder, plus the expected
50//! audience and nonce; then every credential it carries is validated as above, and its subject
51//! is checked against the holder per the [`SubjectHolderRelationship`]. A VCDM 1.1
52//! presentation (`typ` `JWT`, [`validate`](JwtPresentationValidator::validate)) carries
53//! credential JWTs in its `vp` claim; a VCDM 2.0 one (`typ` `vp+jwt`,
54//! [`validate_v2`](JwtPresentationValidator::validate_v2)) is the payload itself and envelopes
55//! `vc+jwt` credentials and, with the `sd-jwt` feature, `vc+sd-jwt` ones (validated as
56//! `SdJwtCredentialValidator::validate_v2` of `sd_jwt_vc` does, with the key binding the
57//! presentation options require); an envelope of another media type fails for that
58//! credential alone.
59//!
60//! Everything a verifier decides by policy (which issuers to trust, which credential types to
61//! accept, what the claims must say) is left to the application, which gets the decoded
62//! credential back. [`decode_credential`], [`decode_credential_v2`], [`decode_presentation`]
63//! and [`decode_presentation_v2`] decode without verifying, for a look at a token before
64//! deciding how to validate it, and refuse an SD-JWT payload as the validators do; [`extract_issuer`] and [`extract_holder`] read just the signer's DID.
65
66use core::time::Duration;
67use std::collections::BTreeSet;
68use std::sync::Arc;
69
70use chrono::{DateTime, TimeDelta, Utc};
71
72use crate::did::Did;
73pub use crate::resolver::{
74    KeyReference, KeyReferenceError, KeyResolutionError, KeyResolver, KeyRole,
75};
76use crate::vc::{Jwt, JwtClaims, TypedObject, claims};
77
78mod credential;
79mod error;
80mod presentation;
81
82pub use credential::{
83    DecodedCredential, DecodedCredentialV2, JwtCredentialValidator, decode_credential,
84    decode_credential_v2,
85};
86pub use error::{PresentationValidationError, StatusError, ValidationError};
87#[cfg(feature = "linked-vp")]
88pub(crate) use presentation::decode_presentation_with;
89pub use presentation::{
90    DecodedPresentation, DecodedPresentationV2, JwtPresentationValidator, PresentedCredentialV2,
91    decode_presentation, decode_presentation_v2,
92};
93
94/// A status entry of a credential, of either credential format, for a [`StatusCheck`].
95///
96/// # Examples
97///
98/// ```
99/// use identity_kit::vc::TypedObject;
100/// use identity_kit::vc::validation::CredentialStatus;
101///
102/// let entry: TypedObject = serde_json::from_value(serde_json::json!({
103///     "id": "https://example.com/credentials/status/3#94567",
104///     "type": "BitstringStatusListEntry",
105/// }))?;
106/// assert_eq!(CredentialStatus::Vcdm(&entry).mechanism(), "BitstringStatusListEntry");
107/// # Ok::<(), serde_json::Error>(())
108/// ```
109#[derive(Debug, Clone, Copy, PartialEq)]
110#[non_exhaustive]
111pub enum CredentialStatus<'a> {
112    /// A `credentialStatus` entry of a VCDM credential.
113    Vcdm(&'a TypedObject),
114    /// The `status` claim of an SD-JWT VC, of a VCDM 2.0 credential secured with SD-JWT, or of
115    /// a JWT credential of either profile (IETF Token Status List §6.1 and other mechanisms).
116    #[cfg(feature = "sd-jwt")]
117    #[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
118    SdJwt(&'a crate::sd_jwt_vc::Status),
119}
120
121impl CredentialStatus<'_> {
122    /// What a check reports as [`StatusError::Unsupported`] for this entry: the `type` of a
123    /// `credentialStatus` entry, or the names of the mechanisms in a `status` claim
124    /// (`status_list`, and the members of other mechanisms), comma separated.
125    pub fn mechanism(&self) -> String {
126        match self {
127            Self::Vcdm(entry) => entry.object_type().to_owned(),
128            #[cfg(feature = "sd-jwt")]
129            Self::SdJwt(status) => status
130                .mechanisms()
131                .map(|mechanism| match mechanism {
132                    crate::sd_jwt_vc::StatusMechanism::StatusList(_) => "status_list",
133                    crate::sd_jwt_vc::StatusMechanism::Other { name, .. } => name,
134                })
135                .collect::<Vec<_>>()
136                .join(", "),
137        }
138    }
139}
140
141/// Checks the status of a credential: fetches the status list, bitmap or registry an entry
142/// points to and reports whether the credential is still good.
143///
144/// One check serves both credential formats: the validators hand it every `credentialStatus`
145/// entry of a VCDM credential as [`CredentialStatus::Vcdm`] and the `status` claim of an
146/// SD-JWT credential, or of a JWT credential, as `CredentialStatus::SdJwt` (feature
147/// `sd-jwt`), so an application implements status checking once. A check that does not
148/// understand an entry reports [`StatusError::Unsupported`], which the [`StatusPolicy`]
149/// decides about. `StatusListCheck` of `sd_jwt_vc::validation` (features `sd-jwt` and
150/// `did-web`) checks the Token Status List and reports every VCDM entry as unsupported.
151///
152/// # Examples
153///
154/// A check that knows only the Token Status List, and only the list it was given:
155///
156/// ```
157/// use chrono::{DateTime, Utc};
158/// use identity_kit::vc::validation::{CredentialStatus, StatusCheck, StatusError};
159///
160/// struct Revoked;
161///
162/// #[async_trait::async_trait]
163/// impl StatusCheck for Revoked {
164///     async fn check(&self, status: CredentialStatus<'_>, _: DateTime<Utc>) -> Result<(), StatusError> {
165///         match status {
166///             #[cfg(feature = "sd-jwt")]
167///             CredentialStatus::SdJwt(status) if status.status_list.is_some() => {
168///                 Err(StatusError::Invalid("revoked".to_owned()))
169///             }
170///             other => Err(StatusError::Unsupported(other.mechanism())),
171///         }
172///     }
173/// }
174/// ```
175#[async_trait::async_trait]
176pub trait StatusCheck: Send + Sync {
177    /// Checks `status` at the validation time `now`, against which a check judges what it
178    /// fetched (the expiry of a status list token, for instance).
179    ///
180    /// # Errors
181    ///
182    /// A [`StatusError`]: [`Unsupported`](StatusError::Unsupported) for an entry or mechanism
183    /// the check does not know, [`Unavailable`](StatusError::Unavailable) when the status
184    /// could not be obtained, [`Invalid`](StatusError::Invalid) when the credential is revoked
185    /// or suspended.
186    async fn check(
187        &self,
188        status: CredentialStatus<'_>,
189        now: DateTime<Utc>,
190    ) -> Result<(), StatusError>;
191}
192
193macro_rules! forward_status_check {
194    ($($ty:ty),* $(,)?) => {
195        $(
196            #[async_trait::async_trait]
197            impl<T: StatusCheck + ?Sized> StatusCheck for $ty {
198                async fn check(
199                    &self,
200                    status: CredentialStatus<'_>,
201                    now: DateTime<Utc>,
202                ) -> Result<(), StatusError> {
203                    (**self).check(status, now).await
204                }
205            }
206        )*
207    };
208}
209
210forward_status_check!(&T, Box<T>, Arc<T>);
211
212/// Checks every entry of `statuses` with `check` per `policy`, at `now`.
213pub(crate) async fn check_statuses<'a>(
214    check: Option<&dyn StatusCheck>,
215    statuses: impl IntoIterator<Item = CredentialStatus<'a>>,
216    policy: StatusPolicy,
217    now: DateTime<Utc>,
218) -> Result<(), ValidationError> {
219    if policy == StatusPolicy::SkipAll {
220        return Ok(());
221    }
222    for status in statuses {
223        let result = match check {
224            Some(check) => check.check(status, now).await,
225            None => Err(StatusError::Unsupported(status.mechanism())),
226        };
227        match result {
228            Ok(()) => {}
229            Err(StatusError::Unsupported(_)) if policy == StatusPolicy::SkipUnsupported => {}
230            Err(StatusError::Unsupported(mechanism)) => {
231                return Err(ValidationError::StatusUnchecked(mechanism));
232            }
233            Err(error) => return Err(ValidationError::Status(error)),
234        }
235    }
236    Ok(())
237}
238
239/// The `status` claim of a JWT credential, which the Token Status List defines for any JWT
240/// (§6.1): a `sd_jwt_vc::Status` with the `sd-jwt` feature, uninhabited without it.
241#[cfg(feature = "sd-jwt")]
242pub(super) type StatusClaim = crate::sd_jwt_vc::Status;
243
244/// The `status` claim of a JWT credential, which the Token Status List defines for any JWT
245/// (§6.1): a `sd_jwt_vc::Status` with the `sd-jwt` feature, uninhabited without it.
246#[cfg(not(feature = "sd-jwt"))]
247pub(super) enum StatusClaim {}
248
249/// The `status` claim of `claims`, for the [`StatusCheck`] beside the `credentialStatus`
250/// entries.
251///
252/// With the `sd-jwt` feature the claim is parsed, and one that does not parse, or names no
253/// status mechanism, is an [`InvalidClaim`](crate::vc::CredentialError::InvalidClaim)
254/// whatever the `policy`. Without it the claim cannot be handed to a check, so it fails under
255/// [`StatusPolicy::Strict`] as an unchecked entry and is ignored otherwise.
256pub(super) fn status_claim(
257    claims: &JwtClaims,
258    policy: StatusPolicy,
259) -> Result<Option<StatusClaim>, ValidationError> {
260    let Some(status) = claims.additional.get("status") else {
261        return Ok(None);
262    };
263    #[cfg(feature = "sd-jwt")]
264    {
265        let _ = policy;
266        let invalid = |reason: String| crate::vc::CredentialError::InvalidClaim {
267            claim: "status",
268            reason,
269        };
270        let status: StatusClaim =
271            serde_json::from_value(status.clone()).map_err(|e| invalid(e.to_string()))?;
272        // The claim references at least one status mechanism (Token Status List §6.1).
273        if status.is_empty() {
274            return Err(invalid("names no status mechanism".to_owned()).into());
275        }
276        Ok(Some(status))
277    }
278    #[cfg(not(feature = "sd-jwt"))]
279    {
280        let _ = status;
281        match policy {
282            StatusPolicy::Strict => Err(ValidationError::StatusUnchecked("status".to_owned())),
283            _ => Ok(None),
284        }
285    }
286}
287
288/// `status` as the entry a [`StatusCheck`] is handed.
289pub(super) fn status_claim_entry(status: &StatusClaim) -> CredentialStatus<'_> {
290    #[cfg(feature = "sd-jwt")]
291    {
292        CredentialStatus::SdJwt(status)
293    }
294    #[cfg(not(feature = "sd-jwt"))]
295    {
296        match *status {}
297    }
298}
299
300/// What to do about a status entry: a `credentialStatus` entry, or the `status` claim of an
301/// SD-JWT credential or of a JWT credential.
302#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
303#[non_exhaustive]
304pub enum StatusPolicy {
305    /// Every entry must pass the validator's [`StatusCheck`]; without one, or for a type it
306    /// does not support, validation fails.
307    #[default]
308    Strict,
309    /// Entries of a type the [`StatusCheck`] does not support, or any entry when there is no
310    /// check, are ignored.
311    SkipUnsupported,
312    /// Every status entry is ignored: `credentialStatus` and the `status` claim.
313    SkipAll,
314}
315
316/// Whether to stop at the first failing credential of a presentation.
317#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
318#[non_exhaustive]
319pub enum FailFast {
320    /// Stop at the first credential that fails.
321    #[default]
322    FirstError,
323    /// Validate every credential and report all failures.
324    AllErrors,
325}
326
327/// How the subject of a presented credential must relate to the holder presenting it.
328#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
329#[non_exhaustive]
330pub enum SubjectHolderRelationship {
331    /// Every subject of every credential must be the holder. A bearer credential fails.
332    #[default]
333    AlwaysSubject,
334    /// As [`AlwaysSubject`](Self::AlwaysSubject), but only for credentials whose
335    /// `nonTransferable` property is `true`.
336    SubjectOnNonTransferable,
337    /// Not checked.
338    Any,
339}
340
341/// The validation time and the clock skew allowed around it.
342///
343/// # Examples
344///
345/// ```
346/// use std::time::Duration;
347///
348/// use chrono::{TimeZone as _, Utc};
349/// use identity_kit::vc::validation::TimeOptions;
350///
351/// let fixed = TimeOptions::at(Utc.with_ymd_and_hms(2019, 11, 6, 0, 0, 0).unwrap())
352///     .with_leeway(Duration::from_secs(60));
353/// assert_eq!(fixed.now().to_rfc3339(), "2019-11-06T00:00:00+00:00");
354/// assert_eq!(fixed.leeway, Duration::from_secs(60));
355/// assert!(TimeOptions::default().now() > fixed.now());
356/// ```
357#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
358#[non_exhaustive]
359pub struct TimeOptions {
360    /// The validation time; the current time when `None`.
361    pub now: Option<DateTime<Utc>>,
362    /// How far `nbf`, `exp` and `iat` may miss the validation time and still pass; zero by
363    /// default.
364    pub leeway: Duration,
365}
366
367impl TimeOptions {
368    /// Options validating at `now`.
369    pub fn at(now: DateTime<Utc>) -> Self {
370        Self {
371            now: Some(now),
372            leeway: Duration::ZERO,
373        }
374    }
375
376    /// These options with `leeway`.
377    #[must_use]
378    pub fn with_leeway(mut self, leeway: Duration) -> Self {
379        self.leeway = leeway;
380        self
381    }
382
383    /// The validation time.
384    pub fn now(&self) -> DateTime<Utc> {
385        self.now.unwrap_or_else(Utc::now)
386    }
387
388    /// The leeway as a `chrono` delta, saturated.
389    fn delta(&self) -> TimeDelta {
390        TimeDelta::from_std(self.leeway).unwrap_or(TimeDelta::MAX)
391    }
392
393    /// Checks that `now` is within `[not_before, expires)` with the leeway, and that
394    /// `issued_at` is not in the future.
395    ///
396    /// # Errors
397    ///
398    /// [`ValidationError::NotYetValid`], [`ValidationError::Expired`] or
399    /// [`ValidationError::IssuedInFuture`].
400    pub fn check(
401        &self,
402        not_before: Option<DateTime<Utc>>,
403        expires: Option<DateTime<Utc>>,
404        issued_at: Option<DateTime<Utc>>,
405    ) -> Result<(), ValidationError> {
406        let now = self.now();
407        let delta = self.delta();
408        // A leeway past the range `chrono` represents saturates rather than overflowing.
409        let latest = now
410            .checked_add_signed(delta)
411            .unwrap_or(DateTime::<Utc>::MAX_UTC);
412        let earliest = now
413            .checked_sub_signed(delta)
414            .unwrap_or(DateTime::<Utc>::MIN_UTC);
415        if let Some(not_before) = not_before
416            && not_before > latest
417        {
418            return Err(ValidationError::NotYetValid { not_before, now });
419        }
420        if let Some(expired_at) = expires
421            && expired_at <= earliest
422        {
423            return Err(ValidationError::Expired { expired_at, now });
424        }
425        if let Some(issued_at) = issued_at
426            && issued_at > latest
427        {
428            return Err(ValidationError::IssuedInFuture { issued_at, now });
429        }
430        Ok(())
431    }
432
433    /// Checks the registered time claims of `claims`: `nbf`, `exp` and `iat`.
434    ///
435    /// # Errors
436    ///
437    /// As [`check`](Self::check), or [`ValidationError::Credential`] for a claim out of
438    /// range.
439    pub fn check_claims(&self, claims: &JwtClaims) -> Result<(), ValidationError> {
440        self.check(
441            timestamp(claims.nbf, "nbf")?,
442            timestamp(claims.exp, "exp")?,
443            timestamp(claims.iat, "iat")?,
444        )
445    }
446}
447
448/// The time a `NumericDate` claim names.
449fn timestamp(
450    value: Option<i64>,
451    claim: &'static str,
452) -> Result<Option<DateTime<Utc>>, ValidationError> {
453    value
454        .map(|value| {
455            JwtClaims::timestamp(value).ok_or_else(|| {
456                ValidationError::Credential(crate::vc::CredentialError::InvalidClaim {
457                    claim,
458                    reason: format!("{value} is out of range"),
459                })
460            })
461        })
462        .transpose()
463}
464
465/// Options of [`JwtCredentialValidator`].
466#[derive(Debug, Clone, Default, PartialEq, Eq)]
467#[non_exhaustive]
468pub struct CredentialValidationOptions {
469    /// The validation time and leeway.
470    pub time: TimeOptions,
471    /// What to do about `credentialStatus`.
472    pub status: StatusPolicy,
473    /// Header `typ` values accepted besides the profile's own (`JWT` for VCDM 1.1, `vc+jwt`
474    /// for VCDM 2.0); none by default. A token without `typ` is always accepted.
475    pub additional_typ: BTreeSet<String>,
476}
477
478impl CredentialValidationOptions {
479    /// Default options: validate now without leeway, [`StatusPolicy::Strict`].
480    pub fn new() -> Self {
481        Self::default()
482    }
483
484    /// These options with `time`.
485    #[must_use]
486    pub fn with_time(mut self, time: TimeOptions) -> Self {
487        self.time = time;
488        self
489    }
490
491    /// These options with `status`.
492    #[must_use]
493    pub fn with_status(mut self, status: StatusPolicy) -> Self {
494        self.status = status;
495        self
496    }
497
498    /// These options also accepting a credential whose header `typ` is `typ`.
499    #[must_use]
500    pub fn with_additional_typ(mut self, typ: impl Into<String>) -> Self {
501        self.additional_typ.insert(typ.into());
502        self
503    }
504}
505
506/// Options of [`JwtPresentationValidator`].
507#[derive(Debug, Clone, Default, PartialEq, Eq)]
508#[non_exhaustive]
509pub struct PresentationValidationOptions {
510    /// The validation time and leeway of the presentation JWT.
511    pub time: TimeOptions,
512    /// The audience the presentation must name in `aud`, if any is required.
513    pub audience: Option<String>,
514    /// The `nonce` the presentation must carry, if one is required.
515    pub nonce: Option<String>,
516    /// How credential subjects must relate to the holder.
517    pub subject_holder: SubjectHolderRelationship,
518    /// The options every presented credential is validated with.
519    pub credentials: CredentialValidationOptions,
520    /// Whether to stop at the first failing credential.
521    pub fail_fast: FailFast,
522    /// Header `typ` values accepted for the presentation JWT besides the profile's own (`JWT`
523    /// for VCDM 1.1, `vp+jwt` for VCDM 2.0), by both profiles; none by default. A token
524    /// without `typ` is always accepted. The credentials' own are
525    /// [`CredentialValidationOptions::additional_typ`].
526    pub additional_typ: BTreeSet<String>,
527    /// The key binding every enveloped credential secured with SD-JWT must prove, if any
528    /// is required; none by default, since the presentation JWT already proves holder
529    /// control. See [`JwtPresentationValidator::validate_v2`].
530    #[cfg(feature = "sd-jwt")]
531    #[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
532    pub key_binding: Option<crate::sd_jwt_vc::KeyBindingOptions>,
533}
534
535impl PresentationValidationOptions {
536    /// Default options: validate now without leeway, no audience or nonce required,
537    /// [`SubjectHolderRelationship::AlwaysSubject`], default credential options,
538    /// [`FailFast::FirstError`].
539    pub fn new() -> Self {
540        Self::default()
541    }
542
543    /// These options with `time`, for the presentation JWT only.
544    #[must_use]
545    pub fn with_time(mut self, time: TimeOptions) -> Self {
546        self.time = time;
547        self
548    }
549
550    /// These options requiring `audience`.
551    #[must_use]
552    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
553        self.audience = Some(audience.into());
554        self
555    }
556
557    /// These options requiring `nonce`.
558    #[must_use]
559    pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
560        self.nonce = Some(nonce.into());
561        self
562    }
563
564    /// These options with `relationship`.
565    #[must_use]
566    pub fn with_subject_holder(mut self, relationship: SubjectHolderRelationship) -> Self {
567        self.subject_holder = relationship;
568        self
569    }
570
571    /// These options validating credentials with `credentials`.
572    #[must_use]
573    pub fn with_credentials(mut self, credentials: CredentialValidationOptions) -> Self {
574        self.credentials = credentials;
575        self
576    }
577
578    /// These options with `fail_fast`.
579    #[must_use]
580    pub fn with_fail_fast(mut self, fail_fast: FailFast) -> Self {
581        self.fail_fast = fail_fast;
582        self
583    }
584
585    /// These options also accepting a presentation whose header `typ` is `typ`.
586    #[must_use]
587    pub fn with_additional_typ(mut self, typ: impl Into<String>) -> Self {
588        self.additional_typ.insert(typ.into());
589        self
590    }
591
592    /// These options requiring `key_binding` of every enveloped credential secured with
593    /// SD-JWT.
594    #[cfg(feature = "sd-jwt")]
595    #[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
596    #[must_use]
597    pub fn with_key_binding(mut self, key_binding: crate::sd_jwt_vc::KeyBindingOptions) -> Self {
598        self.key_binding = Some(key_binding);
599        self
600    }
601}
602
603/// The DID of the issuer of a credential JWT, read without verifying anything: `iss`, else
604/// the `issuer` of the `vc` claim (VCDM 1.1) or of the payload (VCDM 2.0).
605///
606/// # Errors
607///
608/// [`ValidationError::Jws`] or [`ValidationError::Payload`] when the token does not decode,
609/// [`ValidationError::Credential`] when it names no issuer, and [`ValidationError::NotDid`]
610/// when the issuer is not a DID.
611pub fn extract_issuer(jwt: &Jwt) -> Result<Did, ValidationError> {
612    let claims = decode_claims(jwt)?;
613    let issuer = claims
614        .iss
615        .clone()
616        .or_else(|| property_id(&claims, "vc", "issuer"))
617        .ok_or(crate::vc::CredentialError::MissingClaim("iss"))?;
618    Did::parse(&issuer).map_err(|_| ValidationError::NotDid {
619        role: "issuer",
620        id: issuer,
621    })
622}
623
624/// The DID of the holder of a presentation JWT, read without verifying anything: `iss`, else
625/// the `holder` of the `vp` claim (VCDM 1.1) or of the payload (VCDM 2.0).
626///
627/// # Errors
628///
629/// As [`extract_issuer`], for the holder.
630pub fn extract_holder(jwt: &Jwt) -> Result<Did, ValidationError> {
631    let claims = decode_claims(jwt)?;
632    let holder = claims
633        .iss
634        .clone()
635        .or_else(|| property_id(&claims, "vp", "holder"))
636        .ok_or(crate::vc::CredentialError::MissingClaim("iss"))?;
637    Did::parse(&holder).map_err(|_| ValidationError::NotDid {
638        role: "holder",
639        id: holder,
640    })
641}
642
643/// The identifier of `property` in the `container` claim (VCDM 1.1), or in the payload.
644fn property_id(claims: &JwtClaims, container: &str, property: &str) -> Option<String> {
645    let value = match claims.additional.get(container) {
646        Some(container) => container.get(property)?,
647        None => claims.additional.get(property)?,
648    };
649    claims::id_of(value).map(str::to_owned)
650}
651
652/// The claims set of `jwt`, unverified.
653pub(super) fn decode_claims(jwt: &Jwt) -> Result<JwtClaims, ValidationError> {
654    let jws = jwt.decode()?;
655    serde_json::from_slice(jws.payload()).map_err(|e| ValidationError::Payload(e.to_string()))
656}