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            let sd_jwt_options = SdJwtValidationOptions {
334                time: options.credentials.time,
335                key_binding: options.key_binding.clone(),
336                status: options.credentials.status,
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::SdJwt)),
353                    options.credentials.status,
354                    options.credentials.time.now(),
355                )
356                .await?;
357            check_subject_holder(
358                &credential.credential().properties,
359                credential.credential().credential_subject.iter(),
360                holder,
361                options.subject_holder,
362            )?;
363            return Ok(PresentedCredentialV2::SdJwt(Box::new(credential)));
364        }
365        let jwt = enveloped.to_jwt()?;
366        let validated = self
367            .credentials
368            .validate_v2(&jwt, &options.credentials)
369            .await?;
370        check_subject_holder(
371            &validated.credential.properties,
372            validated.credential.credential_subject.iter(),
373            holder,
374            options.subject_holder,
375        )?;
376        Ok(PresentedCredentialV2::Jwt(Box::new(validated)))
377    }
378
379    /// Validates the presentation JWT itself; the credentials are left to decode.
380    async fn validate_presentation(
381        &self,
382        jwt: &Jwt,
383        options: &PresentationValidationOptions,
384    ) -> Result<DecodedPresentation, ValidationError> {
385        let jws = jwt.decode()?;
386        check_typ(jws.header(), "JWT", &options.additional_typ)?;
387        let claims = plain_claims(&jws)?;
388        let presentation = Presentation::from_jwt_claims(&claims)?;
389        self.check_holder_and_claims(&jws, presentation.holder.as_ref(), "iss", &claims, options)
390            .await?;
391        Ok(DecodedPresentation {
392            header: jws.header().clone(),
393            claims,
394            presentation,
395            credentials: Vec::new(),
396        })
397    }
398
399    /// Validates the VCDM 2.0 presentation JWT itself; the credentials are left to unwrap.
400    async fn validate_presentation_v2(
401        &self,
402        jwt: &Jwt,
403        options: &PresentationValidationOptions,
404    ) -> Result<DecodedPresentationV2, ValidationError> {
405        let jws = jwt.decode()?;
406        let (claims, presentation) = decode_payload_v2(&jws, &options.additional_typ)?;
407        self.check_holder_and_claims(
408            &jws,
409            presentation.holder.as_ref(),
410            "holder",
411            &claims,
412            options,
413        )
414        .await?;
415        Ok(DecodedPresentationV2 {
416            header: jws.header().clone(),
417            claims,
418            presentation,
419            credentials: Vec::new(),
420        })
421    }
422
423    /// The checks both profiles share once the payload is decoded: the signature under a key
424    /// resolved for [`KeyRole::Subject`], the key belongs to the holder (reported missing as
425    /// `missing`), the time claims, the audience and the nonce.
426    async fn check_holder_and_claims(
427        &self,
428        jws: &CompactJws,
429        holder: Option<&Party>,
430        missing: &'static str,
431        claims: &JwtClaims,
432        options: &PresentationValidationOptions,
433    ) -> Result<(), ValidationError> {
434        let (reference, key) = self
435            .credentials
436            .verify(
437                jws,
438                KeyRole::Subject,
439                holder.map(|holder| holder.id().as_str()),
440            )
441            .await?;
442        let holder = holder.ok_or(CredentialError::MissingClaim(missing))?;
443        check_controller(holder, &reference, &key, "holder")?;
444        options.time.check_claims(claims)?;
445        if let Some(expected) = &options.audience
446            && !claims.audiences().any(|aud| aud == expected)
447        {
448            return Err(ValidationError::AudienceMismatch {
449                expected: expected.clone(),
450                found: claims.aud.clone(),
451            });
452        }
453        if let Some(expected) = &options.nonce {
454            let found = claims.additional.get("nonce").and_then(Value::as_str);
455            if found != Some(expected.as_str()) {
456                return Err(ValidationError::NonceMismatch {
457                    expected: expected.clone(),
458                    found: found.map(str::to_owned),
459                });
460            }
461        }
462        Ok(())
463    }
464}
465
466impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtPresentationValidator<R, V> {
467    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
468        f.debug_struct("JwtPresentationValidator")
469            .field("credentials", &self.credentials)
470            .finish()
471    }
472}
473
474/// Decodes a VCDM 1.1 presentation JWT and its credentials without verifying or validating
475/// anything.
476///
477/// # Errors
478///
479/// [`ValidationError::Jws`], [`ValidationError::Payload`], [`ValidationError::UnexpectedTyp`]
480/// or [`ValidationError::Credential`] when the token or one of its credentials does not decode,
481/// and [`ValidationError::SdJwtPayload`] when one of them is the issuer-signed JWT of an
482/// SD-JWT.
483pub fn decode_presentation(jwt: &Jwt) -> Result<DecodedPresentation, ValidationError> {
484    decode_presentation_with(jwt, &NO_TYP, &NO_TYP)
485}
486
487/// [`decode_presentation`] also accepting `presentation_typ` on the presentation and
488/// `credential_typ` on its credentials.
489pub(crate) fn decode_presentation_with(
490    jwt: &Jwt,
491    presentation_typ: &BTreeSet<String>,
492    credential_typ: &BTreeSet<String>,
493) -> Result<DecodedPresentation, ValidationError> {
494    let jws = jwt.decode()?;
495    check_typ(jws.header(), "JWT", presentation_typ)?;
496    let claims = plain_claims(&jws)?;
497    let presentation = Presentation::from_jwt_claims(&claims)?;
498    let credentials = presentation
499        .verifiable_credential
500        .iter()
501        .map(|credential| decode(&credential.decode()?, credential_typ))
502        .collect::<Result<_, _>>()?;
503    Ok(DecodedPresentation {
504        header: jws.header().clone(),
505        claims,
506        presentation,
507        credentials,
508    })
509}
510
511/// Decodes a VCDM 2.0 presentation JWT and unwraps and decodes its credentials without
512/// verifying or validating anything. A credential secured with SD-JWT (feature `sd-jwt`) has
513/// its disclosures processed, which checks the digests, but no signature is verified.
514///
515/// # Errors
516///
517/// [`ValidationError::Jws`], [`ValidationError::Payload`], [`ValidationError::UnexpectedTyp`]
518/// or [`ValidationError::Credential`] when the token, or one of its credentials, does not
519/// decode, and `ValidationError::SdJwt` when an SD-JWT credential does not; an envelope of
520/// a media type other than `application/vc+jwt` (or `application/vc+sd-jwt`) is a
521/// [`CredentialError::UnsupportedMediaType`]. A `vp+jwt` payload, or a `vc+jwt` one, that is
522/// the issuer-signed JWT of an SD-JWT is a [`ValidationError::SdJwtPayload`].
523pub fn decode_presentation_v2(jwt: &Jwt) -> Result<DecodedPresentationV2, ValidationError> {
524    let jws = jwt.decode()?;
525    let (claims, presentation) = decode_payload_v2(&jws, &NO_TYP)?;
526    let credentials = presentation
527        .verifiable_credential
528        .iter()
529        .map(decode_enveloped)
530        .collect::<Result<_, _>>()?;
531    Ok(DecodedPresentationV2 {
532        header: jws.header().clone(),
533        claims,
534        presentation,
535        credentials,
536    })
537}
538
539/// Decodes the credential `enveloped` holds, by its media type.
540fn decode_enveloped(enveloped: &EnvelopedVc) -> Result<PresentedCredentialV2, ValidationError> {
541    #[cfg(feature = "sd-jwt")]
542    if enveloped.is_sd_jwt() {
543        let credential = SdJwtCredentialV2::from_sd_jwt(enveloped.to_sd_jwt()?)
544            .map_err(crate::sd_jwt_vc::SdJwtValidationError::from)?;
545        return Ok(PresentedCredentialV2::SdJwt(Box::new(credential)));
546    }
547    Ok(PresentedCredentialV2::Jwt(Box::new(decode_v2(
548        &enveloped.to_jwt()?.decode()?,
549        &NO_TYP,
550    )?)))
551}
552
553/// The claims and presentation of a VCDM 2.0 presentation JWT whose `typ`, when present, is
554/// `vp+jwt` or one of `additional_typ`.
555fn decode_payload_v2(
556    jws: &CompactJws,
557    additional_typ: &BTreeSet<String>,
558) -> Result<(JwtClaims, PresentationV2), ValidationError> {
559    check_typ(jws.header(), PresentationV2::JWT_TYP, additional_typ)?;
560    let claims = plain_claims(jws)?;
561    let presentation = PresentationV2::from_jwt_claims(&claims)?;
562    Ok((claims, presentation))
563}
564
565/// Checks `subjects` of a credential with `properties` against `holder` per `relationship`.
566fn check_subject_holder<'a>(
567    properties: &IndexMap<String, Value>,
568    subjects: impl Iterator<Item = &'a Subject>,
569    holder: Option<&Uri>,
570    relationship: SubjectHolderRelationship,
571) -> Result<(), ValidationError> {
572    let required = match relationship {
573        SubjectHolderRelationship::AlwaysSubject => true,
574        SubjectHolderRelationship::SubjectOnNonTransferable => properties
575            .get("nonTransferable")
576            .is_some_and(|value| value == &Value::Bool(true)),
577        SubjectHolderRelationship::Any => false,
578    };
579    if !required {
580        return Ok(());
581    }
582    // The presentation validator sets the holder before validating credentials.
583    let holder = holder.expect("a validated presentation has a holder");
584    for subject in subjects {
585        if subject.id.as_ref() != Some(holder) {
586            return Err(ValidationError::SubjectHolderMismatch {
587                subject: subject.id.clone(),
588                holder: holder.clone(),
589            });
590        }
591    }
592    Ok(())
593}