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