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::cause::cause;
9use crate::did::{OneOrMany, Uri};
10use crate::jose::{JwsError, VerificationError};
11use crate::resolver::{KeyReference, KeyReferenceError, KeyResolutionError};
12use crate::vc::CredentialError;
13
14/// Why a credential or presentation JWT did not validate.
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum ValidationError {
18 /// The token is not a compact JWS this crate decodes.
19 #[error("invalid JWS: {0}")]
20 Jws(#[from] JwsError),
21 /// The JWS header names no key this crate can look for: no `kid`, a `kid` that is not a
22 /// DID URL, a DID URL with a path or DID parameters, a relative `kid` without a DID issuer.
23 #[error(transparent)]
24 KeyReference(#[from] KeyReferenceError),
25 /// The header `typ` is not the one the profile requires.
26 #[error("header `typ` is {found:?}, not `{expected}`")]
27 UnexpectedTyp {
28 /// The `typ` the profile requires.
29 expected: &'static str,
30 /// The `typ` found.
31 found: String,
32 },
33 /// The payload is not a JSON claims set; the source is the JSON error.
34 #[error("the payload is not a JWT claims set: {0}")]
35 Payload(#[source] serde_json::Error),
36 /// The claims do not map to a credential or presentation.
37 #[error(transparent)]
38 Credential(#[from] CredentialError),
39 /// The payload carries `_sd_alg`, `_sd` or an array element `{"...": "<digest>"}`: it is
40 /// the issuer-signed JWT of an SD-JWT, whose claims mean something else until its
41 /// disclosures are processed, so it is not read as a plain JWT (RFC 8725 §3.12).
42 /// `SdJwtCredentialValidator` of `sd_jwt_vc` validates it.
43 #[error("the payload carries `{claim}`, so it is an SD-JWT, not a plain JWT")]
44 SdJwtPayload {
45 /// The claim found: `_sd_alg`, `_sd` or `...`.
46 claim: &'static str,
47 },
48 /// The key the token names could not be resolved.
49 #[error("resolving key {key}: {source}")]
50 KeyResolution {
51 /// Where the token says its key is.
52 key: Box<KeyReference>,
53 /// What went wrong.
54 #[source]
55 source: KeyResolutionError,
56 },
57 /// The signature does not verify under the resolved key.
58 #[error("signature: {0}")]
59 Signature(#[from] VerificationError),
60 /// The issuer of the credential does not control the key that signed it.
61 #[error("issuer `{issuer}` does not control signing key {key}")]
62 IssuerMismatch {
63 /// The issuer of the credential.
64 issuer: Uri,
65 /// Where the token says its key is.
66 key: Box<KeyReference>,
67 },
68 /// The issuer is not a DID, so no key of it can be resolved.
69 #[error("{role} `{id}` is not a DID")]
70 NotDid {
71 /// `issuer` or `holder`.
72 role: &'static str,
73 /// The identifier.
74 id: String,
75 },
76 /// The holder of the presentation does not control the key that signed it.
77 #[error("holder `{holder}` does not control signing key {key}")]
78 HolderMismatch {
79 /// The holder of the presentation.
80 holder: Uri,
81 /// Where the token says its key is.
82 key: Box<KeyReference>,
83 },
84 /// The token or credential is not valid yet.
85 #[error("not valid before {} (now is {})", not_before.to_rfc3339(), now.to_rfc3339())]
86 NotYetValid {
87 /// When it becomes valid.
88 not_before: DateTime<Utc>,
89 /// The validation time.
90 now: DateTime<Utc>,
91 },
92 /// The token or credential has expired.
93 #[error("expired at {} (now is {})", expired_at.to_rfc3339(), now.to_rfc3339())]
94 Expired {
95 /// When it expired.
96 expired_at: DateTime<Utc>,
97 /// The validation time.
98 now: DateTime<Utc>,
99 },
100 /// The token claims to have been issued in the future.
101 #[error("issued at {}, after now ({})", issued_at.to_rfc3339(), now.to_rfc3339())]
102 IssuedInFuture {
103 /// The `iat`.
104 issued_at: DateTime<Utc>,
105 /// The validation time.
106 now: DateTime<Utc>,
107 },
108 /// The `aud` does not include the expected audience.
109 #[error("audience {found:?} does not include {expected:?}")]
110 AudienceMismatch {
111 /// The audience expected.
112 expected: String,
113 /// The `aud` found.
114 found: Option<OneOrMany<String>>,
115 },
116 /// The `nonce` is not the expected one.
117 #[error("nonce {found:?} is not {expected:?}")]
118 NonceMismatch {
119 /// The nonce expected.
120 expected: String,
121 /// The `nonce` found.
122 found: Option<String>,
123 },
124 /// The credential has a `credentialStatus` and the validator has no
125 /// [`StatusCheck`](super::StatusCheck) for it.
126 #[error("`credentialStatus` of type `{0}` cannot be checked")]
127 StatusUnchecked(String),
128 /// The status check failed or found the credential revoked or suspended.
129 #[error("credential status: {0}")]
130 Status(#[from] StatusError),
131 /// The subject of a presented credential is not the holder presenting it.
132 #[error("credential subject {subject:?} is not the holder `{holder}`")]
133 SubjectHolderMismatch {
134 /// The `id` of the subject, if any.
135 subject: Option<Uri>,
136 /// The holder of the presentation.
137 holder: Uri,
138 },
139 /// A credential secured with SD-JWT, enveloped in a presentation, failed a check of its
140 /// own: it is not a well-formed SD-JWT credential, or its key binding is missing or wrong.
141 /// (A check shared with the JWT validators is reported as that check's own variant.)
142 #[cfg(feature = "sd-jwt")]
143 #[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
144 #[error(transparent)]
145 SdJwt(Box<crate::sd_jwt_vc::SdJwtValidationError>),
146}
147
148#[cfg(feature = "sd-jwt")]
149impl From<crate::sd_jwt_vc::SdJwtValidationError> for ValidationError {
150 /// Unwraps a shared check's error; boxes the SD-JWT-specific ones.
151 fn from(error: crate::sd_jwt_vc::SdJwtValidationError) -> Self {
152 match error {
153 crate::sd_jwt_vc::SdJwtValidationError::Validation(error) => error,
154 other => Self::SdJwt(Box::new(other)),
155 }
156 }
157}
158
159/// Why a [`StatusCheck`] did not pass.
160///
161/// The variants tell apart what a caller may want to handle differently: an
162/// [`Unavailable`](Self::Unavailable) list may be fetched on a later attempt, an
163/// [`Expired`](Self::Expired) one may be valid once its provider has refreshed it, and an
164/// [`UnresolvedKey`](Self::UnresolvedKey) depends on why the resolver failed, which its source
165/// says. An [`InvalidList`](Self::InvalidList) or an
166/// [`IndexOutOfRange`](Self::IndexOutOfRange) will not pass by trying again.
167///
168/// Where a variant has a `source`, [`Error::source`](std::error::Error::source) returns it,
169/// and the message ends with it.
170///
171/// [`StatusCheck`]: super::StatusCheck
172#[derive(Debug, thiserror::Error)]
173#[non_exhaustive]
174pub enum StatusError {
175 /// The check does not understand the status type.
176 #[error("status type `{0}` is not supported")]
177 Unsupported(String),
178 /// The status list could not be fetched: the request failed, the server answered with an
179 /// error status, or the check refused a redirect. The source is the client's error, when
180 /// there is one.
181 #[error("status list {uri} unavailable: {reason}{}", cause(.source.as_ref()))]
182 Unavailable {
183 /// The status list URI the credential references.
184 uri: String,
185 /// What went wrong, naming the URL of a redirect hop where it is not `uri`.
186 reason: String,
187 /// The underlying error.
188 source: Option<Box<dyn std::error::Error + Send + Sync>>,
189 },
190 /// The reference or the status list is not one the check can read or trust: a URI that is
191 /// not a URL, a body that is not a status list token, a `sub` other than the URI, a
192 /// signature that does not verify, a key that is not the issuer's. The source is the
193 /// underlying error, when there is one.
194 #[error("status list {uri} is invalid: {reason}{}", cause(.source.as_ref()))]
195 InvalidList {
196 /// The status list URI the credential references.
197 uri: String,
198 /// What is wrong.
199 reason: String,
200 /// The underlying error.
201 source: Option<Box<dyn std::error::Error + Send + Sync>>,
202 },
203 /// The key that signed the status list could not be resolved; the source is the
204 /// resolver's error.
205 #[error("status list {uri}: cannot resolve key {key}: {source}")]
206 UnresolvedKey {
207 /// The status list URI the credential references.
208 uri: String,
209 /// The key reference, as the token's header names it.
210 key: String,
211 /// The resolver's error.
212 source: KeyResolutionError,
213 },
214 /// The status list has expired.
215 #[error(
216 "status list {uri} expired at {} (now is {})",
217 expired_at.to_rfc3339(),
218 now.to_rfc3339()
219 )]
220 Expired {
221 /// The status list URI the credential references.
222 uri: String,
223 /// Its `exp`.
224 expired_at: DateTime<Utc>,
225 /// The validation time.
226 now: DateTime<Utc>,
227 },
228 /// The credential's index is beyond the end of the status list.
229 #[error("index {idx} is out of range of status list {uri} of {len} entries")]
230 IndexOutOfRange {
231 /// The status list URI the credential references.
232 uri: String,
233 /// The credential's index.
234 idx: u64,
235 /// The number of entries in the list.
236 len: usize,
237 },
238 /// The credential's status makes it unusable: `revoked`, `suspended`, …
239 #[error("credential is {0}")]
240 Invalid(String),
241}
242
243/// Why a presentation did not validate: the presentation itself, or some of its credentials.
244#[derive(Debug, thiserror::Error)]
245#[non_exhaustive]
246pub struct PresentationValidationError {
247 /// What is wrong with the presentation JWT; when set, no credential was validated.
248 pub presentation: Option<Box<ValidationError>>,
249 /// What is wrong with each failing credential, by its position in
250 /// `verifiableCredential`.
251 pub credentials: BTreeMap<usize, ValidationError>,
252}
253
254impl PresentationValidationError {
255 /// An error of the presentation itself.
256 pub fn presentation(error: ValidationError) -> Self {
257 Self {
258 presentation: Some(Box::new(error)),
259 credentials: BTreeMap::new(),
260 }
261 }
262
263 /// An error of the credentials at the given positions.
264 pub fn credentials(errors: BTreeMap<usize, ValidationError>) -> Self {
265 Self {
266 presentation: None,
267 credentials: errors,
268 }
269 }
270}
271
272impl fmt::Display for PresentationValidationError {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 if let Some(error) = &self.presentation {
275 return write!(f, "presentation: {error}");
276 }
277 for (i, (index, error)) in self.credentials.iter().enumerate() {
278 if i > 0 {
279 f.write_str("; ")?;
280 }
281 write!(f, "credential {index}: {error}")?;
282 }
283 Ok(())
284 }
285}