identity_kit/sd_jwt_vc/credential.rs
1//! SD-JWT VC: an SD-JWT whose payload is a verifiable digital credential of a `vct` type.
2
3use core::fmt;
4use core::str::FromStr;
5
6use chrono::{DateTime, Utc};
7use indexmap::IndexMap;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::{Map, Value};
10
11use super::{
12 ClaimPath, Disclosure, HashAlgorithm, KeyBinding, PathComponent, Processed, SdJwt,
13 SdJwtBuilder, SdJwtError, Status,
14};
15use crate::did::DidUrl;
16use crate::jose::{Jwk, JwsHeader, Signer};
17use crate::resolver::{KeyReference, KeyReferenceError};
18use crate::vc::JwtClaims;
19
20/// The `typ` of the issuer-signed JWT of an SD-JWT VC, `dc+sd-jwt`.
21pub const TYP: &str = "dc+sd-jwt";
22
23/// The `typ` earlier drafts of SD-JWT VC used, `vc+sd-jwt`; accepted when parsing.
24pub const LEGACY_TYP: &str = "vc+sd-jwt";
25
26/// The media type of an SD-JWT VC, `application/dc+sd-jwt`.
27pub const MEDIA_TYPE: &str = "application/dc+sd-jwt";
28
29/// The registered claims that, with their sub-claims, must not be selectively disclosed
30/// (SD-JWT VC §2.2.2.3).
31pub const NON_DISCLOSABLE_CLAIMS: [&str; 8] = [
32 "iss",
33 "nbf",
34 "exp",
35 "cnf",
36 "vct",
37 "vct#integrity",
38 "aka_vcts",
39 "status",
40];
41
42/// The `cnf` claim (RFC 7800): the holder's key, by value as `jwk` or by reference as `kid`.
43#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
44#[non_exhaustive]
45pub struct Confirmation {
46 /// The holder's public key.
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub jwk: Option<Jwk>,
49 /// A reference to the holder's key.
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub kid: Option<String>,
52 /// Every other confirmation method.
53 #[serde(flatten)]
54 pub additional: IndexMap<String, Value>,
55}
56
57impl Confirmation {
58 /// A confirmation by the public part of `jwk`.
59 pub fn jwk(jwk: &Jwk) -> Self {
60 Self {
61 jwk: Some(jwk.public()),
62 ..Self::default()
63 }
64 }
65
66 /// A confirmation by key reference: an absolute DID URL naming a verification method of
67 /// the holder, which a verifier resolves under `authentication`.
68 pub fn kid(kid: impl Into<String>) -> Self {
69 Self {
70 kid: Some(kid.into()),
71 ..Self::default()
72 }
73 }
74}
75
76impl KeyReference {
77 /// Where the holder's key of a `cnf` claim (RFC 7800) is: `jwk` if present, else a `kid`
78 /// that is an absolute DID URL, else `jku` with the `kid`.
79 ///
80 /// RFC 7800 §3.4 leaves the meaning of a key id to the application, and the confirmation
81 /// carries no DID or key set to resolve a `#fragment` or a bare id against (using `sub`
82 /// would conflate the subject with the holder), so a `cnf.kid` must be an absolute DID URL
83 /// without a path or DID parameters (ADR 0006).
84 ///
85 /// # Examples
86 ///
87 /// ```
88 /// use identity_kit::resolver::{KeyReference, KeyReferenceError};
89 /// use identity_kit::sd_jwt_vc::Confirmation;
90 ///
91 /// let cnf = Confirmation::kid("did:example:holder#key-1");
92 /// assert_eq!(KeyReference::from_confirmation(&cnf)?.to_string(), "did:example:holder#key-1");
93 ///
94 /// let relative = Confirmation::kid("#key-1");
95 /// assert!(matches!(
96 /// KeyReference::from_confirmation(&relative),
97 /// Err(KeyReferenceError::InvalidKid { .. })
98 /// ));
99 /// # Ok::<(), KeyReferenceError>(())
100 /// ```
101 ///
102 /// # Errors
103 ///
104 /// [`KeyReferenceError::Missing`] for an empty `cnf`,
105 /// [`KeyReferenceError::UnsupportedConfirmation`] for one that names its key only by another
106 /// method (`x5t#S256`, `jwe`), and otherwise the member that is wrong.
107 pub fn from_confirmation(cnf: &Confirmation) -> Result<Self, KeyReferenceError> {
108 if let Some(jwk) = &cnf.jwk {
109 if jwk.is_private() {
110 return Err(KeyReferenceError::InvalidJwk(
111 "`cnf.jwk` holds a private key".to_owned(),
112 ));
113 }
114 return Ok(Self::Jwk(Box::new(jwk.clone())));
115 }
116 let jku = cnf.additional.get("jku");
117 if let Some(kid) = &cnf.kid {
118 match DidUrl::parse(kid) {
119 Ok(url) if url.path().is_some() || url.query().is_some() => {
120 return Err(KeyReferenceError::KidWithParameters(url));
121 }
122 Ok(url) => return Ok(Self::DidUrl(url)),
123 Err(source) if jku.is_none() => {
124 return Err(KeyReferenceError::InvalidKid {
125 kid: kid.clone(),
126 source,
127 });
128 }
129 Err(_) => {}
130 }
131 }
132 if let Some(jku) = jku {
133 let url = jku
134 .as_str()
135 .and_then(|jku| url::Url::parse(jku).ok())
136 .ok_or_else(|| KeyReferenceError::InvalidUrl {
137 parameter: "jku",
138 value: jku.to_string(),
139 reason: "not an absolute URL".to_owned(),
140 })?;
141 return Ok(Self::Jku {
142 url,
143 kid: cnf.kid.clone(),
144 });
145 }
146 match cnf.additional.keys().next() {
147 Some(method) => Err(KeyReferenceError::UnsupportedConfirmation(method.clone())),
148 None => Err(KeyReferenceError::Missing),
149 }
150 }
151}
152
153/// The claims of an SD-JWT VC (SD-JWT VC §2.2.2): the credential type and the registered
154/// claims as fields, every claim about the subject in `claims`.
155///
156/// # Examples
157///
158/// ```
159/// use identity_kit::sd_jwt_vc::SdJwtVcClaims;
160/// use serde_json::json;
161///
162/// // SD-JWT VC §2.2.2.1, Figure 3.
163/// let claims: SdJwtVcClaims = serde_json::from_value(json!({
164/// "vct": "https://credentials.example.com/identity_credential",
165/// "given_name": "Russ",
166/// "family_name": "Lasky",
167/// "exp": 1777707000,
168/// "cnf": { "jwk": { "kty": "EC", "crv": "P-256",
169/// "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
170/// "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" } }
171/// }))?;
172/// assert_eq!(claims.vct, "https://credentials.example.com/identity_credential");
173/// assert_eq!(claims.exp, Some(1777707000));
174/// assert_eq!(claims.claims["given_name"], "Russ");
175/// assert_eq!(claims.holder_key().map(|k| k.crv()), Some(Some("P-256")));
176/// # Ok::<(), serde_json::Error>(())
177/// ```
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179#[non_exhaustive]
180pub struct SdJwtVcClaims {
181 /// `vct`: the credential type, a collision-resistant name.
182 pub vct: String,
183 /// `vct#integrity`: the integrity metadata of the type metadata document.
184 #[serde(
185 rename = "vct#integrity",
186 default,
187 skip_serializing_if = "Option::is_none"
188 )]
189 pub vct_integrity: Option<String>,
190 /// `aka_vcts`: further types the credential is of.
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub aka_vcts: Option<Vec<String>>,
193 /// `iss`: the issuer.
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub iss: Option<String>,
196 /// `sub`: the subject.
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub sub: Option<String>,
199 /// `jti`: the identifier of the credential.
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub jti: Option<String>,
202 /// `iat`: when the credential was issued. A `NumericDate` with a fraction is floored.
203 #[serde(
204 default,
205 skip_serializing_if = "Option::is_none",
206 deserialize_with = "crate::vc::numeric_date"
207 )]
208 pub iat: Option<i64>,
209 /// `nbf`: when the credential becomes valid. A `NumericDate` with a fraction is floored.
210 #[serde(
211 default,
212 skip_serializing_if = "Option::is_none",
213 deserialize_with = "crate::vc::numeric_date"
214 )]
215 pub nbf: Option<i64>,
216 /// `exp`: when the credential expires. A `NumericDate` with a fraction is floored.
217 #[serde(
218 default,
219 skip_serializing_if = "Option::is_none",
220 deserialize_with = "crate::vc::numeric_date"
221 )]
222 pub exp: Option<i64>,
223 /// `cnf`: the holder's key, for key binding.
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub cnf: Option<Confirmation>,
226 /// `status`: how to learn the status of the credential.
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub status: Option<Status>,
229 /// Every other claim, in insertion order.
230 #[serde(flatten)]
231 pub claims: IndexMap<String, Value>,
232}
233
234impl SdJwtVcClaims {
235 /// Claims of the type `vct`, nothing else set.
236 pub fn new(vct: impl Into<String>) -> Self {
237 Self {
238 vct: vct.into(),
239 vct_integrity: None,
240 aka_vcts: None,
241 iss: None,
242 sub: None,
243 jti: None,
244 iat: None,
245 nbf: None,
246 exp: None,
247 cnf: None,
248 status: None,
249 claims: IndexMap::new(),
250 }
251 }
252
253 /// These claims with `iss`.
254 #[must_use]
255 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
256 self.iss = Some(issuer.into());
257 self
258 }
259
260 /// These claims with `sub`.
261 #[must_use]
262 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
263 self.sub = Some(subject.into());
264 self
265 }
266
267 /// These claims with `jti`.
268 #[must_use]
269 pub fn with_jti(mut self, jti: impl Into<String>) -> Self {
270 self.jti = Some(jti.into());
271 self
272 }
273
274 /// These claims with `iat`.
275 #[must_use]
276 pub fn with_issued_at(mut self, time: DateTime<Utc>) -> Self {
277 self.iat = Some(JwtClaims::numeric_date(time));
278 self
279 }
280
281 /// These claims with `nbf`.
282 #[must_use]
283 pub fn with_not_before(mut self, time: DateTime<Utc>) -> Self {
284 self.nbf = Some(JwtClaims::numeric_date(time));
285 self
286 }
287
288 /// These claims with `exp`.
289 #[must_use]
290 pub fn with_expires_at(mut self, time: DateTime<Utc>) -> Self {
291 self.exp = Some(JwtClaims::numeric_date(time));
292 self
293 }
294
295 /// These claims with `cnf`.
296 #[must_use]
297 pub fn with_confirmation(mut self, cnf: Confirmation) -> Self {
298 self.cnf = Some(cnf);
299 self
300 }
301
302 /// These claims with `cnf` holding the public part of `jwk`.
303 #[must_use]
304 pub fn with_holder_key(self, jwk: &Jwk) -> Self {
305 self.with_confirmation(Confirmation::jwk(jwk))
306 }
307
308 /// These claims with `status`.
309 #[must_use]
310 pub fn with_status(mut self, status: Status) -> Self {
311 self.status = Some(status);
312 self
313 }
314
315 /// These claims with `vct#integrity`.
316 #[must_use]
317 pub fn with_type_integrity(mut self, integrity: impl Into<String>) -> Self {
318 self.vct_integrity = Some(integrity.into());
319 self
320 }
321
322 /// These claims with `vct` added to `aka_vcts`.
323 #[must_use]
324 pub fn with_aka_vct(mut self, vct: impl Into<String>) -> Self {
325 self.aka_vcts.get_or_insert_with(Vec::new).push(vct.into());
326 self
327 }
328
329 /// These claims with the claim `name` set to `value`.
330 #[must_use]
331 pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
332 self.claims.insert(name.into(), value.into());
333 self
334 }
335
336 /// The holder's public key, when `cnf` carries one by value.
337 pub fn holder_key(&self) -> Option<&Jwk> {
338 self.cnf.as_ref()?.jwk.as_ref()
339 }
340
341 /// Checks the rules on the registered claims: `aka_vcts` is non-empty and does not repeat
342 /// `vct`.
343 ///
344 /// # Errors
345 ///
346 /// [`SdJwtVcError::InvalidClaim`].
347 pub fn check(&self) -> Result<(), SdJwtVcError> {
348 if let Some(aka) = &self.aka_vcts {
349 if aka.is_empty() {
350 return Err(SdJwtVcError::InvalidClaim {
351 claim: "aka_vcts",
352 reason: "the array is empty".into(),
353 });
354 }
355 if aka.contains(&self.vct) {
356 return Err(SdJwtVcError::InvalidClaim {
357 claim: "aka_vcts",
358 reason: "the array repeats `vct`".into(),
359 });
360 }
361 }
362 Ok(())
363 }
364
365 /// The claims as a JSON object.
366 pub fn to_map(&self) -> Map<String, Value> {
367 match serde_json::to_value(self).expect("claims serialize") {
368 Value::Object(map) => map,
369 _ => unreachable!("a struct serializes to an object"),
370 }
371 }
372}
373
374/// Why an SD-JWT is not an SD-JWT VC.
375#[derive(Debug, thiserror::Error)]
376#[non_exhaustive]
377pub enum SdJwtVcError {
378 /// The SD-JWT itself is invalid.
379 #[error(transparent)]
380 SdJwt(#[from] SdJwtError),
381 /// The issuer-signed JWT has no `typ`.
382 #[error("the issuer-signed JWT has no `typ`; `{TYP}` is required")]
383 MissingTyp,
384 /// The `typ` is neither `dc+sd-jwt` nor `vc+sd-jwt`.
385 #[error("header `typ` is {0:?}, not `{TYP}`")]
386 UnexpectedTyp(String),
387 /// The `typ` is `vc+sd-jwt` and the payload has `@context` and no `vct`: a VCDM 2.0
388 /// credential secured with SD-JWT (VC-JOSE-COSE), which
389 /// [`SdJwtCredentialV2`](super::SdJwtCredentialV2) parses, not an SD-JWT VC.
390 #[error(
391 "header `typ` is `{LEGACY_TYP}` and the payload has `@context` and no `vct`: a VCDM 2.0 \
392 credential secured with SD-JWT, not an SD-JWT VC"
393 )]
394 VcdmPayload,
395 /// A claim that must not be selectively disclosed came from a disclosure.
396 #[error("`{0}` must not be selectively disclosed")]
397 DisclosedRegisteredClaim(String),
398 /// The processed payload is not an SD-JWT VC claims set: no `vct`, say.
399 #[error("the payload is not an SD-JWT VC: {0}")]
400 Claims(String),
401 /// A registered claim breaks a rule of SD-JWT VC.
402 #[error("claim `{claim}`: {reason}")]
403 InvalidClaim {
404 /// The claim.
405 claim: &'static str,
406 /// What is wrong with it.
407 reason: String,
408 },
409}
410
411/// Issues an SD-JWT VC: the registered claims first-class, the claims about the subject
412/// selectively disclosable at the paths given to [`conceal`](Self::conceal), signed with
413/// `typ: dc+sd-jwt`.
414///
415/// # Examples
416///
417/// ```
418/// use identity_kit::sd_jwt_vc::SdJwtVcBuilder;
419/// use serde_json::json;
420///
421/// let builder = SdJwtVcBuilder::new("https://credentials.example.com/identity_credential")
422/// .with_issuer("https://example.com/issuer")
423/// .with_claim("given_name", "John")
424/// .with_claim("address", json!({"street_address": "123 Main St", "country": "US"}))
425/// .conceal(["given_name"])
426/// .conceal(["address"]);
427/// assert_eq!(builder.claims().vct, "https://credentials.example.com/identity_credential");
428/// // `iss` is a registered claim that cannot be concealed.
429/// assert!(builder.clone_claims().conceal(["iss"]).check_concealed().is_err());
430/// ```
431pub struct SdJwtVcBuilder {
432 claims: SdJwtVcClaims,
433 kid: Option<String>,
434 concealed: Vec<ClaimPath>,
435 decoys: usize,
436 alg: HashAlgorithm,
437 salts: Option<Box<dyn FnMut() -> String + Send>>,
438}
439
440impl SdJwtVcBuilder {
441 /// A builder for a credential of type `vct`.
442 pub fn new(vct: impl Into<String>) -> Self {
443 Self::from_claims(SdJwtVcClaims::new(vct))
444 }
445
446 /// A builder over `claims`.
447 pub fn from_claims(claims: SdJwtVcClaims) -> Self {
448 Self {
449 claims,
450 kid: None,
451 concealed: Vec::new(),
452 decoys: 0,
453 alg: HashAlgorithm::default(),
454 salts: None,
455 }
456 }
457
458 /// The claims so far.
459 pub fn claims(&self) -> &SdJwtVcClaims {
460 &self.claims
461 }
462
463 /// A builder over a copy of these claims, nothing concealed.
464 pub fn clone_claims(&self) -> Self {
465 Self::from_claims(self.claims.clone())
466 }
467
468 /// This builder with `iss`.
469 #[must_use]
470 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
471 self.claims = self.claims.with_issuer(issuer);
472 self
473 }
474
475 /// This builder with `sub`.
476 #[must_use]
477 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
478 self.claims = self.claims.with_subject(subject);
479 self
480 }
481
482 /// This builder with `jti`.
483 #[must_use]
484 pub fn with_jti(mut self, jti: impl Into<String>) -> Self {
485 self.claims = self.claims.with_jti(jti);
486 self
487 }
488
489 /// This builder with `iat`.
490 #[must_use]
491 pub fn with_issued_at(mut self, time: DateTime<Utc>) -> Self {
492 self.claims = self.claims.with_issued_at(time);
493 self
494 }
495
496 /// This builder with `nbf`.
497 #[must_use]
498 pub fn with_not_before(mut self, time: DateTime<Utc>) -> Self {
499 self.claims = self.claims.with_not_before(time);
500 self
501 }
502
503 /// This builder with `exp`.
504 #[must_use]
505 pub fn with_expires_at(mut self, time: DateTime<Utc>) -> Self {
506 self.claims = self.claims.with_expires_at(time);
507 self
508 }
509
510 /// This builder with `cnf`.
511 #[must_use]
512 pub fn with_confirmation(mut self, cnf: Confirmation) -> Self {
513 self.claims = self.claims.with_confirmation(cnf);
514 self
515 }
516
517 /// This builder with `cnf` holding the public part of `jwk`.
518 #[must_use]
519 pub fn with_holder_key(mut self, jwk: &Jwk) -> Self {
520 self.claims = self.claims.with_holder_key(jwk);
521 self
522 }
523
524 /// This builder with `status`.
525 #[must_use]
526 pub fn with_status(mut self, status: Status) -> Self {
527 self.claims = self.claims.with_status(status);
528 self
529 }
530
531 /// This builder with `vct#integrity`.
532 #[must_use]
533 pub fn with_type_integrity(mut self, integrity: impl Into<String>) -> Self {
534 self.claims = self.claims.with_type_integrity(integrity);
535 self
536 }
537
538 /// This builder with `vct` added to `aka_vcts`.
539 #[must_use]
540 pub fn with_aka_vct(mut self, vct: impl Into<String>) -> Self {
541 self.claims = self.claims.with_aka_vct(vct);
542 self
543 }
544
545 /// This builder with the claim `name` set to `value`.
546 #[must_use]
547 pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
548 self.claims = self.claims.with_claim(name, value);
549 self
550 }
551
552 /// This builder signing with `kid` in the header, instead of the signer's.
553 #[must_use]
554 pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
555 self.kid = Some(kid.into());
556 self
557 }
558
559 /// This builder concealing the claim at `path`; see [`SdJwtBuilder::conceal`].
560 #[must_use]
561 pub fn conceal(mut self, path: impl Into<ClaimPath>) -> Self {
562 self.concealed.push(path.into());
563 self
564 }
565
566 /// This builder adding `count` decoy digests to every `_sd` array.
567 #[must_use]
568 pub fn with_decoys(mut self, count: usize) -> Self {
569 self.decoys = count;
570 self
571 }
572
573 /// This builder hashing with `alg`.
574 #[must_use]
575 pub fn with_hash_algorithm(mut self, alg: HashAlgorithm) -> Self {
576 self.alg = alg;
577 self
578 }
579
580 /// This builder taking its salts from `salts`; see [`SdJwtBuilder::with_salt_generator`].
581 #[must_use]
582 pub fn with_salt_generator(mut self, salts: impl FnMut() -> String + Send + 'static) -> Self {
583 self.salts = Some(Box::new(salts));
584 self
585 }
586
587 /// Checks that no path to conceal starts at a claim of [`NON_DISCLOSABLE_CLAIMS`].
588 ///
589 /// # Errors
590 ///
591 /// [`SdJwtError::NotConcealable`].
592 pub fn check_concealed(&self) -> Result<(), SdJwtVcError> {
593 for path in &self.concealed {
594 if let Some(PathComponent::Key(name)) = path.components().first()
595 && NON_DISCLOSABLE_CLAIMS.contains(&name.as_str())
596 {
597 return Err(SdJwtError::NotConcealable {
598 path: path.clone(),
599 reason: format!("SD-JWT VC does not allow `{name}` to be disclosed"),
600 }
601 .into());
602 }
603 }
604 Ok(())
605 }
606
607 /// The credential, signed with `signer` under `alg`, `typ: dc+sd-jwt` and the `kid`
608 /// given or the signer's.
609 ///
610 /// # Errors
611 ///
612 /// [`SdJwtVcError::InvalidClaim`], [`SdJwtError::NotConcealable`], and the errors of
613 /// [`SdJwtBuilder::sign`].
614 pub async fn sign(self, signer: &dyn Signer) -> Result<SdJwtVc, SdJwtVcError> {
615 self.claims.check()?;
616 self.check_concealed()?;
617 let mut header = JwsHeader::new(signer.alg()).with_typ(TYP);
618 header.kid = self.kid.or_else(|| signer.kid().map(str::to_owned));
619 let mut builder = SdJwtBuilder::new(self.claims.to_map())
620 .with_decoys(self.decoys)
621 .with_hash_algorithm(self.alg);
622 for path in self.concealed {
623 builder = builder.conceal(path);
624 }
625 if let Some(salts) = self.salts {
626 builder = builder.with_salt_generator(salts);
627 }
628 let sd_jwt = builder.sign(&header, signer).await?;
629 SdJwtVc::from_sd_jwt(sd_jwt)
630 }
631}
632
633impl fmt::Debug for SdJwtVcBuilder {
634 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635 f.debug_struct("SdJwtVcBuilder")
636 .field("claims", &self.claims)
637 .field("kid", &self.kid)
638 .field("concealed", &self.concealed)
639 .field("decoys", &self.decoys)
640 .field("alg", &self.alg)
641 .finish_non_exhaustive()
642 }
643}
644
645/// An SD-JWT VC: an [`SdJwt`] with `typ: dc+sd-jwt` whose processed payload is an
646/// [`SdJwtVcClaims`] set, with the registered claims not selectively disclosed.
647///
648/// Parsing processes the disclosures (so the digests and the structure are checked) but
649/// verifies no signature: that is [`SdJwtCredentialValidator`](super::SdJwtCredentialValidator).
650///
651/// # Examples
652///
653/// The presentation without key binding of SD-JWT VC §2.3.2, Figure 10.
654///
655/// ```
656/// use identity_kit::sd_jwt_vc::SdJwtVc;
657///
658/// let presentation = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImRjK3NkLWp3dCJ9.eyJfc2QiOiBbIjA5dkt\
659/// ySk1PbHlUV00wc2pwdV9wZE9CVkJRMk0xeTNLaHBINTE1blhrcFkiLCAiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1\
660/// RxMF9kYXcxc1g3NnBvVWxnQ3diSSIsICJFa084ZGhXMGRIRUpidlVIbEVfVkNldUM5dVJFTE9pZUxaaGg3WGJVV\
661/// HRBIiwgIklsRHpJS2VpWmREd3BxcEs2WmZieXBoRnZ6NUZnbldhLXNONndxUVhDaXciLCAiSnpZakg0c3ZsaUgw\
662/// UjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkp\
663/// HbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiam\
664/// RyVEU4WWNiWTRFaWZ1Z2loaUFlX0JQZWt4SlFaSUNlaVVRd1k5UXF4SSIsICJqc3U5eVZ1bHdRUWxoRmxNXzNKb\
665/// HpNYVNGemdsaFFHMERwZmF5UXdMVUs0Il0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAi\
666/// aWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2Y3QiOiAiaHR0cHM6Ly9jcmVkZW50aWFscy5\
667/// leGFtcGxlLmNvbS9pZGVudGl0eV9jcmVkZW50aWFsIiwgIl9zZF9hbGciOiAic2hhLTI1NiJ9.1ar8IhAJaZ8Jn\
668/// bPGD2dx-6c8PzGQUUafDJdsjRD1FwRna2CICwhANOQfXi5RPkP_iH8hOHFh5-2WJg8VsMJUcA~WyJsa2x4RjVqT\
669/// VlsR1RQVW92TU5JdkNBIiwgImlzX292ZXJfNjUiLCB0cnVlXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgI\
670/// mFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24i\
671/// LCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~";
672/// let credential: SdJwtVc = presentation.parse()?;
673/// assert_eq!(credential.claims().vct, "https://credentials.example.com/identity_credential");
674/// assert_eq!(credential.claims().iss.as_deref(), Some("https://example.com/issuer"));
675/// assert_eq!(credential.claims().claims["is_over_65"], true);
676/// assert_eq!(credential.claims().claims["address"]["locality"], "Anytown");
677/// assert!(!credential.claims().claims.contains_key("given_name"));
678/// # Ok::<(), identity_kit::sd_jwt_vc::SdJwtVcError>(())
679/// ```
680#[derive(Clone, PartialEq, Eq)]
681pub struct SdJwtVc {
682 sd_jwt: SdJwt,
683 header: JwsHeader,
684 processed: Processed,
685 claims: SdJwtVcClaims,
686}
687
688impl SdJwtVc {
689 /// Parses the compact serialization and processes the disclosures.
690 ///
691 /// # Errors
692 ///
693 /// As [`from_sd_jwt`](Self::from_sd_jwt).
694 pub fn parse(s: &str) -> Result<Self, SdJwtVcError> {
695 Self::from_sd_jwt(SdJwt::parse(s)?)
696 }
697
698 /// Takes `sd_jwt` as an SD-JWT VC: checks `typ`, processes the disclosures, reads the
699 /// claims and checks that none of [`NON_DISCLOSABLE_CLAIMS`] came from a disclosure.
700 ///
701 /// `vc+sd-jwt` is also the `typ` of a VCDM 2.0 credential secured with SD-JWT
702 /// (VC-JOSE-COSE), so it is accepted only when the payload has `vct`; see the [module
703 /// documentation](super#two-sd-jwt-credential-types).
704 ///
705 /// # Errors
706 ///
707 /// [`SdJwtVcError::MissingTyp`], [`SdJwtVcError::UnexpectedTyp`],
708 /// [`SdJwtVcError::VcdmPayload`], [`SdJwtVcError::DisclosedRegisteredClaim`],
709 /// [`SdJwtVcError::Claims`], [`SdJwtVcError::InvalidClaim`], or the [`SdJwtError`] of
710 /// processing.
711 pub fn from_sd_jwt(sd_jwt: SdJwt) -> Result<Self, SdJwtVcError> {
712 let header = sd_jwt
713 .jwt()
714 .decode()
715 .map_err(SdJwtError::from)?
716 .header()
717 .clone();
718 match header.typ.as_deref() {
719 None => return Err(SdJwtVcError::MissingTyp),
720 Some(TYP | LEGACY_TYP) => {}
721 Some(other) => return Err(SdJwtVcError::UnexpectedTyp(other.to_owned())),
722 }
723 let processed = sd_jwt.process()?;
724 if header.typ.as_deref() == Some(LEGACY_TYP)
725 && !processed.payload.contains_key("vct")
726 && processed.payload.contains_key("@context")
727 {
728 return Err(SdJwtVcError::VcdmPayload);
729 }
730 for claim in &processed.disclosed {
731 if let Some(PathComponent::Key(name)) = claim.path.components().first()
732 && NON_DISCLOSABLE_CLAIMS.contains(&name.as_str())
733 {
734 return Err(SdJwtVcError::DisclosedRegisteredClaim(name.clone()));
735 }
736 }
737 let claims: SdJwtVcClaims =
738 serde_json::from_value(Value::Object(processed.payload.clone()))
739 .map_err(|e| SdJwtVcError::Claims(e.to_string()))?;
740 claims.check()?;
741 Ok(Self {
742 sd_jwt,
743 header,
744 processed,
745 claims,
746 })
747 }
748
749 /// The SD-JWT.
750 pub fn sd_jwt(&self) -> &SdJwt {
751 &self.sd_jwt
752 }
753
754 /// The SD-JWT.
755 pub fn into_sd_jwt(self) -> SdJwt {
756 self.sd_jwt
757 }
758
759 /// The header of the issuer-signed JWT.
760 pub fn header(&self) -> &JwsHeader {
761 &self.header
762 }
763
764 /// Whether the `typ` is the pre-standard `vc+sd-jwt`.
765 pub fn has_legacy_typ(&self) -> bool {
766 self.header.typ.as_deref() == Some(LEGACY_TYP)
767 }
768
769 /// The claims, with the disclosures this SD-JWT carries applied.
770 pub fn claims(&self) -> &SdJwtVcClaims {
771 &self.claims
772 }
773
774 /// The processed payload as a JSON object.
775 pub fn payload(&self) -> &Map<String, Value> {
776 &self.processed.payload
777 }
778
779 /// The outcome of processing: which claims the disclosures revealed.
780 pub fn processed(&self) -> &Processed {
781 &self.processed
782 }
783
784 /// The holder's public key from `cnf`, if given by value. A key given by reference
785 /// ([`Confirmation::kid`]) is in [`confirmation`](Self::confirmation), and
786 /// [`KeyReference::from_confirmation`] reads either.
787 pub fn holder_key(&self) -> Option<&Jwk> {
788 self.claims.holder_key()
789 }
790
791 /// `cnf`: the holder's key, for key binding.
792 pub fn confirmation(&self) -> Option<&Confirmation> {
793 self.claims.cnf.as_ref()
794 }
795
796 /// This credential with only the disclosures at `paths` and beneath them, plus those they
797 /// are nested in, without key binding; see [`SdJwt::disclose`].
798 ///
799 /// # Errors
800 ///
801 /// As [`SdJwt::disclose`].
802 pub fn disclose<'a>(
803 &self,
804 paths: impl IntoIterator<Item = &'a ClaimPath>,
805 ) -> Result<Self, SdJwtVcError> {
806 Self::from_sd_jwt(self.sd_jwt.disclose(paths)?)
807 }
808
809 /// This credential with only the disclosures `keep` selects, without key binding; see
810 /// [`SdJwt::select`].
811 ///
812 /// # Errors
813 ///
814 /// As [`SdJwt::select`].
815 pub fn select(&self, keep: impl FnMut(&Disclosure) -> bool) -> Result<Self, SdJwtVcError> {
816 Self::from_sd_jwt(self.sd_jwt.select(keep)?)
817 }
818
819 /// This credential as an SD-JWT+KB; see [`SdJwt::sign_key_binding`].
820 ///
821 /// # Errors
822 ///
823 /// As [`SdJwt::sign_key_binding`].
824 pub async fn sign_key_binding(
825 self,
826 binding: &KeyBinding,
827 signer: &dyn Signer,
828 ) -> Result<Self, SdJwtVcError> {
829 let sd_jwt = self.sd_jwt.sign_key_binding(binding, signer).await?;
830 Ok(Self { sd_jwt, ..self })
831 }
832}
833
834impl fmt::Debug for SdJwtVc {
835 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
836 f.debug_struct("SdJwtVc")
837 .field("header", &self.header)
838 .field("claims", &self.claims)
839 .field("sd_jwt", &self.sd_jwt)
840 .finish_non_exhaustive()
841 }
842}
843
844impl fmt::Display for SdJwtVc {
845 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
846 fmt::Display::fmt(&self.sd_jwt, f)
847 }
848}
849
850impl FromStr for SdJwtVc {
851 type Err = SdJwtVcError;
852
853 fn from_str(s: &str) -> Result<Self, Self::Err> {
854 Self::parse(s)
855 }
856}
857
858impl Serialize for SdJwtVc {
859 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
860 serializer.collect_str(self)
861 }
862}
863
864impl<'de> Deserialize<'de> for SdJwtVc {
865 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
866 let s = String::deserialize(deserializer)?;
867 Self::parse(&s).map_err(serde::de::Error::custom)
868 }
869}