Skip to main content

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}