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}