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, 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}