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::StatusClaim` after the `credentialStatus` entries. With the
44//! `status-list` feature (which `sd-jwt` implies) a `status` claim that does not parse, or
45//! names no status mechanism, is refused; without it the claim cannot be checked, so it
46//! fails under [`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 a JWT credential of either profile, of an SD-JWT VC or of a VCDM
115 /// 2.0 credential secured with SD-JWT (IETF Token Status List §6.1 and other mechanisms).
116 #[cfg(feature = "status-list")]
117 #[cfg_attr(docsrs, doc(cfg(feature = "status-list")))]
118 StatusClaim(&'a crate::status_list::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 = "status-list")]
129 Self::StatusClaim(status) => status
130 .mechanisms()
131 .map(|mechanism| match mechanism {
132 crate::status_list::StatusMechanism::StatusList(_) => "status_list",
133 crate::status_list::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 a JWT
146/// or SD-JWT credential as `CredentialStatus::StatusClaim` (feature `status-list`), so an
147/// application implements status checking once. A check that does not understand an entry
148/// reports [`StatusError::Unsupported`], which the [`StatusPolicy`] decides about.
149/// `StatusListCheck` of `status_list` (features `status-list` and `did-web`) checks the Token
150/// 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 = "status-list")]
167/// CredentialStatus::StatusClaim(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) and the other
184 /// variants when the status could not be obtained or trusted,
185 /// [`Invalid`](StatusError::Invalid) when the credential is revoked
186 /// or suspended.
187 async fn check(
188 &self,
189 status: CredentialStatus<'_>,
190 now: DateTime<Utc>,
191 ) -> Result<(), StatusError>;
192}
193
194macro_rules! forward_status_check {
195 ($($ty:ty),* $(,)?) => {
196 $(
197 #[async_trait::async_trait]
198 impl<T: StatusCheck + ?Sized> StatusCheck for $ty {
199 async fn check(
200 &self,
201 status: CredentialStatus<'_>,
202 now: DateTime<Utc>,
203 ) -> Result<(), StatusError> {
204 (**self).check(status, now).await
205 }
206 }
207 )*
208 };
209}
210
211forward_status_check!(&T, Box<T>, Arc<T>);
212
213/// Checks every entry of `statuses` with `check` per `policy`, at `now`.
214pub(crate) async fn check_statuses(
215 check: Option<&dyn StatusCheck>,
216 statuses: Vec<CredentialStatus<'_>>,
217 policy: StatusPolicy,
218 now: DateTime<Utc>,
219) -> Result<(), ValidationError> {
220 if policy == StatusPolicy::SkipAll {
221 return Ok(());
222 }
223 for status in statuses {
224 let result = match check {
225 Some(check) => check.check(status, now).await,
226 None => Err(StatusError::Unsupported(status.mechanism())),
227 };
228 match result {
229 Ok(()) => {}
230 Err(StatusError::Unsupported(_)) if policy == StatusPolicy::SkipUnsupported => {}
231 Err(StatusError::Unsupported(mechanism)) => {
232 return Err(ValidationError::StatusUnchecked(mechanism));
233 }
234 Err(error) => return Err(ValidationError::Status(error)),
235 }
236 }
237 Ok(())
238}
239
240/// The `status` claim of a JWT credential, which the Token Status List defines for any JWT
241/// (§6.1): a `status_list::Status` with the `status-list` feature, uninhabited without it.
242#[cfg(feature = "status-list")]
243pub(super) type StatusClaim = crate::status_list::Status;
244
245/// The `status` claim of a JWT credential, which the Token Status List defines for any JWT
246/// (§6.1): a `status_list::Status` with the `status-list` feature, uninhabited without it.
247#[cfg(not(feature = "status-list"))]
248pub(super) enum StatusClaim {}
249
250/// The `status` claim of `claims`, for the [`StatusCheck`] beside the `credentialStatus`
251/// entries.
252///
253/// With the `status-list` feature the claim is parsed, and one that does not parse, or names no
254/// status mechanism, is an [`InvalidClaim`](crate::vc::CredentialError::InvalidClaim)
255/// whatever the `policy`. Without it the claim cannot be handed to a check, so it fails under
256/// [`StatusPolicy::Strict`] as an unchecked entry and is ignored otherwise.
257pub(super) fn status_claim(
258 claims: &JwtClaims,
259 policy: StatusPolicy,
260) -> Result<Option<StatusClaim>, ValidationError> {
261 let Some(status) = claims.additional.get("status") else {
262 return Ok(None);
263 };
264 #[cfg(feature = "status-list")]
265 {
266 let _ = policy;
267 let invalid = |reason: String| crate::vc::CredentialError::InvalidClaim {
268 claim: "status",
269 reason,
270 };
271 let status: StatusClaim =
272 serde_json::from_value(status.clone()).map_err(|e| invalid(e.to_string()))?;
273 // The claim references at least one status mechanism (Token Status List §6.1).
274 if status.is_empty() {
275 return Err(invalid("names no status mechanism".to_owned()).into());
276 }
277 Ok(Some(status))
278 }
279 #[cfg(not(feature = "status-list"))]
280 {
281 let _ = status;
282 match policy {
283 StatusPolicy::Strict => Err(ValidationError::StatusUnchecked("status".to_owned())),
284 _ => Ok(None),
285 }
286 }
287}
288
289/// `status` as the entry a [`StatusCheck`] is handed.
290pub(super) fn status_claim_entry(status: &StatusClaim) -> CredentialStatus<'_> {
291 #[cfg(feature = "status-list")]
292 {
293 CredentialStatus::StatusClaim(status)
294 }
295 #[cfg(not(feature = "status-list"))]
296 {
297 match *status {}
298 }
299}
300
301/// What to do about a status entry: a `credentialStatus` entry, or the `status` claim of an
302/// SD-JWT credential or of a JWT credential.
303#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
304#[non_exhaustive]
305pub enum StatusPolicy {
306 /// Every entry must pass the validator's [`StatusCheck`]; without one, or for a type it
307 /// does not support, validation fails.
308 #[default]
309 Strict,
310 /// Entries of a type the [`StatusCheck`] does not support, or any entry when there is no
311 /// check, are ignored.
312 SkipUnsupported,
313 /// Every status entry is ignored: `credentialStatus` and the `status` claim.
314 SkipAll,
315}
316
317/// Whether to stop at the first failing credential of a presentation.
318#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
319#[non_exhaustive]
320pub enum FailFast {
321 /// Stop at the first credential that fails.
322 #[default]
323 FirstError,
324 /// Validate every credential and report all failures.
325 AllErrors,
326}
327
328/// How the subject of a presented credential must relate to the holder presenting it.
329#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
330#[non_exhaustive]
331pub enum SubjectHolderRelationship {
332 /// Every subject of every credential must be the holder. A bearer credential fails.
333 #[default]
334 AlwaysSubject,
335 /// As [`AlwaysSubject`](Self::AlwaysSubject), but only for credentials whose
336 /// `nonTransferable` property is `true`.
337 SubjectOnNonTransferable,
338 /// Not checked.
339 Any,
340}
341
342/// The validation time and the clock skew allowed around it.
343///
344/// # Examples
345///
346/// ```
347/// use std::time::Duration;
348///
349/// use chrono::{TimeZone as _, Utc};
350/// use identity_kit::vc::validation::TimeOptions;
351///
352/// let fixed = TimeOptions::at(Utc.with_ymd_and_hms(2019, 11, 6, 0, 0, 0).unwrap())
353/// .with_leeway(Duration::from_secs(60));
354/// assert_eq!(fixed.now().to_rfc3339(), "2019-11-06T00:00:00+00:00");
355/// assert_eq!(fixed.leeway, Duration::from_secs(60));
356/// assert!(TimeOptions::default().now() > fixed.now());
357/// ```
358#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
359#[non_exhaustive]
360pub struct TimeOptions {
361 /// The validation time; the current time when `None`.
362 pub now: Option<DateTime<Utc>>,
363 /// How far `nbf`, `exp` and `iat` may miss the validation time and still pass; zero by
364 /// default.
365 pub leeway: Duration,
366}
367
368impl TimeOptions {
369 /// Options validating at `now`.
370 pub fn at(now: DateTime<Utc>) -> Self {
371 Self {
372 now: Some(now),
373 leeway: Duration::ZERO,
374 }
375 }
376
377 /// These options with `leeway`.
378 #[must_use]
379 pub fn with_leeway(mut self, leeway: Duration) -> Self {
380 self.leeway = leeway;
381 self
382 }
383
384 /// The validation time.
385 pub fn now(&self) -> DateTime<Utc> {
386 self.now.unwrap_or_else(Utc::now)
387 }
388
389 /// The leeway as a `chrono` delta, saturated.
390 fn delta(&self) -> TimeDelta {
391 TimeDelta::from_std(self.leeway).unwrap_or(TimeDelta::MAX)
392 }
393
394 /// Checks that `now` is within `[not_before, expires)` with the leeway, and that
395 /// `issued_at` is not in the future.
396 ///
397 /// # Errors
398 ///
399 /// [`ValidationError::NotYetValid`], [`ValidationError::Expired`] or
400 /// [`ValidationError::IssuedInFuture`].
401 pub fn check(
402 &self,
403 not_before: Option<DateTime<Utc>>,
404 expires: Option<DateTime<Utc>>,
405 issued_at: Option<DateTime<Utc>>,
406 ) -> Result<(), ValidationError> {
407 let now = self.now();
408 let delta = self.delta();
409 // A leeway past the range `chrono` represents saturates rather than overflowing.
410 let latest = now
411 .checked_add_signed(delta)
412 .unwrap_or(DateTime::<Utc>::MAX_UTC);
413 let earliest = now
414 .checked_sub_signed(delta)
415 .unwrap_or(DateTime::<Utc>::MIN_UTC);
416 if let Some(not_before) = not_before
417 && not_before > latest
418 {
419 return Err(ValidationError::NotYetValid { not_before, now });
420 }
421 if let Some(expired_at) = expires
422 && expired_at <= earliest
423 {
424 return Err(ValidationError::Expired { expired_at, now });
425 }
426 if let Some(issued_at) = issued_at
427 && issued_at > latest
428 {
429 return Err(ValidationError::IssuedInFuture { issued_at, now });
430 }
431 Ok(())
432 }
433
434 /// Checks the registered time claims of `claims`: `nbf`, `exp` and `iat`.
435 ///
436 /// # Errors
437 ///
438 /// As [`check`](Self::check), or [`ValidationError::Credential`] for a claim out of
439 /// range.
440 pub fn check_claims(&self, claims: &JwtClaims) -> Result<(), ValidationError> {
441 self.check(
442 timestamp(claims.nbf, "nbf")?,
443 timestamp(claims.exp, "exp")?,
444 timestamp(claims.iat, "iat")?,
445 )
446 }
447}
448
449/// The time a `NumericDate` claim names.
450fn timestamp(
451 value: Option<i64>,
452 claim: &'static str,
453) -> Result<Option<DateTime<Utc>>, ValidationError> {
454 value
455 .map(|value| {
456 JwtClaims::timestamp(value).ok_or_else(|| {
457 ValidationError::Credential(crate::vc::CredentialError::InvalidClaim {
458 claim,
459 reason: format!("{value} is out of range"),
460 })
461 })
462 })
463 .transpose()
464}
465
466/// Options of [`JwtCredentialValidator`].
467#[derive(Debug, Clone, Default, PartialEq, Eq)]
468#[non_exhaustive]
469pub struct CredentialValidationOptions {
470 /// The validation time and leeway.
471 pub time: TimeOptions,
472 /// What to do about `credentialStatus`.
473 pub status: StatusPolicy,
474 /// Header `typ` values accepted besides the profile's own (`JWT` for VCDM 1.1, `vc+jwt`
475 /// for VCDM 2.0); none by default. A token without `typ` is always accepted.
476 pub additional_typ: BTreeSet<String>,
477}
478
479impl CredentialValidationOptions {
480 /// Default options: validate now without leeway, [`StatusPolicy::Strict`].
481 pub fn new() -> Self {
482 Self::default()
483 }
484
485 /// These options with `time`.
486 #[must_use]
487 pub fn with_time(mut self, time: TimeOptions) -> Self {
488 self.time = time;
489 self
490 }
491
492 /// These options with `status`.
493 #[must_use]
494 pub fn with_status(mut self, status: StatusPolicy) -> Self {
495 self.status = status;
496 self
497 }
498
499 /// These options also accepting a credential whose header `typ` is `typ`.
500 #[must_use]
501 pub fn with_additional_typ(mut self, typ: impl Into<String>) -> Self {
502 self.additional_typ.insert(typ.into());
503 self
504 }
505}
506
507/// Options of [`JwtPresentationValidator`].
508#[derive(Debug, Clone, Default, PartialEq, Eq)]
509#[non_exhaustive]
510pub struct PresentationValidationOptions {
511 /// The validation time and leeway of the presentation JWT.
512 pub time: TimeOptions,
513 /// The audience the presentation must name in `aud`, if any is required.
514 pub audience: Option<String>,
515 /// The `nonce` the presentation must carry, if one is required.
516 pub nonce: Option<String>,
517 /// How credential subjects must relate to the holder.
518 pub subject_holder: SubjectHolderRelationship,
519 /// The options every presented credential is validated with.
520 pub credentials: CredentialValidationOptions,
521 /// Whether to stop at the first failing credential.
522 pub fail_fast: FailFast,
523 /// Header `typ` values accepted for the presentation JWT besides the profile's own (`JWT`
524 /// for VCDM 1.1, `vp+jwt` for VCDM 2.0), by both profiles; none by default. A token
525 /// without `typ` is always accepted. The credentials' own are
526 /// [`CredentialValidationOptions::additional_typ`].
527 pub additional_typ: BTreeSet<String>,
528 /// The key binding every enveloped credential secured with SD-JWT must prove, if any
529 /// is required; none by default, since the presentation JWT already proves holder
530 /// control. See [`JwtPresentationValidator::validate_v2`].
531 #[cfg(feature = "sd-jwt")]
532 #[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
533 pub key_binding: Option<crate::sd_jwt_vc::KeyBindingOptions>,
534}
535
536impl PresentationValidationOptions {
537 /// Default options: validate now without leeway, no audience or nonce required,
538 /// [`SubjectHolderRelationship::AlwaysSubject`], default credential options,
539 /// [`FailFast::FirstError`].
540 pub fn new() -> Self {
541 Self::default()
542 }
543
544 /// These options with `time`, for the presentation JWT only.
545 #[must_use]
546 pub fn with_time(mut self, time: TimeOptions) -> Self {
547 self.time = time;
548 self
549 }
550
551 /// These options requiring `audience`.
552 #[must_use]
553 pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
554 self.audience = Some(audience.into());
555 self
556 }
557
558 /// These options requiring `nonce`.
559 #[must_use]
560 pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
561 self.nonce = Some(nonce.into());
562 self
563 }
564
565 /// These options with `relationship`.
566 #[must_use]
567 pub fn with_subject_holder(mut self, relationship: SubjectHolderRelationship) -> Self {
568 self.subject_holder = relationship;
569 self
570 }
571
572 /// These options validating credentials with `credentials`.
573 #[must_use]
574 pub fn with_credentials(mut self, credentials: CredentialValidationOptions) -> Self {
575 self.credentials = credentials;
576 self
577 }
578
579 /// These options with `fail_fast`.
580 #[must_use]
581 pub fn with_fail_fast(mut self, fail_fast: FailFast) -> Self {
582 self.fail_fast = fail_fast;
583 self
584 }
585
586 /// These options also accepting a presentation whose header `typ` is `typ`.
587 #[must_use]
588 pub fn with_additional_typ(mut self, typ: impl Into<String>) -> Self {
589 self.additional_typ.insert(typ.into());
590 self
591 }
592
593 /// These options requiring `key_binding` of every enveloped credential secured with
594 /// SD-JWT.
595 #[cfg(feature = "sd-jwt")]
596 #[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
597 #[must_use]
598 pub fn with_key_binding(mut self, key_binding: crate::sd_jwt_vc::KeyBindingOptions) -> Self {
599 self.key_binding = Some(key_binding);
600 self
601 }
602}
603
604/// The DID of the issuer of a credential JWT, read without verifying anything: `iss`, else
605/// the `issuer` of the `vc` claim (VCDM 1.1) or of the payload (VCDM 2.0).
606///
607/// # Errors
608///
609/// [`ValidationError::Jws`] or [`ValidationError::Payload`] when the token does not decode,
610/// [`ValidationError::Credential`] when it names no issuer, and [`ValidationError::NotDid`]
611/// when the issuer is not a DID.
612pub fn extract_issuer(jwt: &Jwt) -> Result<Did, ValidationError> {
613 let claims = decode_claims(jwt)?;
614 let issuer = claims
615 .iss
616 .clone()
617 .or_else(|| property_id(&claims, "vc", "issuer"))
618 .ok_or(crate::vc::CredentialError::MissingClaim("iss"))?;
619 Did::parse(&issuer).map_err(|_| ValidationError::NotDid {
620 role: "issuer",
621 id: issuer,
622 })
623}
624
625/// The DID of the holder of a presentation JWT, read without verifying anything: `iss`, else
626/// the `holder` of the `vp` claim (VCDM 1.1) or of the payload (VCDM 2.0).
627///
628/// # Errors
629///
630/// As [`extract_issuer`], for the holder.
631pub fn extract_holder(jwt: &Jwt) -> Result<Did, ValidationError> {
632 let claims = decode_claims(jwt)?;
633 let holder = claims
634 .iss
635 .clone()
636 .or_else(|| property_id(&claims, "vp", "holder"))
637 .ok_or(crate::vc::CredentialError::MissingClaim("iss"))?;
638 Did::parse(&holder).map_err(|_| ValidationError::NotDid {
639 role: "holder",
640 id: holder,
641 })
642}
643
644/// The identifier of `property` in the `container` claim (VCDM 1.1), or in the payload.
645fn property_id(claims: &JwtClaims, container: &str, property: &str) -> Option<String> {
646 let value = match claims.additional.get(container) {
647 Some(container) => container.get(property)?,
648 None => claims.additional.get(property)?,
649 };
650 claims::id_of(value).map(str::to_owned)
651}
652
653/// The claims set of `jwt`, unverified.
654pub(super) fn decode_claims(jwt: &Jwt) -> Result<JwtClaims, ValidationError> {
655 let jws = jwt.decode()?;
656 serde_json::from_slice(jws.payload()).map_err(ValidationError::Payload)
657}