1use core::time::Duration;
24
25use chrono::{DateTime, Utc};
26
27use super::{SdJwt, SdJwtVc, SdJwtVcError, Status};
28use crate::did::{Did, DidUrl, Uri, VerificationRelationship};
29use crate::jose::{DefaultVerifier, JwsVerifier};
30use crate::vc::validation::{KeyResolver, StatusError, StatusPolicy, TimeOptions, ValidationError};
31use crate::vc::{CredentialError, JwtClaims};
32
33#[cfg(feature = "did-web")]
34pub use status_list::StatusListCheck;
35
36#[async_trait::async_trait]
43pub trait SdJwtStatusCheck: Send + Sync {
44 async fn check(&self, status: &Status, now: DateTime<Utc>) -> Result<(), StatusError>;
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56#[non_exhaustive]
57pub struct KeyBindingOptions {
58 pub audience: String,
60 pub nonce: String,
62 pub time: TimeOptions,
64 pub max_age: Option<Duration>,
66}
67
68impl KeyBindingOptions {
69 pub fn new(audience: impl Into<String>, nonce: impl Into<String>) -> Self {
72 Self {
73 audience: audience.into(),
74 nonce: nonce.into(),
75 time: TimeOptions::default(),
76 max_age: None,
77 }
78 }
79
80 #[must_use]
82 pub fn with_time(mut self, time: TimeOptions) -> Self {
83 self.time = time;
84 self
85 }
86
87 #[must_use]
89 pub fn with_max_age(mut self, max_age: Duration) -> Self {
90 self.max_age = Some(max_age);
91 self
92 }
93}
94
95#[derive(Debug, Clone, Default, PartialEq, Eq)]
97#[non_exhaustive]
98pub struct SdJwtValidationOptions {
99 pub time: TimeOptions,
101 pub key_binding: Option<KeyBindingOptions>,
103 pub status: StatusPolicy,
105 pub reject_legacy_typ: bool,
107}
108
109impl SdJwtValidationOptions {
110 pub fn new() -> Self {
113 Self::default()
114 }
115
116 #[must_use]
118 pub fn with_time(mut self, time: TimeOptions) -> Self {
119 self.time = time;
120 self
121 }
122
123 #[must_use]
125 pub fn with_key_binding(mut self, key_binding: KeyBindingOptions) -> Self {
126 self.key_binding = Some(key_binding);
127 self
128 }
129
130 #[must_use]
132 pub fn with_status(mut self, status: StatusPolicy) -> Self {
133 self.status = status;
134 self
135 }
136
137 #[must_use]
139 pub fn rejecting_legacy_typ(mut self) -> Self {
140 self.reject_legacy_typ = true;
141 self
142 }
143}
144
145#[derive(Debug, thiserror::Error)]
147#[non_exhaustive]
148pub enum SdJwtValidationError {
149 #[error(transparent)]
151 Credential(#[from] SdJwtVcError),
152 #[error("header `typ` is the pre-standard `vc+sd-jwt`")]
154 LegacyTyp,
155 #[error(transparent)]
158 Validation(#[from] ValidationError),
159 #[error("key binding is required, but the SD-JWT has no key binding JWT")]
161 KeyBindingRequired,
162 #[error("key binding is required, but `cnf` carries no `jwk`")]
164 NoHolderKey,
165 #[error("key binding JWT issued at {} is older than {max_age:?} (now is {})", issued_at.to_rfc3339(), now.to_rfc3339())]
167 KeyBindingTooOld {
168 issued_at: DateTime<Utc>,
170 now: DateTime<Utc>,
172 max_age: Duration,
174 },
175 #[error("key binding `sd_hash` {found:?} is not the digest {expected:?} of the SD-JWT")]
177 SdHashMismatch {
178 expected: String,
180 found: Option<String>,
182 },
183}
184
185#[derive(Debug, Clone)]
187#[non_exhaustive]
188pub struct ValidatedSdJwtVc {
189 pub credential: SdJwtVc,
191 pub kid: DidUrl,
193 pub key_binding: Option<JwtClaims>,
195}
196
197pub struct SdJwtCredentialValidator<R, V = DefaultVerifier> {
199 resolver: R,
200 verifier: V,
201 status: Option<Box<dyn SdJwtStatusCheck>>,
202}
203
204impl<R: KeyResolver, V: JwsVerifier> SdJwtCredentialValidator<R, V> {
205 pub fn new(resolver: R, verifier: V) -> Self {
208 Self {
209 resolver,
210 verifier,
211 status: None,
212 }
213 }
214
215 #[must_use]
217 pub fn with_status_check(mut self, check: impl SdJwtStatusCheck + 'static) -> Self {
218 self.status = Some(Box::new(check));
219 self
220 }
221
222 pub fn resolver(&self) -> &R {
224 &self.resolver
225 }
226
227 pub fn verifier(&self) -> &V {
229 &self.verifier
230 }
231
232 pub fn has_status_check(&self) -> bool {
234 self.status.is_some()
235 }
236
237 pub async fn validate(
244 &self,
245 sd_jwt: &SdJwt,
246 options: &SdJwtValidationOptions,
247 ) -> Result<ValidatedSdJwtVc, SdJwtValidationError> {
248 let credential = SdJwtVc::from_sd_jwt(sd_jwt.clone())?;
249 self.validate_credential(credential, options).await
250 }
251
252 pub async fn validate_credential(
258 &self,
259 credential: SdJwtVc,
260 options: &SdJwtValidationOptions,
261 ) -> Result<ValidatedSdJwtVc, SdJwtValidationError> {
262 if options.reject_legacy_typ && credential.has_legacy_typ() {
263 return Err(SdJwtValidationError::LegacyTyp);
264 }
265 let jws = credential
266 .sd_jwt()
267 .jwt()
268 .decode()
269 .map_err(ValidationError::from)?;
270 let kid = jws.kid().ok_or(ValidationError::MissingKid)?;
271 let kid = DidUrl::parse(kid).map_err(|source| ValidationError::InvalidKid {
272 kid: kid.to_owned(),
273 source,
274 })?;
275 if kid.path().is_some() || kid.query().is_some() {
276 return Err(ValidationError::KidWithParameters(kid).into());
277 }
278 let key = self
279 .resolver
280 .resolve_key(&kid, VerificationRelationship::AssertionMethod)
281 .await
282 .map_err(|source| ValidationError::KeyResolution {
283 kid: kid.clone(),
284 source,
285 })?;
286 jws.verify(&self.verifier, &key)
287 .map_err(ValidationError::from)?;
288
289 let claims = credential.claims();
290 let issuer = claims
291 .iss
292 .as_deref()
293 .ok_or(CredentialError::MissingClaim("iss"))
294 .map_err(ValidationError::from)?;
295 let issuer_did = Did::parse(issuer).map_err(|_| ValidationError::NotDid {
296 role: "issuer",
297 id: issuer.to_owned(),
298 })?;
299 if issuer_did != *kid.did() {
300 return Err(ValidationError::IssuerMismatch {
301 issuer: Uri::from(issuer_did),
302 kid,
303 }
304 .into());
305 }
306
307 let mut time_claims = JwtClaims::new();
308 time_claims.nbf = claims.nbf;
309 time_claims.exp = claims.exp;
310 time_claims.iat = claims.iat;
311 options.time.check_claims(&time_claims)?;
312
313 let key_binding = match &options.key_binding {
314 Some(required) => Some(self.check_key_binding(&credential, required)?),
315 None => None,
316 };
317
318 if let Some(status) = &claims.status {
319 self.check_status(status, options).await?;
320 }
321
322 Ok(ValidatedSdJwtVc {
323 credential,
324 kid,
325 key_binding,
326 })
327 }
328
329 fn check_key_binding(
331 &self,
332 credential: &SdJwtVc,
333 required: &KeyBindingOptions,
334 ) -> Result<JwtClaims, SdJwtValidationError> {
335 let sd_jwt = credential.sd_jwt();
336 let kb_jwt = sd_jwt
337 .key_binding_jwt()
338 .ok_or(SdJwtValidationError::KeyBindingRequired)?;
339 let holder_key = credential
340 .holder_key()
341 .ok_or(SdJwtValidationError::NoHolderKey)?;
342 let jws = kb_jwt.decode().map_err(ValidationError::from)?;
343 match jws.header().typ.as_deref() {
344 Some(super::KB_JWT_TYP) => {}
345 found => {
346 return Err(ValidationError::UnexpectedTyp {
347 expected: super::KB_JWT_TYP,
348 found: found.unwrap_or("").to_owned(),
349 }
350 .into());
351 }
352 }
353 jws.verify(&self.verifier, holder_key)
354 .map_err(ValidationError::from)?;
355 let claims: JwtClaims = serde_json::from_slice(jws.payload())
356 .map_err(|e| ValidationError::Payload(e.to_string()))?;
357
358 if !claims.audiences().any(|aud| aud == required.audience) {
359 return Err(ValidationError::AudienceMismatch {
360 expected: required.audience.clone(),
361 found: claims.aud.clone(),
362 }
363 .into());
364 }
365 let nonce = claims.additional.get("nonce").and_then(|v| v.as_str());
366 if nonce != Some(required.nonce.as_str()) {
367 return Err(ValidationError::NonceMismatch {
368 expected: required.nonce.clone(),
369 found: nonce.map(str::to_owned),
370 }
371 .into());
372 }
373 let iat = claims
374 .iat
375 .ok_or(CredentialError::MissingClaim("iat"))
376 .map_err(ValidationError::from)?;
377 let issued_at = JwtClaims::timestamp(iat).ok_or_else(|| {
378 ValidationError::from(CredentialError::InvalidClaim {
379 claim: "iat",
380 reason: format!("{iat} is out of range"),
381 })
382 })?;
383 required.time.check(None, None, Some(issued_at))?;
384 if let Some(max_age) = required.max_age {
385 let now = required.time.now();
386 let oldest = chrono::TimeDelta::from_std(max_age)
388 .ok()
389 .and_then(|delta| now.checked_sub_signed(delta))
390 .unwrap_or(DateTime::<Utc>::MIN_UTC);
391 if issued_at < oldest {
392 return Err(SdJwtValidationError::KeyBindingTooOld {
393 issued_at,
394 now,
395 max_age,
396 });
397 }
398 }
399 let alg = sd_jwt.hash_algorithm().map_err(SdJwtVcError::from)?;
400 let expected = sd_jwt.sd_hash(alg);
401 let found = claims.additional.get("sd_hash").and_then(|v| v.as_str());
402 if found != Some(expected.as_str()) {
403 return Err(SdJwtValidationError::SdHashMismatch {
404 expected,
405 found: found.map(str::to_owned),
406 });
407 }
408 Ok(claims)
409 }
410
411 async fn check_status(
413 &self,
414 status: &Status,
415 options: &SdJwtValidationOptions,
416 ) -> Result<(), SdJwtValidationError> {
417 if options.status == StatusPolicy::SkipAll {
418 return Ok(());
419 }
420 let names = || {
421 status
422 .mechanisms()
423 .map(|m| match m {
424 super::StatusMechanism::StatusList(_) => "status_list".to_owned(),
425 super::StatusMechanism::Other { name, .. } => name.to_owned(),
426 })
427 .collect::<Vec<_>>()
428 .join(", ")
429 };
430 let result = match &self.status {
431 Some(check) => check.check(status, options.time.now()).await,
432 None => Err(StatusError::Unsupported(names())),
433 };
434 match result {
435 Ok(()) => Ok(()),
436 Err(StatusError::Unsupported(_)) if options.status == StatusPolicy::SkipUnsupported => {
437 Ok(())
438 }
439 Err(StatusError::Unsupported(mechanism)) => {
440 Err(ValidationError::StatusUnchecked(mechanism).into())
441 }
442 Err(error) => Err(ValidationError::Status(error).into()),
443 }
444 }
445}
446
447impl<R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for SdJwtCredentialValidator<R, V> {
448 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
449 f.debug_struct("SdJwtCredentialValidator")
450 .field("resolver", &self.resolver)
451 .field("verifier", &self.verifier)
452 .field("status", &self.status.as_ref().map(|_| "SdJwtStatusCheck"))
453 .finish()
454 }
455}
456
457#[cfg(feature = "did-web")]
458mod status_list {
459 use chrono::{DateTime, Utc};
460 use url::Url;
461
462 use super::SdJwtStatusCheck;
463 use crate::did::{DidUrl, VerificationRelationship};
464 use crate::jose::{DefaultVerifier, JwsVerifier};
465 use crate::methods::web::HttpClient;
466 use crate::sd_jwt_vc::{Status, StatusListToken, StatusMechanism};
467 use crate::vc::Jwt;
468 use crate::vc::validation::{KeyResolver, StatusError};
469
470 pub struct StatusListCheck<C, R, V = DefaultVerifier> {
483 client: C,
484 resolver: R,
485 verifier: V,
486 }
487
488 impl<C: HttpClient, R: KeyResolver, V: JwsVerifier> StatusListCheck<C, R, V> {
489 pub fn new(client: C, resolver: R, verifier: V) -> Self {
492 Self {
493 client,
494 resolver,
495 verifier,
496 }
497 }
498
499 async fn fetch(&self, uri: &str) -> Result<StatusListToken, StatusError> {
501 let url = Url::parse(uri)
502 .map_err(|e| StatusError::Unavailable(format!("`uri` {uri:?}: {e}")))?;
503 let response = self
504 .client
505 .get(&url)
506 .await
507 .map_err(|e| StatusError::Unavailable(format!("fetching {uri}: {e}")))?;
508 if !(200..300).contains(&response.status) {
509 return Err(StatusError::Unavailable(format!(
510 "fetching {uri}: HTTP {}",
511 response.status
512 )));
513 }
514 let body = String::from_utf8(response.body)
515 .map_err(|_| StatusError::Unavailable(format!("{uri}: body is not UTF-8")))?;
516 let jwt = Jwt::new(body.trim())
517 .map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
518 let (header, token) = StatusListToken::decode(&jwt)
519 .map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?;
520 let kid = header
521 .kid
522 .as_deref()
523 .ok_or_else(|| StatusError::Unavailable(format!("{uri}: no `kid`")))?;
524 let kid = DidUrl::parse(kid).map_err(|e| {
525 StatusError::Unavailable(format!("{uri}: `kid` {kid:?} is not a DID URL: {e}"))
526 })?;
527 if kid.path().is_some() || kid.query().is_some() {
528 return Err(StatusError::Unavailable(format!(
529 "{uri}: `kid` `{kid}` carries a path or DID parameters"
530 )));
531 }
532 let key = self
533 .resolver
534 .resolve_key(&kid, VerificationRelationship::AssertionMethod)
535 .await
536 .map_err(|e| StatusError::Unavailable(format!("{uri}: resolving `kid`: {e}")))?;
537 jwt.decode()
538 .map_err(|e| StatusError::Unavailable(format!("{uri}: {e}")))?
539 .verify(&self.verifier, &key)
540 .map_err(|e| StatusError::Unavailable(format!("{uri}: signature: {e}")))?;
541 Ok(token)
542 }
543 }
544
545 #[async_trait::async_trait]
546 impl<C: HttpClient, R: KeyResolver, V: JwsVerifier> SdJwtStatusCheck for StatusListCheck<C, R, V> {
547 async fn check(&self, status: &Status, now: DateTime<Utc>) -> Result<(), StatusError> {
548 let Some(reference) = &status.status_list else {
549 let names: Vec<&str> = status
550 .mechanisms()
551 .filter_map(|m| match m {
552 StatusMechanism::Other { name, .. } => Some(name),
553 _ => None,
554 })
555 .collect();
556 return Err(StatusError::Unsupported(names.join(", ")));
557 };
558 let token = self.fetch(&reference.uri).await?;
559 let status_type = token
560 .status_of(reference, now)
561 .map_err(|e| StatusError::Unavailable(e.to_string()))?;
562 if status_type.is_valid() {
563 Ok(())
564 } else {
565 Err(StatusError::Invalid(status_type.to_string()))
566 }
567 }
568 }
569
570 impl<C: core::fmt::Debug, R: core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug
571 for StatusListCheck<C, R, V>
572 {
573 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
574 f.debug_struct("StatusListCheck")
575 .field("client", &self.client)
576 .field("resolver", &self.resolver)
577 .field("verifier", &self.verifier)
578 .finish()
579 }
580 }
581}