Skip to main content

identity_kit/vc/validation/
presentation.rs

1//! `JwtPresentationValidator`.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use indexmap::IndexMap;
6use serde_json::Value;
7
8use crate::did::Uri;
9use crate::jose::{CompactJws, DefaultVerifier, JwsHeader, JwsVerifier};
10#[cfg(feature = "sd-jwt")]
11use crate::sd_jwt_vc::{SdJwtCredentialV2, SdJwtCredentialValidator, SdJwtValidationOptions};
12use crate::vc::{
13    CredentialError, CredentialV2, EnvelopedVc, Jwt, JwtClaims, Party, Presentation,
14    PresentationV2, Subject,
15};
16
17#[cfg(feature = "sd-jwt")]
18use super::CredentialStatus;
19use super::credential::{NO_TYP, check_controller, check_typ, decode, decode_v2, plain_claims};
20use super::{
21    DecodedCredential, DecodedCredentialV2, FailFast, JwtCredentialValidator, KeyResolver, KeyRole,
22    PresentationValidationError, PresentationValidationOptions, StatusCheck,
23    SubjectHolderRelationship, ValidationError,
24};
25
26/// A validated or decoded VCDM 1.1 presentation JWT: its header, its claims (custom ones
27/// included), the presentation, and its credentials decoded.
28#[derive(Debug, Clone)]
29#[non_exhaustive]
30pub struct DecodedPresentation {
31    /// The JWS header.
32    pub header: JwsHeader,
33    /// The claims set, `vp` included.
34    pub claims: JwtClaims,
35    /// The presentation, with `iss` and `jti` mapped back into it.
36    pub presentation: Presentation,
37    /// The presented credentials, in the order of `verifiableCredential`.
38    pub credentials: Vec<DecodedCredential>,
39}
40
41impl DecodedPresentation {
42    /// The holder: `holder` of the presentation, which decoding set from `iss`.
43    pub fn holder(&self) -> Option<&Uri> {
44        self.presentation.holder.as_ref().map(Party::id)
45    }
46}
47
48/// A validated or decoded VCDM 2.0 presentation JWT (`vp+jwt`): its header, its claims, the
49/// presentation, and its enveloped credentials unwrapped and decoded.
50#[derive(Debug, Clone)]
51#[non_exhaustive]
52pub struct DecodedPresentationV2 {
53    /// The JWS header.
54    pub header: JwsHeader,
55    /// The claims set, which is the presentation plus any registered claims.
56    pub claims: JwtClaims,
57    /// The presentation, with `iss` and `jti` mapped into `holder` and `id` when those are
58    /// absent.
59    pub presentation: PresentationV2,
60    /// The presented credentials, in the order of `verifiableCredential`, each in the form
61    /// its envelope held.
62    pub credentials: Vec<PresentedCredentialV2>,
63}
64
65impl DecodedPresentationV2 {
66    /// The holder: `holder` of the presentation, or `iss` when it has none.
67    pub fn holder(&self) -> Option<&Uri> {
68        self.presentation.holder.as_ref().map(Party::id)
69    }
70}
71
72/// A credential unwrapped from an `EnvelopedVerifiableCredential` of a VCDM 2.0
73/// presentation: secured as a JWT (`application/vc+jwt`) or, with the `sd-jwt` feature, with
74/// SD-JWT (`application/vc+sd-jwt`).
75#[derive(Debug, Clone)]
76#[non_exhaustive]
77pub enum PresentedCredentialV2 {
78    /// A `vc+jwt` credential, decoded.
79    Jwt(Box<DecodedCredentialV2>),
80    /// A `vc+sd-jwt` credential, with the disclosures it was presented with applied.
81    #[cfg(feature = "sd-jwt")]
82    #[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
83    SdJwt(Box<SdJwtCredentialV2>),
84}
85
86impl PresentedCredentialV2 {
87    /// The credential, whichever way it was secured.
88    pub fn credential(&self) -> &CredentialV2 {
89        match self {
90            Self::Jwt(decoded) => &decoded.credential,
91            #[cfg(feature = "sd-jwt")]
92            Self::SdJwt(credential) => credential.credential(),
93        }
94    }
95
96    /// The header of the JWT securing it (the issuer-signed JWT, for SD-JWT).
97    pub fn header(&self) -> &JwsHeader {
98        match self {
99            Self::Jwt(decoded) => &decoded.header,
100            #[cfg(feature = "sd-jwt")]
101            Self::SdJwt(credential) => credential.header(),
102        }
103    }
104
105    /// The decoded JWT, when the credential was secured as one.
106    pub fn as_jwt(&self) -> Option<&DecodedCredentialV2> {
107        match self {
108            Self::Jwt(decoded) => Some(decoded),
109            #[cfg(feature = "sd-jwt")]
110            Self::SdJwt(_) => None,
111        }
112    }
113
114    /// The SD-JWT credential, when the credential was secured with SD-JWT.
115    #[cfg(feature = "sd-jwt")]
116    #[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
117    pub fn as_sd_jwt(&self) -> Option<&SdJwtCredentialV2> {
118        match self {
119            Self::SdJwt(credential) => Some(credential),
120            Self::Jwt(_) => None,
121        }
122    }
123}
124
125/// Validates presentation JWTs and the credential JWTs they carry, see the [module
126/// documentation](super) for the checks.
127///
128/// Built over a [`JwtCredentialValidator`], which validates the credentials with the same key
129/// resolver, verifier and status check.
130///
131/// Each profile has its own method, as for credentials: [`validate`](Self::validate) for the
132/// VCDM 1.1 JWT profile (`typ` `JWT`, the `vp` claim, credentials as JWTs) and
133/// [`validate_v2`](Self::validate_v2) for VCDM 2.0 secured with VC-JOSE-COSE (`typ`
134/// `vp+jwt`, the payload is the presentation, credentials enveloped). Neither accepts the
135/// other's `typ`. A verifier that takes both picks by `typ`, and since `typ` is optional in
136/// both profiles, picks a token without one by its payload: VCDM 1.1 puts the presentation in
137/// a `vp` claim, VCDM 2.0 never has one.
138///
139/// ```
140/// # #[cfg(feature = "did-key")]
141/// # {
142/// use identity_kit::jose::DefaultVerifier;
143/// use identity_kit::methods::DidKey;
144/// use identity_kit::resolver::Resolver;
145/// use identity_kit::vc::validation::{JwtPresentationValidator, PresentationValidationOptions};
146/// use identity_kit::vc::{Jwt, JwtClaims, PresentationV2};
147///
148/// /// Whether `jwt` is a VCDM 2.0 presentation rather than a VCDM 1.1 one.
149/// fn is_v2(jwt: &Jwt) -> Result<bool, Box<dyn std::error::Error>> {
150///     let jws = jwt.decode()?;
151///     Ok(match jws.header().typ.as_deref() {
152///         Some(typ) => typ == PresentationV2::JWT_TYP,
153///         None => {
154///             let claims: JwtClaims = serde_json::from_slice(jws.payload())?;
155///             !claims.additional.contains_key("vp")
156///         }
157///     })
158/// }
159///
160/// async fn holder_of(
161///     validator: &JwtPresentationValidator<Resolver>,
162///     jwt: &Jwt,
163///     options: &PresentationValidationOptions,
164/// ) -> Result<Option<String>, Box<dyn std::error::Error>> {
165///     let holder = if is_v2(jwt)? {
166///         validator.validate_v2(jwt, options).await?.holder().cloned()
167///     } else {
168///         validator.validate(jwt, options).await?.holder().cloned()
169///     };
170///     Ok(holder.map(|holder| holder.to_string()))
171/// }
172///
173/// let resolver = Resolver::builder().with(DidKey::default()).build();
174/// let _validator = JwtPresentationValidator::new(resolver, DefaultVerifier);
175/// # }
176/// ```
177pub struct JwtPresentationValidator<R, V = DefaultVerifier> {
178    credentials: JwtCredentialValidator<R, V>,
179}
180
181impl<R: KeyResolver, V: JwsVerifier> JwtPresentationValidator<R, V> {
182    /// A validator resolving keys with `resolver` and verifying signatures with `verifier`,
183    /// without a status check.
184    pub fn new(resolver: R, verifier: V) -> Self {
185        Self {
186            credentials: JwtCredentialValidator::new(resolver, verifier),
187        }
188    }
189
190    /// A validator over `credentials`, sharing its resolver, verifier and status check.
191    pub fn from_credential_validator(credentials: JwtCredentialValidator<R, V>) -> Self {
192        Self { credentials }
193    }
194
195    /// This validator checking `credentialStatus` with `check`.
196    #[must_use]
197    pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
198        self.credentials = self.credentials.with_status_check(check);
199        self
200    }
201
202    /// The credential validator.
203    pub fn credential_validator(&self) -> &JwtCredentialValidator<R, V> {
204        &self.credentials
205    }
206
207    /// Validates a presentation JWT and every credential it carries.
208    ///
209    /// # Errors
210    ///
211    /// A [`PresentationValidationError`] naming what failed: the presentation (in which case
212    /// no credential was validated), or the credentials that failed, all of them under
213    /// [`FailFast::AllErrors`].
214    pub async fn validate(
215        &self,
216        jwt: &Jwt,
217        options: &PresentationValidationOptions,
218    ) -> Result<DecodedPresentation, PresentationValidationError> {
219        let mut decoded = self
220            .validate_presentation(jwt, options)
221            .await
222            .map_err(PresentationValidationError::presentation)?;
223        let holder = decoded.holder().cloned();
224        let mut errors = BTreeMap::new();
225        for (index, credential) in decoded
226            .presentation
227            .verifiable_credential
228            .iter()
229            .enumerate()
230        {
231            let result = self
232                .credentials
233                .validate(credential, &options.credentials)
234                .await
235                .and_then(|validated| {
236                    let credential = &validated.credential;
237                    check_subject_holder(
238                        &credential.properties,
239                        credential.credential_subject.iter(),
240                        holder.as_ref(),
241                        options.subject_holder,
242                    )?;
243                    Ok(validated)
244                });
245            match result {
246                Ok(validated) => decoded.credentials.push(validated),
247                Err(error) => {
248                    errors.insert(index, error);
249                    if options.fail_fast == FailFast::FirstError {
250                        break;
251                    }
252                }
253            }
254        }
255        if errors.is_empty() {
256            Ok(decoded)
257        } else {
258            Err(PresentationValidationError::credentials(errors))
259        }
260    }
261
262    /// Validates a VCDM 2.0 presentation JWT secured with VC-JOSE-COSE and every credential
263    /// it envelopes.
264    ///
265    /// The checks are those of [`validate`](Self::validate), with the profile's own shape:
266    /// `typ`, when present, is `vp+jwt` (or one of `options.additional_typ`); the payload is
267    /// the presentation, with `iss` and `jti` agreeing with `holder` and `id` and no `vp`
268    /// claim; the holder, from `holder` or else `iss`, is required and owns the signing key
269    /// ([`KeyReference::binds_to`](crate::resolver::KeyReference::binds_to)).
270    /// Each credential is unwrapped from its `EnvelopedVerifiableCredential` and validated
271    /// under `options.credentials`: an `application/vc+jwt` one with
272    /// [`JwtCredentialValidator::validate_v2`]; with the `sd-jwt` feature, an
273    /// `application/vc+sd-jwt` one as `SdJwtCredentialValidator::validate_v2` of `sd_jwt_vc`
274    /// does, with its `credentialStatus` entries and its `status` claim checked by this
275    /// validator's [`StatusCheck`] and the key binding of `options.key_binding` required when
276    /// set (none by default: the presentation JWT already proves holder control). An envelope
277    /// of another media type (a COSE credential) fails for that credential alone.
278    ///
279    /// # Errors
280    ///
281    /// As [`validate`](Self::validate).
282    pub async fn validate_v2(
283        &self,
284        jwt: &Jwt,
285        options: &PresentationValidationOptions,
286    ) -> Result<DecodedPresentationV2, PresentationValidationError> {
287        let mut decoded = self
288            .validate_presentation_v2(jwt, options)
289            .await
290            .map_err(PresentationValidationError::presentation)?;
291        let holder = decoded.holder().cloned();
292        let mut errors = BTreeMap::new();
293        for (index, enveloped) in decoded
294            .presentation
295            .verifiable_credential
296            .iter()
297            .enumerate()
298        {
299            match self
300                .validate_enveloped(enveloped, holder.as_ref(), options)
301                .await
302            {
303                Ok(validated) => decoded.credentials.push(validated),
304                Err(error) => {
305                    errors.insert(index, error);
306                    if options.fail_fast == FailFast::FirstError {
307                        break;
308                    }
309                }
310            }
311        }
312        if errors.is_empty() {
313            Ok(decoded)
314        } else {
315            Err(PresentationValidationError::credentials(errors))
316        }
317    }
318
319    /// Validates the credential `enveloped` holds, by its media type, and its subjects
320    /// against `holder`.
321    async fn validate_enveloped(
322        &self,
323        enveloped: &EnvelopedVc,
324        holder: Option<&Uri>,
325        options: &PresentationValidationOptions,
326    ) -> Result<PresentedCredentialV2, ValidationError> {
327        #[cfg(feature = "sd-jwt")]
328        if enveloped.is_sd_jwt() {
329            let sd_jwt = enveloped.to_sd_jwt()?;
330            let credential =
331                SdJwtCredentialV2::from_sd_jwt_with(sd_jwt, &options.credentials.additional_typ)
332                    .map_err(crate::sd_jwt_vc::SdJwtValidationError::from)?;
333            // No status policy here: the status is checked below, for both kinds of entry.
334            let sd_jwt_options = SdJwtValidationOptions {
335                time: options.credentials.time,
336                key_binding: options.key_binding.clone(),
337                ..SdJwtValidationOptions::default()
338            };
339            let validated = SdJwtCredentialValidator::new(
340                self.credentials.resolver(),
341                self.credentials.verifier(),
342            )
343            .validate_credential_v2_unchecked_status(credential, &sd_jwt_options)
344            .await?;
345            let credential = validated.credential;
346            self.credentials
347                .check_status(
348                    credential
349                        .credential()
350                        .statuses()
351                        .map(CredentialStatus::Vcdm)
352                        .chain(credential.status().map(CredentialStatus::StatusClaim))
353                        .collect(),
354                    options.credentials.status,
355                    options.credentials.time.now(),
356                )
357                .await?;
358            check_subject_holder(
359                &credential.credential().properties,
360                credential.credential().credential_subject.iter(),
361                holder,
362                options.subject_holder,
363            )?;
364            return Ok(PresentedCredentialV2::SdJwt(Box::new(credential)));
365        }
366        let jwt = enveloped.to_jwt()?;
367        let validated = self
368            .credentials
369            .validate_v2(&jwt, &options.credentials)
370            .await?;
371        check_subject_holder(
372            &validated.credential.properties,
373            validated.credential.credential_subject.iter(),
374            holder,
375            options.subject_holder,
376        )?;
377        Ok(PresentedCredentialV2::Jwt(Box::new(validated)))
378    }
379
380    /// Validates the presentation JWT itself; the credentials are left to decode.
381    async fn validate_presentation(
382        &self,
383        jwt: &Jwt,
384        options: &PresentationValidationOptions,
385    ) -> Result<DecodedPresentation, ValidationError> {
386        let jws = jwt.decode()?;
387        check_typ(jws.header(), "JWT", &options.additional_typ)?;
388        let claims = plain_claims(&jws)?;
389        let presentation = Presentation::from_jwt_claims(&claims)?;
390        self.check_holder_and_claims(&jws, presentation.holder.as_ref(), "iss", &claims, options)
391            .await?;
392        Ok(DecodedPresentation {
393            header: jws.header().clone(),
394            claims,
395            presentation,
396            credentials: Vec::new(),
397        })
398    }
399
400    /// Validates the VCDM 2.0 presentation JWT itself; the credentials are left to unwrap.
401    async fn validate_presentation_v2(
402        &self,
403        jwt: &Jwt,
404        options: &PresentationValidationOptions,
405    ) -> Result<DecodedPresentationV2, ValidationError> {
406        let jws = jwt.decode()?;
407        let (claims, presentation) = decode_payload_v2(&jws, &options.additional_typ)?;
408        self.check_holder_and_claims(
409            &jws,
410            presentation.holder.as_ref(),
411            "holder",
412            &claims,
413            options,
414        )
415        .await?;
416        Ok(DecodedPresentationV2 {
417            header: jws.header().clone(),
418            claims,
419            presentation,
420            credentials: Vec::new(),
421        })
422    }
423
424    /// The checks both profiles share once the payload is decoded: the signature under a key
425    /// resolved for [`KeyRole::Subject`], the key belongs to the holder (reported missing as
426    /// `missing`), the time claims, the audience and the nonce.
427    async fn check_holder_and_claims(
428        &self,
429        jws: &CompactJws,
430        holder: Option<&Party>,
431        missing: &'static str,
432        claims: &JwtClaims,
433        options: &PresentationValidationOptions,
434    ) -> Result<(), ValidationError> {
435        let (reference, key) = self
436            .credentials
437            .verify(
438                jws,
439                KeyRole::Subject,
440                holder.map(|holder| holder.id().as_str()),
441            )
442            .await?;
443        let holder = holder.ok_or(CredentialError::MissingClaim(missing))?;
444        check_controller(holder, &reference, &key, "holder")?;
445        options.time.check_claims(claims)?;
446        if let Some(expected) = &options.audience
447            && !claims.audiences().any(|aud| aud == expected)
448        {
449            return Err(ValidationError::AudienceMismatch {
450                expected: expected.clone(),
451                found: claims.aud.clone(),
452            });
453        }
454        if let Some(expected) = &options.nonce {
455            let found = claims.additional.get("nonce").and_then(Value::as_str);
456            if found != Some(expected.as_str()) {
457                return Err(ValidationError::NonceMismatch {
458                    expected: expected.clone(),
459                    found: found.map(str::to_owned),
460                });
461            }
462        }
463        Ok(())
464    }
465}
466
467impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtPresentationValidator<R, V> {
468    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
469        f.debug_struct("JwtPresentationValidator")
470            .field("credentials", &self.credentials)
471            .finish()
472    }
473}
474
475/// Decodes a VCDM 1.1 presentation JWT and its credentials without verifying or validating
476/// anything.
477///
478/// # Errors
479///
480/// [`ValidationError::Jws`], [`ValidationError::Payload`], [`ValidationError::UnexpectedTyp`]
481/// or [`ValidationError::Credential`] when the token or one of its credentials does not decode,
482/// and [`ValidationError::SdJwtPayload`] when one of them is the issuer-signed JWT of an
483/// SD-JWT.
484pub fn decode_presentation(jwt: &Jwt) -> Result<DecodedPresentation, ValidationError> {
485    decode_presentation_with(jwt, &NO_TYP, &NO_TYP)
486}
487
488/// [`decode_presentation`] also accepting `presentation_typ` on the presentation and
489/// `credential_typ` on its credentials.
490pub(crate) fn decode_presentation_with(
491    jwt: &Jwt,
492    presentation_typ: &BTreeSet<String>,
493    credential_typ: &BTreeSet<String>,
494) -> Result<DecodedPresentation, ValidationError> {
495    let jws = jwt.decode()?;
496    check_typ(jws.header(), "JWT", presentation_typ)?;
497    let claims = plain_claims(&jws)?;
498    let presentation = Presentation::from_jwt_claims(&claims)?;
499    let credentials = presentation
500        .verifiable_credential
501        .iter()
502        .map(|credential| decode(&credential.decode()?, credential_typ))
503        .collect::<Result<_, _>>()?;
504    Ok(DecodedPresentation {
505        header: jws.header().clone(),
506        claims,
507        presentation,
508        credentials,
509    })
510}
511
512/// Decodes a VCDM 2.0 presentation JWT and unwraps and decodes its credentials without
513/// verifying or validating anything. A credential secured with SD-JWT (feature `sd-jwt`) has
514/// its disclosures processed, which checks the digests, but no signature is verified.
515///
516/// # Errors
517///
518/// [`ValidationError::Jws`], [`ValidationError::Payload`], [`ValidationError::UnexpectedTyp`]
519/// or [`ValidationError::Credential`] when the token, or one of its credentials, does not
520/// decode, and `ValidationError::SdJwt` when an SD-JWT credential does not; an envelope of
521/// a media type other than `application/vc+jwt` (or `application/vc+sd-jwt`) is a
522/// [`CredentialError::UnsupportedMediaType`]. A `vp+jwt` payload, or a `vc+jwt` one, that is
523/// the issuer-signed JWT of an SD-JWT is a [`ValidationError::SdJwtPayload`].
524pub fn decode_presentation_v2(jwt: &Jwt) -> Result<DecodedPresentationV2, ValidationError> {
525    let jws = jwt.decode()?;
526    let (claims, presentation) = decode_payload_v2(&jws, &NO_TYP)?;
527    let credentials = presentation
528        .verifiable_credential
529        .iter()
530        .map(decode_enveloped)
531        .collect::<Result<_, _>>()?;
532    Ok(DecodedPresentationV2 {
533        header: jws.header().clone(),
534        claims,
535        presentation,
536        credentials,
537    })
538}
539
540/// Decodes the credential `enveloped` holds, by its media type.
541fn decode_enveloped(enveloped: &EnvelopedVc) -> Result<PresentedCredentialV2, ValidationError> {
542    #[cfg(feature = "sd-jwt")]
543    if enveloped.is_sd_jwt() {
544        let credential = SdJwtCredentialV2::from_sd_jwt(enveloped.to_sd_jwt()?)
545            .map_err(crate::sd_jwt_vc::SdJwtValidationError::from)?;
546        return Ok(PresentedCredentialV2::SdJwt(Box::new(credential)));
547    }
548    Ok(PresentedCredentialV2::Jwt(Box::new(decode_v2(
549        &enveloped.to_jwt()?.decode()?,
550        &NO_TYP,
551    )?)))
552}
553
554/// The claims and presentation of a VCDM 2.0 presentation JWT whose `typ`, when present, is
555/// `vp+jwt` or one of `additional_typ`.
556fn decode_payload_v2(
557    jws: &CompactJws,
558    additional_typ: &BTreeSet<String>,
559) -> Result<(JwtClaims, PresentationV2), ValidationError> {
560    check_typ(jws.header(), PresentationV2::JWT_TYP, additional_typ)?;
561    let claims = plain_claims(jws)?;
562    let presentation = PresentationV2::from_jwt_claims(&claims)?;
563    Ok((claims, presentation))
564}
565
566/// Checks `subjects` of a credential with `properties` against `holder` per `relationship`.
567fn check_subject_holder<'a>(
568    properties: &IndexMap<String, Value>,
569    subjects: impl Iterator<Item = &'a Subject>,
570    holder: Option<&Uri>,
571    relationship: SubjectHolderRelationship,
572) -> Result<(), ValidationError> {
573    let required = match relationship {
574        SubjectHolderRelationship::AlwaysSubject => true,
575        SubjectHolderRelationship::SubjectOnNonTransferable => properties
576            .get("nonTransferable")
577            .is_some_and(|value| value == &Value::Bool(true)),
578        SubjectHolderRelationship::Any => false,
579    };
580    if !required {
581        return Ok(());
582    }
583    // The presentation validator sets the holder before validating credentials.
584    let holder = holder.expect("a validated presentation has a holder");
585    for subject in subjects {
586        if subject.id.as_ref() != Some(holder) {
587            return Err(ValidationError::SubjectHolderMismatch {
588                subject: subject.id.clone(),
589                holder: holder.clone(),
590            });
591        }
592    }
593    Ok(())
594}