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