Skip to main content

identity_kit/vc/validation/
presentation.rs

1//! `JwtPresentationValidator`.
2
3use std::collections::BTreeMap;
4
5use serde_json::Value;
6
7use crate::did::{Uri, VerificationRelationship};
8use crate::jose::{DefaultVerifier, JwsHeader, JwsVerifier};
9use crate::vc::{Jwt, JwtClaims, Party, Presentation};
10
11use super::credential::{check_controller, check_typ, decode};
12use super::{
13    DecodedCredential, FailFast, JwtCredentialValidator, KeyResolver, PresentationValidationError,
14    PresentationValidationOptions, StatusCheck, SubjectHolderRelationship, ValidationError,
15};
16
17/// A validated or decoded VCDM 1.1 presentation JWT: its header, its claims (custom ones
18/// included), the presentation, and its credentials decoded.
19#[derive(Debug, Clone)]
20#[non_exhaustive]
21pub struct DecodedPresentation {
22    /// The JWS header.
23    pub header: JwsHeader,
24    /// The claims set, `vp` included.
25    pub claims: JwtClaims,
26    /// The presentation, with `iss` and `jti` mapped back into it.
27    pub presentation: Presentation,
28    /// The presented credentials, in the order of `verifiableCredential`.
29    pub credentials: Vec<DecodedCredential>,
30}
31
32impl DecodedPresentation {
33    /// The holder: `holder` of the presentation, which decoding set from `iss`.
34    pub fn holder(&self) -> Option<&Uri> {
35        self.presentation.holder.as_ref().map(Party::id)
36    }
37}
38
39/// Validates presentation JWTs and the credential JWTs they carry, see the [module
40/// documentation](super) for the checks.
41///
42/// Built over a [`JwtCredentialValidator`], which validates the credentials with the same key
43/// resolver, verifier and status check.
44pub struct JwtPresentationValidator<R, V = DefaultVerifier> {
45    credentials: JwtCredentialValidator<R, V>,
46}
47
48impl<R: KeyResolver, V: JwsVerifier> JwtPresentationValidator<R, V> {
49    /// A validator resolving keys with `resolver` and verifying signatures with `verifier`,
50    /// without a status check.
51    pub fn new(resolver: R, verifier: V) -> Self {
52        Self {
53            credentials: JwtCredentialValidator::new(resolver, verifier),
54        }
55    }
56
57    /// A validator over `credentials`, sharing its resolver, verifier and status check.
58    pub fn from_credential_validator(credentials: JwtCredentialValidator<R, V>) -> Self {
59        Self { credentials }
60    }
61
62    /// This validator checking `credentialStatus` with `check`.
63    #[must_use]
64    pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
65        self.credentials = self.credentials.with_status_check(check);
66        self
67    }
68
69    /// The credential validator.
70    pub fn credential_validator(&self) -> &JwtCredentialValidator<R, V> {
71        &self.credentials
72    }
73
74    /// Validates a presentation JWT and every credential it carries.
75    ///
76    /// # Errors
77    ///
78    /// A [`PresentationValidationError`] naming what failed: the presentation (in which case
79    /// no credential was validated), or the credentials that failed, all of them under
80    /// [`FailFast::AllErrors`].
81    pub async fn validate(
82        &self,
83        jwt: &Jwt,
84        options: &PresentationValidationOptions,
85    ) -> Result<DecodedPresentation, PresentationValidationError> {
86        let mut decoded = self
87            .validate_presentation(jwt, options)
88            .await
89            .map_err(PresentationValidationError::presentation)?;
90        let holder = decoded.holder().cloned();
91        let mut errors = BTreeMap::new();
92        for (index, credential) in decoded
93            .presentation
94            .verifiable_credential
95            .iter()
96            .enumerate()
97        {
98            let result = self
99                .credentials
100                .validate(credential, &options.credentials)
101                .await
102                .and_then(|validated| {
103                    check_subject_holder(&validated, holder.as_ref(), options.subject_holder)?;
104                    Ok(validated)
105                });
106            match result {
107                Ok(validated) => decoded.credentials.push(validated),
108                Err(error) => {
109                    errors.insert(index, error);
110                    if options.fail_fast == FailFast::FirstError {
111                        break;
112                    }
113                }
114            }
115        }
116        if errors.is_empty() {
117            Ok(decoded)
118        } else {
119            Err(PresentationValidationError::credentials(errors))
120        }
121    }
122
123    /// Validates the presentation JWT itself; the credentials are left to decode.
124    async fn validate_presentation(
125        &self,
126        jwt: &Jwt,
127        options: &PresentationValidationOptions,
128    ) -> Result<DecodedPresentation, ValidationError> {
129        let jws = jwt.decode()?;
130        check_typ(jws.header(), "JWT")?;
131        let claims: JwtClaims = serde_json::from_slice(jws.payload())
132            .map_err(|e| ValidationError::Payload(e.to_string()))?;
133        let presentation = Presentation::from_jwt_claims(&claims)?;
134        let kid = self
135            .credentials
136            .verify(&jws, VerificationRelationship::Authentication)
137            .await?;
138        let holder = presentation
139            .holder
140            .as_ref()
141            .ok_or(crate::vc::CredentialError::MissingClaim("iss"))?;
142        check_controller(holder, &kid, "holder")?;
143        options.time.check_claims(&claims)?;
144        if let Some(expected) = &options.audience
145            && !claims.audiences().any(|aud| aud == expected)
146        {
147            return Err(ValidationError::AudienceMismatch {
148                expected: expected.clone(),
149                found: claims.aud.clone(),
150            });
151        }
152        if let Some(expected) = &options.nonce {
153            let found = claims.additional.get("nonce").and_then(Value::as_str);
154            if found != Some(expected.as_str()) {
155                return Err(ValidationError::NonceMismatch {
156                    expected: expected.clone(),
157                    found: found.map(str::to_owned),
158                });
159            }
160        }
161        Ok(DecodedPresentation {
162            header: jws.header().clone(),
163            claims,
164            presentation,
165            credentials: Vec::new(),
166        })
167    }
168}
169
170impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtPresentationValidator<R, V> {
171    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
172        f.debug_struct("JwtPresentationValidator")
173            .field("credentials", &self.credentials)
174            .finish()
175    }
176}
177
178/// Decodes a VCDM 1.1 presentation JWT and its credentials without verifying or validating
179/// anything.
180///
181/// # Errors
182///
183/// [`ValidationError::Jws`], [`ValidationError::Payload`], [`ValidationError::UnexpectedTyp`]
184/// or [`ValidationError::Credential`] when the token or one of its credentials does not decode.
185pub fn decode_presentation(jwt: &Jwt) -> Result<DecodedPresentation, ValidationError> {
186    let jws = jwt.decode()?;
187    check_typ(jws.header(), "JWT")?;
188    let claims: JwtClaims = serde_json::from_slice(jws.payload())
189        .map_err(|e| ValidationError::Payload(e.to_string()))?;
190    let presentation = Presentation::from_jwt_claims(&claims)?;
191    let credentials = presentation
192        .verifiable_credential
193        .iter()
194        .map(|credential| decode(&credential.decode()?))
195        .collect::<Result<_, _>>()?;
196    Ok(DecodedPresentation {
197        header: jws.header().clone(),
198        claims,
199        presentation,
200        credentials,
201    })
202}
203
204/// Checks the subjects of `credential` against `holder` per `relationship`.
205fn check_subject_holder(
206    credential: &DecodedCredential,
207    holder: Option<&Uri>,
208    relationship: SubjectHolderRelationship,
209) -> Result<(), ValidationError> {
210    let required = match relationship {
211        SubjectHolderRelationship::AlwaysSubject => true,
212        SubjectHolderRelationship::SubjectOnNonTransferable => credential
213            .credential
214            .properties
215            .get("nonTransferable")
216            .is_some_and(|value| value == &Value::Bool(true)),
217        SubjectHolderRelationship::Any => false,
218    };
219    if !required {
220        return Ok(());
221    }
222    // The presentation validator sets the holder before validating credentials.
223    let holder = holder.expect("a validated presentation has a holder");
224    for subject in credential.credential.credential_subject.iter() {
225        if subject.id.as_ref() != Some(holder) {
226            return Err(ValidationError::SubjectHolderMismatch {
227                subject: subject.id.clone(),
228                holder: holder.clone(),
229            });
230        }
231    }
232    Ok(())
233}