1use 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
18pub const TYP: &str = "dc+sd-jwt";
20
21pub const LEGACY_TYP: &str = "vc+sd-jwt";
23
24pub const MEDIA_TYPE: &str = "application/dc+sd-jwt";
26
27pub 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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42#[non_exhaustive]
43pub struct Confirmation {
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub jwk: Option<Jwk>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub kid: Option<String>,
50 #[serde(flatten)]
52 pub additional: IndexMap<String, Value>,
53}
54
55impl Confirmation {
56 pub fn jwk(jwk: &Jwk) -> Self {
58 Self {
59 jwk: Some(jwk.public()),
60 ..Self::default()
61 }
62 }
63
64 pub fn kid(kid: impl Into<String>) -> Self {
66 Self {
67 kid: Some(kid.into()),
68 ..Self::default()
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[non_exhaustive]
100pub struct SdJwtVcClaims {
101 pub vct: String,
103 #[serde(
105 rename = "vct#integrity",
106 default,
107 skip_serializing_if = "Option::is_none"
108 )]
109 pub vct_integrity: Option<String>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub aka_vcts: Option<Vec<String>>,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub iss: Option<String>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 pub sub: Option<String>,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub jti: Option<String>,
122 #[serde(
124 default,
125 skip_serializing_if = "Option::is_none",
126 deserialize_with = "crate::vc::numeric_date"
127 )]
128 pub iat: Option<i64>,
129 #[serde(
131 default,
132 skip_serializing_if = "Option::is_none",
133 deserialize_with = "crate::vc::numeric_date"
134 )]
135 pub nbf: Option<i64>,
136 #[serde(
138 default,
139 skip_serializing_if = "Option::is_none",
140 deserialize_with = "crate::vc::numeric_date"
141 )]
142 pub exp: Option<i64>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub cnf: Option<Confirmation>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub status: Option<Status>,
149 #[serde(flatten)]
151 pub claims: IndexMap<String, Value>,
152}
153
154impl SdJwtVcClaims {
155 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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
217 pub fn with_confirmation(mut self, cnf: Confirmation) -> Self {
218 self.cnf = Some(cnf);
219 self
220 }
221
222 #[must_use]
224 pub fn with_holder_key(self, jwk: &Jwk) -> Self {
225 self.with_confirmation(Confirmation::jwk(jwk))
226 }
227
228 #[must_use]
230 pub fn with_status(mut self, status: Status) -> Self {
231 self.status = Some(status);
232 self
233 }
234
235 #[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 #[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 #[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 pub fn holder_key(&self) -> Option<&Jwk> {
258 self.cnf.as_ref()?.jwk.as_ref()
259 }
260
261 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 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#[derive(Debug, thiserror::Error)]
296#[non_exhaustive]
297pub enum SdJwtVcError {
298 #[error(transparent)]
300 SdJwt(#[from] SdJwtError),
301 #[error("the issuer-signed JWT has no `typ`; `{TYP}` is required")]
303 MissingTyp,
304 #[error("header `typ` is {0:?}, not `{TYP}`")]
306 UnexpectedTyp(String),
307 #[error("`{0}` must not be selectively disclosed")]
309 DisclosedRegisteredClaim(String),
310 #[error("the payload is not an SD-JWT VC: {0}")]
312 Claims(String),
313 #[error("claim `{claim}`: {reason}")]
315 InvalidClaim {
316 claim: &'static str,
318 reason: String,
320 },
321}
322
323pub 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 pub fn new(vct: impl Into<String>) -> Self {
355 Self::from_claims(SdJwtVcClaims::new(vct))
356 }
357
358 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 pub fn claims(&self) -> &SdJwtVcClaims {
372 &self.claims
373 }
374
375 pub fn clone_claims(&self) -> Self {
377 Self::from_claims(self.claims.clone())
378 }
379
380 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
473 pub fn conceal(mut self, path: impl Into<ClaimPath>) -> Self {
474 self.concealed.push(path.into());
475 self
476 }
477
478 #[must_use]
480 pub fn with_decoys(mut self, count: usize) -> Self {
481 self.decoys = count;
482 self
483 }
484
485 #[must_use]
487 pub fn with_hash_algorithm(mut self, alg: HashAlgorithm) -> Self {
488 self.alg = alg;
489 self
490 }
491
492 #[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 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 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#[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 pub fn parse(s: &str) -> Result<Self, SdJwtVcError> {
607 Self::from_sd_jwt(SdJwt::parse(s)?)
608 }
609
610 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 pub fn sd_jwt(&self) -> &SdJwt {
652 &self.sd_jwt
653 }
654
655 pub fn into_sd_jwt(self) -> SdJwt {
657 self.sd_jwt
658 }
659
660 pub fn header(&self) -> &JwsHeader {
662 &self.header
663 }
664
665 pub fn has_legacy_typ(&self) -> bool {
667 self.header.typ.as_deref() == Some(LEGACY_TYP)
668 }
669
670 pub fn claims(&self) -> &SdJwtVcClaims {
672 &self.claims
673 }
674
675 pub fn payload(&self) -> &Map<String, Value> {
677 &self.processed.payload
678 }
679
680 pub fn processed(&self) -> &Processed {
682 &self.processed
683 }
684
685 pub fn holder_key(&self) -> Option<&Jwk> {
687 self.claims.holder_key()
688 }
689
690 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 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 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}