identity_kit/vc/validation/
credential.rs1use std::collections::BTreeSet;
4
5use crate::did::Uri;
6use crate::jose::{CompactJws, DefaultVerifier, Jwk, JwsHeader, JwsVerifier};
7use crate::vc::{Credential, CredentialV2, Jwt, JwtClaims, Party};
8
9use chrono::{DateTime, Utc};
10use serde_json::Value;
11
12use super::{
13 CredentialStatus, CredentialValidationOptions, KeyReference, KeyResolver, KeyRole, StatusCheck,
14 StatusPolicy, ValidationError, check_statuses, status_claim, status_claim_entry,
15};
16
17#[derive(Debug, Clone)]
20#[non_exhaustive]
21pub struct DecodedCredential {
22 pub header: JwsHeader,
24 pub claims: JwtClaims,
26 pub credential: Credential,
28}
29
30#[derive(Debug, Clone)]
32#[non_exhaustive]
33pub struct DecodedCredentialV2 {
34 pub header: JwsHeader,
36 pub claims: JwtClaims,
38 pub credential: CredentialV2,
40}
41
42pub struct JwtCredentialValidator<R, V = DefaultVerifier> {
103 resolver: R,
104 verifier: V,
105 status: Option<Box<dyn StatusCheck>>,
106}
107
108impl<R: KeyResolver, V: JwsVerifier> JwtCredentialValidator<R, V> {
109 pub fn new(resolver: R, verifier: V) -> Self {
112 Self {
113 resolver,
114 verifier,
115 status: None,
116 }
117 }
118
119 #[must_use]
121 pub fn with_status_check(mut self, check: impl StatusCheck + 'static) -> Self {
122 self.status = Some(Box::new(check));
123 self
124 }
125
126 pub fn resolver(&self) -> &R {
128 &self.resolver
129 }
130
131 pub fn verifier(&self) -> &V {
133 &self.verifier
134 }
135
136 pub fn has_status_check(&self) -> bool {
138 self.status.is_some()
139 }
140
141 pub async fn validate(
148 &self,
149 jwt: &Jwt,
150 options: &CredentialValidationOptions,
151 ) -> Result<DecodedCredential, ValidationError> {
152 let jws = jwt.decode()?;
153 let decoded = decode(&jws, &options.additional_typ)?;
154 let issuer = &decoded.credential.issuer;
155 let (reference, key) = self
156 .verify(&jws, KeyRole::Issuer, Some(issuer.id().as_str()))
157 .await?;
158 check_controller(issuer, &reference, &key, "issuer")?;
159 options.time.check_claims(&decoded.claims)?;
160 options.time.check(
161 Some(decoded.credential.issuance_date),
162 decoded.credential.expiration_date,
163 None,
164 )?;
165 let status = status_claim(&decoded.claims, options.status)?;
166 self.check_status(
167 decoded
168 .credential
169 .credential_status
170 .iter()
171 .map(CredentialStatus::Vcdm)
172 .chain(status.iter().map(status_claim_entry))
173 .collect(),
174 options.status,
175 options.time.now(),
176 )
177 .await?;
178 Ok(decoded)
179 }
180
181 pub async fn validate_v2(
187 &self,
188 jwt: &Jwt,
189 options: &CredentialValidationOptions,
190 ) -> Result<DecodedCredentialV2, ValidationError> {
191 let jws = jwt.decode()?;
192 let decoded = decode_v2(&jws, &options.additional_typ)?;
193 let issuer = &decoded.credential.issuer;
194 let (reference, key) = self
195 .verify(&jws, KeyRole::Issuer, Some(issuer.id().as_str()))
196 .await?;
197 check_controller(issuer, &reference, &key, "issuer")?;
198 options.time.check_claims(&decoded.claims)?;
199 options.time.check(
200 decoded.credential.valid_from,
201 decoded.credential.valid_until,
202 None,
203 )?;
204 let status = status_claim(&decoded.claims, options.status)?;
205 self.check_status(
206 decoded
207 .credential
208 .statuses()
209 .map(CredentialStatus::Vcdm)
210 .chain(status.iter().map(status_claim_entry))
211 .collect(),
212 options.status,
213 options.time.now(),
214 )
215 .await?;
216 Ok(decoded)
217 }
218
219 pub(super) async fn verify(
222 &self,
223 jws: &CompactJws,
224 role: KeyRole,
225 iss: Option<&str>,
226 ) -> Result<(KeyReference, Jwk), ValidationError> {
227 let reference = KeyReference::from_jws(jws, iss)?;
228 let key = self
229 .resolver
230 .resolve_key(&reference, role)
231 .await
232 .map_err(|source| ValidationError::KeyResolution {
233 key: Box::new(reference.clone()),
234 source,
235 })?;
236 jws.verify(&self.verifier, &key)?;
237 Ok((reference, key))
238 }
239
240 pub(super) async fn check_status(
242 &self,
243 statuses: Vec<CredentialStatus<'_>>,
244 policy: StatusPolicy,
245 now: DateTime<Utc>,
246 ) -> Result<(), ValidationError> {
247 check_statuses(self.status.as_deref(), statuses, policy, now).await
248 }
249}
250
251impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtCredentialValidator<R, V> {
252 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253 f.debug_struct("JwtCredentialValidator")
254 .field("resolver", &self.resolver)
255 .field("verifier", &self.verifier)
256 .field("status", &self.status.as_ref().map(|_| "StatusCheck"))
257 .finish()
258 }
259}
260
261pub fn decode_credential(jwt: &Jwt) -> Result<DecodedCredential, ValidationError> {
269 decode(&jwt.decode()?, &NO_TYP)
270}
271
272pub fn decode_credential_v2(jwt: &Jwt) -> Result<DecodedCredentialV2, ValidationError> {
278 decode_v2(&jwt.decode()?, &NO_TYP)
279}
280
281pub(super) static NO_TYP: BTreeSet<String> = BTreeSet::new();
283
284pub(super) fn decode(
287 jws: &CompactJws,
288 additional_typ: &BTreeSet<String>,
289) -> Result<DecodedCredential, ValidationError> {
290 check_typ(jws.header(), "JWT", additional_typ)?;
291 let claims = plain_claims(jws)?;
292 let credential = Credential::from_jwt_claims(&claims)?;
293 Ok(DecodedCredential {
294 header: jws.header().clone(),
295 claims,
296 credential,
297 })
298}
299
300pub(super) fn decode_v2(
304 jws: &CompactJws,
305 additional_typ: &BTreeSet<String>,
306) -> Result<DecodedCredentialV2, ValidationError> {
307 check_typ(jws.header(), CredentialV2::JWT_TYP, additional_typ)?;
308 let claims = plain_claims(jws)?;
309 let credential = CredentialV2::from_jwt_claims(&claims)?;
310 Ok(DecodedCredentialV2 {
311 header: jws.header().clone(),
312 claims,
313 credential,
314 })
315}
316
317pub(super) fn plain_claims(jws: &CompactJws) -> Result<JwtClaims, ValidationError> {
320 let claims: JwtClaims =
321 serde_json::from_slice(jws.payload()).map_err(ValidationError::Payload)?;
322 match claims
323 .additional
324 .iter()
325 .find_map(|(name, value)| sd_jwt_member(name, value))
326 {
327 Some(claim) => Err(ValidationError::SdJwtPayload { claim }),
328 None => Ok(claims),
329 }
330}
331
332const SD_JWT_NAMES: [&str; 2] = ["_sd", "_sd_alg"];
335const ELLIPSIS: &str = "...";
336
337fn sd_jwt_member(name: &str, value: &Value) -> Option<&'static str> {
340 SD_JWT_NAMES
341 .into_iter()
342 .find(|reserved| *reserved == name)
343 .or_else(|| sd_jwt_name_in(value))
344}
345
346fn sd_jwt_name_in(value: &Value) -> Option<&'static str> {
348 match value {
349 Value::Object(object) => object
350 .iter()
351 .find_map(|(name, value)| sd_jwt_member(name, value)),
352 Value::Array(array) => array.iter().find_map(|element| match element {
353 Value::Object(object)
354 if object.len() == 1 && object.get(ELLIPSIS).is_some_and(Value::is_string) =>
355 {
356 Some(ELLIPSIS)
357 }
358 element => sd_jwt_name_in(element),
359 }),
360 _ => None,
361 }
362}
363
364pub(super) fn check_typ(
366 header: &JwsHeader,
367 expected: &'static str,
368 additional: &BTreeSet<String>,
369) -> Result<(), ValidationError> {
370 match header.typ.as_deref() {
371 Some(found) if found != expected && !additional.contains(found) => {
372 Err(ValidationError::UnexpectedTyp {
373 expected,
374 found: found.to_owned(),
375 })
376 }
377 _ => Ok(()),
378 }
379}
380
381pub(super) fn check_controller(
384 party: &Party,
385 reference: &KeyReference,
386 key: &Jwk,
387 role: &'static str,
388) -> Result<(), ValidationError> {
389 let id = party.id();
390 if !reference.binds_to(Some(id.as_str()), key) {
391 return Err(mismatch(role, id.clone(), reference.clone()));
392 }
393 Ok(())
394}
395
396fn mismatch(role: &'static str, id: Uri, key: KeyReference) -> ValidationError {
398 if role == "holder" {
399 ValidationError::HolderMismatch {
400 holder: id,
401 key: Box::new(key),
402 }
403 } else {
404 ValidationError::IssuerMismatch {
405 issuer: id,
406 key: Box::new(key),
407 }
408 }
409}