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