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 options.status,
174 options.time.now(),
175 )
176 .await?;
177 Ok(decoded)
178 }
179
180 pub async fn validate_v2(
186 &self,
187 jwt: &Jwt,
188 options: &CredentialValidationOptions,
189 ) -> Result<DecodedCredentialV2, ValidationError> {
190 let jws = jwt.decode()?;
191 let decoded = decode_v2(&jws, &options.additional_typ)?;
192 let issuer = &decoded.credential.issuer;
193 let (reference, key) = self
194 .verify(&jws, KeyRole::Issuer, Some(issuer.id().as_str()))
195 .await?;
196 check_controller(issuer, &reference, &key, "issuer")?;
197 options.time.check_claims(&decoded.claims)?;
198 options.time.check(
199 decoded.credential.valid_from,
200 decoded.credential.valid_until,
201 None,
202 )?;
203 let status = status_claim(&decoded.claims, options.status)?;
204 self.check_status(
205 decoded
206 .credential
207 .statuses()
208 .map(CredentialStatus::Vcdm)
209 .chain(status.iter().map(status_claim_entry)),
210 options.status,
211 options.time.now(),
212 )
213 .await?;
214 Ok(decoded)
215 }
216
217 pub(super) async fn verify(
220 &self,
221 jws: &CompactJws,
222 role: KeyRole,
223 iss: Option<&str>,
224 ) -> Result<(KeyReference, Jwk), ValidationError> {
225 let reference = KeyReference::from_jws(jws, iss)?;
226 let key = self
227 .resolver
228 .resolve_key(&reference, role)
229 .await
230 .map_err(|source| ValidationError::KeyResolution {
231 key: Box::new(reference.clone()),
232 source,
233 })?;
234 jws.verify(&self.verifier, &key)?;
235 Ok((reference, key))
236 }
237
238 pub(super) async fn check_status<'a>(
240 &self,
241 statuses: impl IntoIterator<Item = CredentialStatus<'a>>,
242 policy: StatusPolicy,
243 now: DateTime<Utc>,
244 ) -> Result<(), ValidationError> {
245 check_statuses(self.status.as_deref(), statuses, policy, now).await
246 }
247}
248
249impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for JwtCredentialValidator<R, V> {
250 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
251 f.debug_struct("JwtCredentialValidator")
252 .field("resolver", &self.resolver)
253 .field("verifier", &self.verifier)
254 .field("status", &self.status.as_ref().map(|_| "StatusCheck"))
255 .finish()
256 }
257}
258
259pub fn decode_credential(jwt: &Jwt) -> Result<DecodedCredential, ValidationError> {
267 decode(&jwt.decode()?, &NO_TYP)
268}
269
270pub fn decode_credential_v2(jwt: &Jwt) -> Result<DecodedCredentialV2, ValidationError> {
276 decode_v2(&jwt.decode()?, &NO_TYP)
277}
278
279pub(super) static NO_TYP: BTreeSet<String> = BTreeSet::new();
281
282pub(super) fn decode(
285 jws: &CompactJws,
286 additional_typ: &BTreeSet<String>,
287) -> Result<DecodedCredential, ValidationError> {
288 check_typ(jws.header(), "JWT", additional_typ)?;
289 let claims = plain_claims(jws)?;
290 let credential = Credential::from_jwt_claims(&claims)?;
291 Ok(DecodedCredential {
292 header: jws.header().clone(),
293 claims,
294 credential,
295 })
296}
297
298pub(super) fn decode_v2(
302 jws: &CompactJws,
303 additional_typ: &BTreeSet<String>,
304) -> Result<DecodedCredentialV2, ValidationError> {
305 check_typ(jws.header(), CredentialV2::JWT_TYP, additional_typ)?;
306 let claims = plain_claims(jws)?;
307 let credential = CredentialV2::from_jwt_claims(&claims)?;
308 Ok(DecodedCredentialV2 {
309 header: jws.header().clone(),
310 claims,
311 credential,
312 })
313}
314
315pub(super) fn plain_claims(jws: &CompactJws) -> Result<JwtClaims, ValidationError> {
318 let claims: JwtClaims = serde_json::from_slice(jws.payload())
319 .map_err(|e| ValidationError::Payload(e.to_string()))?;
320 match claims
321 .additional
322 .iter()
323 .find_map(|(name, value)| sd_jwt_member(name, value))
324 {
325 Some(claim) => Err(ValidationError::SdJwtPayload { claim }),
326 None => Ok(claims),
327 }
328}
329
330const SD_JWT_NAMES: [&str; 2] = ["_sd", "_sd_alg"];
333const ELLIPSIS: &str = "...";
334
335fn sd_jwt_member(name: &str, value: &Value) -> Option<&'static str> {
338 SD_JWT_NAMES
339 .into_iter()
340 .find(|reserved| *reserved == name)
341 .or_else(|| sd_jwt_name_in(value))
342}
343
344fn sd_jwt_name_in(value: &Value) -> Option<&'static str> {
346 match value {
347 Value::Object(object) => object
348 .iter()
349 .find_map(|(name, value)| sd_jwt_member(name, value)),
350 Value::Array(array) => array.iter().find_map(|element| match element {
351 Value::Object(object)
352 if object.len() == 1 && object.get(ELLIPSIS).is_some_and(Value::is_string) =>
353 {
354 Some(ELLIPSIS)
355 }
356 element => sd_jwt_name_in(element),
357 }),
358 _ => None,
359 }
360}
361
362pub(super) fn check_typ(
364 header: &JwsHeader,
365 expected: &'static str,
366 additional: &BTreeSet<String>,
367) -> Result<(), ValidationError> {
368 match header.typ.as_deref() {
369 Some(found) if found != expected && !additional.contains(found) => {
370 Err(ValidationError::UnexpectedTyp {
371 expected,
372 found: found.to_owned(),
373 })
374 }
375 _ => Ok(()),
376 }
377}
378
379pub(super) fn check_controller(
382 party: &Party,
383 reference: &KeyReference,
384 key: &Jwk,
385 role: &'static str,
386) -> Result<(), ValidationError> {
387 let id = party.id();
388 if !reference.binds_to(Some(id.as_str()), key) {
389 return Err(mismatch(role, id.clone(), reference.clone()));
390 }
391 Ok(())
392}
393
394fn mismatch(role: &'static str, id: Uri, key: KeyReference) -> ValidationError {
396 if role == "holder" {
397 ValidationError::HolderMismatch {
398 holder: id,
399 key: Box::new(key),
400 }
401 } else {
402 ValidationError::IssuerMismatch {
403 issuer: id,
404 key: Box::new(key),
405 }
406 }
407}