Skip to main content

identity_kit/sd_jwt_vc/
credential_v2.rs

1//! VCDM 2.0 credentials secured with SD-JWT (VC-JOSE-COSE §3.2.1): the credential is the
2//! payload of the issuer-signed JWT, `typ` is `vc+sd-jwt`.
3
4use core::fmt;
5use core::str::FromStr;
6use std::collections::BTreeSet;
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use serde_json::{Map, Value};
11
12use super::{
13    ClaimPath, Confirmation, Disclosure, HashAlgorithm, KeyBinding, PathComponent, Processed,
14    SdJwt, SdJwtBuilder, SdJwtError, Status,
15};
16use crate::jose::{Jwk, JwsHeader, Signer};
17use crate::vc::{CredentialError, CredentialV2, JwtClaims};
18
19/// Why an SD-JWT is not a VCDM 2.0 credential secured with SD-JWT.
20#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum SdJwtCredentialV2Error {
23    /// The SD-JWT itself is invalid.
24    #[error(transparent)]
25    SdJwt(#[from] SdJwtError),
26    /// The `typ` is present and is not `vc+sd-jwt` (nor one the caller accepts).
27    #[error("header `typ` is {0:?}, not `vc+sd-jwt`")]
28    UnexpectedTyp(String),
29    /// A claim that must not be selectively disclosed came from a disclosure.
30    #[error("`{0}` must not be selectively disclosed")]
31    DisclosedRegisteredClaim(String),
32    /// The processed payload is not a JWT claims set.
33    #[error("the payload is not a JWT claims set: {0}")]
34    Payload(String),
35    /// `cnf` or `status` is not what the claim must hold.
36    #[error("claim `{claim}`: {reason}")]
37    InvalidClaim {
38        /// The claim.
39        claim: &'static str,
40        /// What is wrong with it.
41        reason: String,
42    },
43    /// The processed payload is not a VCDM 2.0 credential, or its registered claims disagree
44    /// with it.
45    #[error(transparent)]
46    Credential(#[from] CredentialError),
47}
48
49/// A VCDM 2.0 credential secured with SD-JWT (VC-JOSE-COSE §3.2.1): an [`SdJwt`] whose
50/// processed payload is a [`CredentialV2`], with `typ: vc+sd-jwt`, and `iss`, `cnf` and
51/// `status` beside it.
52///
53/// This is the VCDM counterpart of [`SdJwtVc`](super::SdJwtVc), which models the IETF SD-JWT
54/// VC claims set with its `vct`. Both are SD-JWTs and share [`SdJwt`]; telling them apart is
55/// described in the [module documentation](super#two-sd-jwt-credential-types).
56///
57/// Parsing processes the disclosures (so the digests and the structure are checked), reads
58/// the payload as a credential through [`CredentialV2::from_jwt_claims`] (so `iss`, `jti` and
59/// `sub`, when present, agree with `issuer`, `id` and the subject, and a `vc` claim is
60/// refused) and checks that none of [`NON_DISCLOSABLE_CLAIMS`](Self::NON_DISCLOSABLE_CLAIMS)
61/// came from a disclosure. No signature is verified: that is
62/// [`SdJwtCredentialValidator::validate_v2`](super::SdJwtCredentialValidator::validate_v2).
63///
64/// # Examples
65///
66/// ```
67/// use identity_kit::sd_jwt_vc::{SdJwt, SdJwtCredentialV2};
68///
69/// // The W3C VC-JOSE-COSE test suite's "SD-JWT Basic Credential Verification" input, with
70/// // `firstName` and `lastName` selectively disclosable, both disclosed. (Its `iat` is an RFC
71/// // 3339 string where RFC 7519 wants a NumericDate, so it processes as an SD-JWT but does
72/// // not map to a credential; the crate's tests cover both.)
73/// let sd_jwt: SdJwt = "eyJhbGciOiJFUzM4NCIsImN0eSI6InZjIiwia2lkIjoiOTZxQm5EcHZTNng4WVNDSF94bW5\
74///     MbFhaTWtEaEhCcjhsajk5dTVhb2c3cyIsInR5cCI6InZjK3NkLWp3dCJ9.eyJAY29udGV4dCI6WyJodHRwczovL3\
75///     d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZ\
76///     XhhbXBsZXMvdjIiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiX3NkIjpbImtuYzN2Z2pmZlpUUDdwRHk2OVAxVndO\
77///     V1RGVFhxblg1b0R1Nl9wU2RTYzAiLCIxeElDZkE3Z3VLVXZwU3F6RF9TelhwTU55U1RyWVRDSWNwNEwxOU1Ydn\
78///     FzIl0sImJpcnRoRGF0ZSI6IjE5OTAtMDEtMDEiLCJpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFj\
79///     Mjc2ZTEyZWMyMSJ9LCJpYXQiOiIyMDI0LTAxLTAxVDAwOjAwOjAwWiIsImlzcyI6Imh0dHBzOi8vZXhhbXBsZS\
80///     5pc3N1ZXIvdmMtam9zZS1jb3NlIiwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmlzc3Vlci92Yy1qb3NlLWNv\
81///     c2UiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiSWRlbnRpdHlDcmVkZW50aWFsIl0sInZhbGlkRn\
82///     JvbSI6IjIwMjQtMDEtMDFUMDA6MDA6MDBaIn0.bCgz_EgNH5ELRr-xvudGvtIWMQxobkjKLnQer3iXV3iZk4_v\
83///     KpMG8hUzvrrWlcpoxJo-Dh_Usm8LYcFOdnCHXdVNlnfz8baLEvNRcUc-60NkEwD739qjbEEvrCnYY9jb~WyI0b\
84///     UZLME14VEsyRmluZFNxQ0ktSzhBIiwiZmlyc3ROYW1lIiwiSmFuZSJd~WyJvU3lzbndsZ1ZGREUzZzdsbTJKZF\
85///     ZRIiwibGFzdE5hbWUiLCJEb2UiXQ~"
86///     .parse()?;
87/// assert_eq!(
88///     sd_jwt.jwt().decode()?.header().typ.as_deref(),
89///     Some(SdJwtCredentialV2::TYP)
90/// );
91/// let processed = sd_jwt.process()?;
92/// assert_eq!(processed.payload["credentialSubject"]["firstName"], "Jane");
93/// assert_eq!(processed.payload["credentialSubject"]["lastName"], "Doe");
94/// assert_eq!(processed.disclosed.len(), 2);
95/// # Ok::<(), Box<dyn std::error::Error>>(())
96/// ```
97#[derive(Clone, PartialEq, Eq)]
98pub struct SdJwtCredentialV2 {
99    sd_jwt: SdJwt,
100    header: JwsHeader,
101    processed: Processed,
102    claims: JwtClaims,
103    credential: CredentialV2,
104    cnf: Option<Confirmation>,
105    status: Option<Status>,
106}
107
108impl SdJwtCredentialV2 {
109    /// The `typ` of the issuer-signed JWT, `vc+sd-jwt`.
110    pub const TYP: &'static str = "vc+sd-jwt";
111
112    /// The media type, `application/vc+sd-jwt`.
113    pub const MEDIA_TYPE: &'static str = "application/vc+sd-jwt";
114
115    /// The claims that, with their sub-claims, must not be selectively disclosed: what a
116    /// verifier needs to validate the credential (VC-JOSE-COSE §3.2.1 names `@context`,
117    /// `type`, `credentialStatus`, `credentialSchema` and `relatedResource`), the issuer,
118    /// the validity period and the registered claims that carry it, the holder's key, the
119    /// status, and what SD-JWT reserves.
120    ///
121    /// A holder withholds a disclosure at will, so a verifier that received the credential
122    /// without, say, `credentialStatus` could not tell a credential issued without one from
123    /// one whose revocation data was dropped. Keeping these claims out of the disclosures is
124    /// what makes their absence meaningful.
125    pub const NON_DISCLOSABLE_CLAIMS: [&'static str; 14] = [
126        "@context",
127        "type",
128        "issuer",
129        "validFrom",
130        "validUntil",
131        "credentialStatus",
132        "credentialSchema",
133        "relatedResource",
134        "_sd_alg",
135        "iss",
136        "nbf",
137        "exp",
138        "cnf",
139        "status",
140    ];
141
142    /// Parses the compact serialization and processes the disclosures.
143    ///
144    /// # Errors
145    ///
146    /// As [`from_sd_jwt`](Self::from_sd_jwt).
147    pub fn parse(s: &str) -> Result<Self, SdJwtCredentialV2Error> {
148        Self::from_sd_jwt(SdJwt::parse(s)?)
149    }
150
151    /// Takes `sd_jwt` as a VCDM 2.0 credential secured with SD-JWT: `typ`, when present, is
152    /// `vc+sd-jwt`; the disclosures are processed; the payload is read as a credential; none
153    /// of [`NON_DISCLOSABLE_CLAIMS`](Self::NON_DISCLOSABLE_CLAIMS) came from a disclosure.
154    ///
155    /// # Errors
156    ///
157    /// [`SdJwtCredentialV2Error::UnexpectedTyp`],
158    /// [`SdJwtCredentialV2Error::DisclosedRegisteredClaim`],
159    /// [`SdJwtCredentialV2Error::Payload`], [`SdJwtCredentialV2Error::InvalidClaim`],
160    /// [`SdJwtCredentialV2Error::Credential`], or the [`SdJwtError`] of processing.
161    pub fn from_sd_jwt(sd_jwt: SdJwt) -> Result<Self, SdJwtCredentialV2Error> {
162        Self::from_sd_jwt_with(sd_jwt, &BTreeSet::new())
163    }
164
165    /// [`from_sd_jwt`](Self::from_sd_jwt), also accepting a `typ` in `additional_typ`.
166    ///
167    /// # Errors
168    ///
169    /// As [`from_sd_jwt`](Self::from_sd_jwt).
170    pub fn from_sd_jwt_with(
171        sd_jwt: SdJwt,
172        additional_typ: &BTreeSet<String>,
173    ) -> Result<Self, SdJwtCredentialV2Error> {
174        let header = sd_jwt
175            .jwt()
176            .decode()
177            .map_err(SdJwtError::from)?
178            .header()
179            .clone();
180        if let Some(typ) = header.typ.as_deref()
181            && typ != Self::TYP
182            && !additional_typ.contains(typ)
183        {
184            return Err(SdJwtCredentialV2Error::UnexpectedTyp(typ.to_owned()));
185        }
186        let processed = sd_jwt.process()?;
187        for claim in &processed.disclosed {
188            if let Some(PathComponent::Key(name)) = claim.path.components().first()
189                && Self::NON_DISCLOSABLE_CLAIMS.contains(&name.as_str())
190            {
191                return Err(SdJwtCredentialV2Error::DisclosedRegisteredClaim(
192                    name.clone(),
193                ));
194            }
195        }
196        let claims: JwtClaims = serde_json::from_value(Value::Object(processed.payload.clone()))
197            .map_err(|e| SdJwtCredentialV2Error::Payload(e.to_string()))?;
198        let mut rest = claims.clone();
199        let cnf = take_claim(&mut rest, "cnf")?;
200        let status = take_claim(&mut rest, "status")?;
201        let credential = CredentialV2::from_jwt_claims(&rest)?;
202        Ok(Self {
203            sd_jwt,
204            header,
205            processed,
206            claims,
207            credential,
208            cnf,
209            status,
210        })
211    }
212
213    /// The SD-JWT.
214    pub fn sd_jwt(&self) -> &SdJwt {
215        &self.sd_jwt
216    }
217
218    /// The SD-JWT.
219    pub fn into_sd_jwt(self) -> SdJwt {
220        self.sd_jwt
221    }
222
223    /// The header of the issuer-signed JWT.
224    pub fn header(&self) -> &JwsHeader {
225        &self.header
226    }
227
228    /// The credential, with the disclosures this SD-JWT carries applied.
229    pub fn credential(&self) -> &CredentialV2 {
230        &self.credential
231    }
232
233    /// The processed payload as a claims set: the registered claims (`iss`, `iat`, `exp`, …)
234    /// as fields, the credential's properties, `cnf` and `status` under `additional`.
235    pub fn claims(&self) -> &JwtClaims {
236        &self.claims
237    }
238
239    /// `cnf`: the holder's key, for key binding.
240    pub fn confirmation(&self) -> Option<&Confirmation> {
241        self.cnf.as_ref()
242    }
243
244    /// `status`: how to learn the status of the credential (Token Status List), beside any
245    /// `credentialStatus` of the credential itself.
246    pub fn status(&self) -> Option<&Status> {
247        self.status.as_ref()
248    }
249
250    /// The processed payload as a JSON object.
251    pub fn payload(&self) -> &Map<String, Value> {
252        &self.processed.payload
253    }
254
255    /// The outcome of processing: which claims the disclosures revealed.
256    pub fn processed(&self) -> &Processed {
257        &self.processed
258    }
259
260    /// The holder's public key from `cnf`, if given by value. A key given by reference
261    /// ([`Confirmation::kid`]) is in [`confirmation`](Self::confirmation), and
262    /// [`KeyReference::from_confirmation`](crate::resolver::KeyReference::from_confirmation)
263    /// reads either.
264    pub fn holder_key(&self) -> Option<&Jwk> {
265        self.cnf.as_ref()?.jwk.as_ref()
266    }
267
268    /// This credential with only the disclosures at `paths` and beneath them, plus those they
269    /// are nested in, without key binding; see [`SdJwt::disclose`].
270    ///
271    /// # Errors
272    ///
273    /// As [`SdJwt::disclose`].
274    pub fn disclose<'a>(
275        &self,
276        paths: impl IntoIterator<Item = &'a ClaimPath>,
277    ) -> Result<Self, SdJwtCredentialV2Error> {
278        Self::from_sd_jwt_with(self.sd_jwt.disclose(paths)?, &self.accepted_typ())
279    }
280
281    /// This credential with only the disclosures `keep` selects, without key binding; see
282    /// [`SdJwt::select`].
283    ///
284    /// # Errors
285    ///
286    /// As [`SdJwt::select`].
287    pub fn select(
288        &self,
289        keep: impl FnMut(&Disclosure) -> bool,
290    ) -> Result<Self, SdJwtCredentialV2Error> {
291        Self::from_sd_jwt_with(self.sd_jwt.select(keep)?, &self.accepted_typ())
292    }
293
294    /// This credential as an SD-JWT+KB; see [`SdJwt::sign_key_binding`].
295    ///
296    /// # Errors
297    ///
298    /// As [`SdJwt::sign_key_binding`].
299    pub async fn sign_key_binding(
300        self,
301        binding: &KeyBinding,
302        signer: &dyn Signer,
303    ) -> Result<Self, SdJwtCredentialV2Error> {
304        let sd_jwt = self.sd_jwt.sign_key_binding(binding, signer).await?;
305        Ok(Self { sd_jwt, ..self })
306    }
307
308    /// The `typ` this credential was accepted with, so that a derived credential is too.
309    fn accepted_typ(&self) -> BTreeSet<String> {
310        self.header.typ.iter().cloned().collect()
311    }
312}
313
314/// Removes `claim` from `claims.additional` and reads it as a `T`.
315fn take_claim<T: serde::de::DeserializeOwned>(
316    claims: &mut JwtClaims,
317    claim: &'static str,
318) -> Result<Option<T>, SdJwtCredentialV2Error> {
319    claims
320        .additional
321        .shift_remove(claim)
322        .map(|value| {
323            serde_json::from_value(value).map_err(|e| SdJwtCredentialV2Error::InvalidClaim {
324                claim,
325                reason: e.to_string(),
326            })
327        })
328        .transpose()
329}
330
331impl fmt::Debug for SdJwtCredentialV2 {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        f.debug_struct("SdJwtCredentialV2")
334            .field("header", &self.header)
335            .field("credential", &self.credential)
336            .field("cnf", &self.cnf)
337            .field("status", &self.status)
338            .field("sd_jwt", &self.sd_jwt)
339            .finish_non_exhaustive()
340    }
341}
342
343impl fmt::Display for SdJwtCredentialV2 {
344    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345        fmt::Display::fmt(&self.sd_jwt, f)
346    }
347}
348
349impl FromStr for SdJwtCredentialV2 {
350    type Err = SdJwtCredentialV2Error;
351
352    fn from_str(s: &str) -> Result<Self, Self::Err> {
353        Self::parse(s)
354    }
355}
356
357impl Serialize for SdJwtCredentialV2 {
358    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
359        serializer.collect_str(self)
360    }
361}
362
363impl<'de> Deserialize<'de> for SdJwtCredentialV2 {
364    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
365        let s = String::deserialize(deserializer)?;
366        Self::parse(&s).map_err(serde::de::Error::custom)
367    }
368}
369
370/// Issues a VCDM 2.0 credential secured with SD-JWT: the credential is the payload, with
371/// `iss` set to its issuer and the registered time claims, `cnf` and `status` given here
372/// beside it; the properties at the paths given to [`conceal`](Self::conceal) are selectively
373/// disclosable; the issuer signs with `typ: vc+sd-jwt`.
374///
375/// # Examples
376///
377/// ```
378/// use identity_kit::did::Uri;
379/// use identity_kit::sd_jwt_vc::SdJwtCredentialV2Builder;
380/// use identity_kit::vc::{CredentialV2, Subject};
381/// use serde_json::json;
382///
383/// let credential = CredentialV2::new(
384///     Uri::parse("did:example:issuer")?,
385///     Subject::new(Uri::parse("did:example:holder")?)
386///         .with_property("given_name", "Erika")
387///         .with_property("degree", json!({ "type": "ExampleBachelorDegree" })),
388/// );
389/// let builder = SdJwtCredentialV2Builder::new(credential)
390///     .conceal(["credentialSubject", "given_name"])
391///     .with_decoys(2);
392/// assert!(builder.check_concealed().is_ok());
393/// // `issuer` and `@context` are needed to validate the credential and cannot be concealed.
394/// assert!(builder.clone_credential().conceal(["issuer"]).check_concealed().is_err());
395/// # Ok::<(), Box<dyn std::error::Error>>(())
396/// ```
397pub struct SdJwtCredentialV2Builder {
398    credential: CredentialV2,
399    iat: Option<i64>,
400    nbf: Option<i64>,
401    exp: Option<i64>,
402    cnf: Option<Confirmation>,
403    status: Option<Status>,
404    kid: Option<String>,
405    concealed: Vec<ClaimPath>,
406    decoys: usize,
407    alg: HashAlgorithm,
408    salts: Option<Box<dyn FnMut() -> String + Send>>,
409}
410
411impl SdJwtCredentialV2Builder {
412    /// A builder over `credential`.
413    pub fn new(credential: CredentialV2) -> Self {
414        Self {
415            credential,
416            iat: None,
417            nbf: None,
418            exp: None,
419            cnf: None,
420            status: None,
421            kid: None,
422            concealed: Vec::new(),
423            decoys: 0,
424            alg: HashAlgorithm::default(),
425            salts: None,
426        }
427    }
428
429    /// The credential.
430    pub fn credential(&self) -> &CredentialV2 {
431        &self.credential
432    }
433
434    /// A builder over a copy of this credential, nothing else set.
435    pub fn clone_credential(&self) -> Self {
436        Self::new(self.credential.clone())
437    }
438
439    /// This builder with `iat`, beside the credential's `validFrom`.
440    #[must_use]
441    pub fn with_issued_at(mut self, time: DateTime<Utc>) -> Self {
442        self.iat = Some(JwtClaims::numeric_date(time));
443        self
444    }
445
446    /// This builder with `nbf`.
447    #[must_use]
448    pub fn with_not_before(mut self, time: DateTime<Utc>) -> Self {
449        self.nbf = Some(JwtClaims::numeric_date(time));
450        self
451    }
452
453    /// This builder with `exp`, beside the credential's `validUntil`.
454    #[must_use]
455    pub fn with_expires_at(mut self, time: DateTime<Utc>) -> Self {
456        self.exp = Some(JwtClaims::numeric_date(time));
457        self
458    }
459
460    /// This builder with `cnf`.
461    #[must_use]
462    pub fn with_confirmation(mut self, cnf: Confirmation) -> Self {
463        self.cnf = Some(cnf);
464        self
465    }
466
467    /// This builder with `cnf` holding the public part of `jwk`.
468    #[must_use]
469    pub fn with_holder_key(self, jwk: &Jwk) -> Self {
470        self.with_confirmation(Confirmation::jwk(jwk))
471    }
472
473    /// This builder with `status`.
474    #[must_use]
475    pub fn with_status(mut self, status: Status) -> Self {
476        self.status = Some(status);
477        self
478    }
479
480    /// This builder signing with `kid` in the header, instead of the signer's.
481    #[must_use]
482    pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
483        self.kid = Some(kid.into());
484        self
485    }
486
487    /// This builder concealing the property at `path` (`credentialSubject.given_name`, an
488    /// array element, …); see [`SdJwtBuilder::conceal`].
489    #[must_use]
490    pub fn conceal(mut self, path: impl Into<ClaimPath>) -> Self {
491        self.concealed.push(path.into());
492        self
493    }
494
495    /// This builder adding `count` decoy digests to every `_sd` array.
496    #[must_use]
497    pub fn with_decoys(mut self, count: usize) -> Self {
498        self.decoys = count;
499        self
500    }
501
502    /// This builder hashing with `alg`.
503    #[must_use]
504    pub fn with_hash_algorithm(mut self, alg: HashAlgorithm) -> Self {
505        self.alg = alg;
506        self
507    }
508
509    /// This builder taking its salts from `salts`; see [`SdJwtBuilder::with_salt_generator`].
510    #[must_use]
511    pub fn with_salt_generator(mut self, salts: impl FnMut() -> String + Send + 'static) -> Self {
512        self.salts = Some(Box::new(salts));
513        self
514    }
515
516    /// Checks that no path to conceal starts at one of
517    /// [`SdJwtCredentialV2::NON_DISCLOSABLE_CLAIMS`].
518    ///
519    /// # Errors
520    ///
521    /// [`SdJwtError::NotConcealable`].
522    pub fn check_concealed(&self) -> Result<(), SdJwtCredentialV2Error> {
523        for path in &self.concealed {
524            if let Some(PathComponent::Key(name)) = path.components().first()
525                && SdJwtCredentialV2::NON_DISCLOSABLE_CLAIMS.contains(&name.as_str())
526            {
527                return Err(SdJwtError::NotConcealable {
528                    path: path.clone(),
529                    reason: format!("a credential secured with SD-JWT keeps `{name}` disclosed"),
530                }
531                .into());
532            }
533        }
534        Ok(())
535    }
536
537    /// The claims set to secure: the credential, `iss`, the time claims, `cnf` and `status`.
538    fn claims(&self) -> JwtClaims {
539        let mut claims = self.credential.to_jwt_claims();
540        claims.iss = Some(self.credential.issuer.id().to_string());
541        claims.iat = self.iat;
542        claims.nbf = self.nbf;
543        claims.exp = self.exp;
544        if let Some(cnf) = &self.cnf {
545            claims.additional.insert(
546                "cnf".to_owned(),
547                serde_json::to_value(cnf).expect("a confirmation serializes"),
548            );
549        }
550        if let Some(status) = &self.status {
551            claims.additional.insert(
552                "status".to_owned(),
553                serde_json::to_value(status).expect("a status serializes"),
554            );
555        }
556        claims
557    }
558
559    /// The credential, signed with `signer` under `alg`, `typ: vc+sd-jwt` and the `kid` given
560    /// or the signer's.
561    ///
562    /// # Errors
563    ///
564    /// [`SdJwtError::NotConcealable`], the errors of [`SdJwtBuilder::sign`], and those of
565    /// [`SdJwtCredentialV2::from_sd_jwt`] should the credential not read back.
566    pub async fn sign(
567        self,
568        signer: &dyn Signer,
569    ) -> Result<SdJwtCredentialV2, SdJwtCredentialV2Error> {
570        self.check_concealed()?;
571        let claims = match serde_json::to_value(self.claims()).expect("claims serialize") {
572            Value::Object(map) => map,
573            _ => unreachable!("a struct serializes to an object"),
574        };
575        let mut header = JwsHeader::new(signer.alg()).with_typ(SdJwtCredentialV2::TYP);
576        header.kid = self.kid.or_else(|| signer.kid().map(str::to_owned));
577        let mut builder = SdJwtBuilder::new(claims)
578            .with_decoys(self.decoys)
579            .with_hash_algorithm(self.alg);
580        for path in self.concealed {
581            builder = builder.conceal(path);
582        }
583        if let Some(salts) = self.salts {
584            builder = builder.with_salt_generator(salts);
585        }
586        let sd_jwt = builder.sign(&header, signer).await?;
587        SdJwtCredentialV2::from_sd_jwt(sd_jwt)
588    }
589}
590
591impl fmt::Debug for SdJwtCredentialV2Builder {
592    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593        f.debug_struct("SdJwtCredentialV2Builder")
594            .field("credential", &self.credential)
595            .field("iat", &self.iat)
596            .field("nbf", &self.nbf)
597            .field("exp", &self.exp)
598            .field("cnf", &self.cnf)
599            .field("status", &self.status)
600            .field("kid", &self.kid)
601            .field("concealed", &self.concealed)
602            .field("decoys", &self.decoys)
603            .field("alg", &self.alg)
604            .finish_non_exhaustive()
605    }
606}