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