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