Skip to main content

identity_kit/vc/validation/
error.rs

1//! Validation errors.
2
3use core::fmt;
4use std::collections::BTreeMap;
5
6use chrono::{DateTime, Utc};
7
8use crate::did::{OneOrMany, Uri};
9use crate::jose::{JwsError, VerificationError};
10use crate::resolver::{KeyReference, KeyReferenceError, KeyResolutionError};
11use crate::vc::CredentialError;
12
13/// Why a credential or presentation JWT did not validate.
14#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum ValidationError {
17    /// The token is not a compact JWS this crate decodes.
18    #[error("invalid JWS: {0}")]
19    Jws(#[from] JwsError),
20    /// The JWS header names no key this crate can look for: no `kid`, a `kid` that is not a
21    /// DID URL, a DID URL with a path or DID parameters, a relative `kid` without a DID issuer.
22    #[error(transparent)]
23    KeyReference(#[from] KeyReferenceError),
24    /// The header `typ` is not the one the profile requires.
25    #[error("header `typ` is {found:?}, not `{expected}`")]
26    UnexpectedTyp {
27        /// The `typ` the profile requires.
28        expected: &'static str,
29        /// The `typ` found.
30        found: String,
31    },
32    /// The payload is not a JSON claims set.
33    #[error("the payload is not a JWT claims set: {0}")]
34    Payload(String),
35    /// The claims do not map to a credential or presentation.
36    #[error(transparent)]
37    Credential(#[from] CredentialError),
38    /// The payload carries `_sd_alg`, `_sd` or an array element `{"...": "<digest>"}`: it is
39    /// the issuer-signed JWT of an SD-JWT, whose claims mean something else until its
40    /// disclosures are processed, so it is not read as a plain JWT (RFC 8725 §3.12).
41    /// `SdJwtCredentialValidator` of `sd_jwt_vc` validates it.
42    #[error("the payload carries `{claim}`, so it is an SD-JWT, not a plain JWT")]
43    SdJwtPayload {
44        /// The claim found: `_sd_alg`, `_sd` or `...`.
45        claim: &'static str,
46    },
47    /// The key the token names could not be resolved.
48    #[error("resolving key {key}: {source}")]
49    KeyResolution {
50        /// Where the token says its key is.
51        key: Box<KeyReference>,
52        /// What went wrong.
53        #[source]
54        source: KeyResolutionError,
55    },
56    /// The signature does not verify under the resolved key.
57    #[error("signature: {0}")]
58    Signature(#[from] VerificationError),
59    /// The issuer of the credential does not control the key that signed it.
60    #[error("issuer `{issuer}` does not control signing key {key}")]
61    IssuerMismatch {
62        /// The issuer of the credential.
63        issuer: Uri,
64        /// Where the token says its key is.
65        key: Box<KeyReference>,
66    },
67    /// The issuer is not a DID, so no key of it can be resolved.
68    #[error("{role} `{id}` is not a DID")]
69    NotDid {
70        /// `issuer` or `holder`.
71        role: &'static str,
72        /// The identifier.
73        id: String,
74    },
75    /// The holder of the presentation does not control the key that signed it.
76    #[error("holder `{holder}` does not control signing key {key}")]
77    HolderMismatch {
78        /// The holder of the presentation.
79        holder: Uri,
80        /// Where the token says its key is.
81        key: Box<KeyReference>,
82    },
83    /// The token or credential is not valid yet.
84    #[error("not valid before {} (now is {})", not_before.to_rfc3339(), now.to_rfc3339())]
85    NotYetValid {
86        /// When it becomes valid.
87        not_before: DateTime<Utc>,
88        /// The validation time.
89        now: DateTime<Utc>,
90    },
91    /// The token or credential has expired.
92    #[error("expired at {} (now is {})", expired_at.to_rfc3339(), now.to_rfc3339())]
93    Expired {
94        /// When it expired.
95        expired_at: DateTime<Utc>,
96        /// The validation time.
97        now: DateTime<Utc>,
98    },
99    /// The token claims to have been issued in the future.
100    #[error("issued at {}, after now ({})", issued_at.to_rfc3339(), now.to_rfc3339())]
101    IssuedInFuture {
102        /// The `iat`.
103        issued_at: DateTime<Utc>,
104        /// The validation time.
105        now: DateTime<Utc>,
106    },
107    /// The `aud` does not include the expected audience.
108    #[error("audience {found:?} does not include {expected:?}")]
109    AudienceMismatch {
110        /// The audience expected.
111        expected: String,
112        /// The `aud` found.
113        found: Option<OneOrMany<String>>,
114    },
115    /// The `nonce` is not the expected one.
116    #[error("nonce {found:?} is not {expected:?}")]
117    NonceMismatch {
118        /// The nonce expected.
119        expected: String,
120        /// The `nonce` found.
121        found: Option<String>,
122    },
123    /// The credential has a `credentialStatus` and the validator has no
124    /// [`StatusCheck`](super::StatusCheck) for it.
125    #[error("`credentialStatus` of type `{0}` cannot be checked")]
126    StatusUnchecked(String),
127    /// The status check failed or found the credential revoked or suspended.
128    #[error("credential status: {0}")]
129    Status(#[from] StatusError),
130    /// The subject of a presented credential is not the holder presenting it.
131    #[error("credential subject {subject:?} is not the holder `{holder}`")]
132    SubjectHolderMismatch {
133        /// The `id` of the subject, if any.
134        subject: Option<Uri>,
135        /// The holder of the presentation.
136        holder: Uri,
137    },
138    /// A credential secured with SD-JWT, enveloped in a presentation, failed a check of its
139    /// own: it is not a well-formed SD-JWT credential, or its key binding is missing or wrong.
140    /// (A check shared with the JWT validators is reported as that check's own variant.)
141    #[cfg(feature = "sd-jwt")]
142    #[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
143    #[error(transparent)]
144    SdJwt(Box<crate::sd_jwt_vc::SdJwtValidationError>),
145}
146
147#[cfg(feature = "sd-jwt")]
148impl From<crate::sd_jwt_vc::SdJwtValidationError> for ValidationError {
149    /// Unwraps a shared check's error; boxes the SD-JWT-specific ones.
150    fn from(error: crate::sd_jwt_vc::SdJwtValidationError) -> Self {
151        match error {
152            crate::sd_jwt_vc::SdJwtValidationError::Validation(error) => error,
153            other => Self::SdJwt(Box::new(other)),
154        }
155    }
156}
157
158/// Why a [`StatusCheck`] did not pass.
159///
160/// [`StatusCheck`]: super::StatusCheck
161#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
162#[non_exhaustive]
163pub enum StatusError {
164    /// The check does not understand the status type.
165    #[error("status type `{0}` is not supported")]
166    Unsupported(String),
167    /// The status could not be obtained: the list could not be fetched, say.
168    #[error("status unavailable: {0}")]
169    Unavailable(String),
170    /// The credential's status makes it unusable: `revoked`, `suspended`, …
171    #[error("credential is {0}")]
172    Invalid(String),
173}
174
175/// Why a presentation did not validate: the presentation itself, or some of its credentials.
176#[derive(Debug, thiserror::Error)]
177#[non_exhaustive]
178pub struct PresentationValidationError {
179    /// What is wrong with the presentation JWT; when set, no credential was validated.
180    pub presentation: Option<Box<ValidationError>>,
181    /// What is wrong with each failing credential, by its position in
182    /// `verifiableCredential`.
183    pub credentials: BTreeMap<usize, ValidationError>,
184}
185
186impl PresentationValidationError {
187    /// An error of the presentation itself.
188    pub fn presentation(error: ValidationError) -> Self {
189        Self {
190            presentation: Some(Box::new(error)),
191            credentials: BTreeMap::new(),
192        }
193    }
194
195    /// An error of the credentials at the given positions.
196    pub fn credentials(errors: BTreeMap<usize, ValidationError>) -> Self {
197        Self {
198            presentation: None,
199            credentials: errors,
200        }
201    }
202}
203
204impl fmt::Display for PresentationValidationError {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        if let Some(error) = &self.presentation {
207            return write!(f, "presentation: {error}");
208        }
209        for (i, (index, error)) in self.credentials.iter().enumerate() {
210            if i > 0 {
211                f.write_str("; ")?;
212            }
213            write!(f, "credential {index}: {error}")?;
214        }
215        Ok(())
216    }
217}