1use std::collections::{HashMap, HashSet};
11
12use cbor2::{Simple, Value};
13use cose2::{Error, Header, Label};
14use sha2::{Digest, Sha256};
15
16pub const HEADER_SD_CLAIMS: i64 = 17;
18pub const HEADER_SD_ALG: i64 = 170;
20pub const HEADER_SD_AEAD_ENCRYPTED_CLAIMS: i64 = 171;
22pub const HEADER_SD_AEAD: i64 = 172;
24pub const HEADER_KCWT: i64 = 13;
26pub const HEADER_CWT_CLAIMS: i64 = 15;
28pub const HEADER_TYP: i64 = 16;
30
31pub const CONTENT_FORMAT_SD_CWT: i64 = 293;
33pub const CONTENT_FORMAT_KB_CWT: i64 = 294;
35
36pub const CWT_CLAIM_VCT: i64 = 11;
38pub const CWT_CLAIM_CNONCE: i64 = 39;
40
41pub const REDACTED_CLAIM_KEYS_SIMPLE: u8 = 59;
43pub const REDACTED_ELEMENT_TAG: u64 = 60;
45pub const TO_BE_REDACTED_TAG: u64 = 58;
47pub const TO_BE_DECOY_TAG: u64 = 62;
49
50pub const ALG_SHA_256: i64 = cose2::iana::AlgorithmSHA_256;
52
53pub fn redacted_claim_keys_label() -> Value {
55 Value::Simple(Simple::new(REDACTED_CLAIM_KEYS_SIMPLE).expect("59 is a valid CBOR simple value"))
56}
57
58pub fn is_redacted_claim_keys_label(value: &Value) -> bool {
60 matches!(
61 value,
62 Value::Simple(simple) if simple.value() == REDACTED_CLAIM_KEYS_SIMPLE
63 )
64}
65
66pub fn redacted_element(hash: impl Into<Vec<u8>>) -> Value {
68 Value::Tag(REDACTED_ELEMENT_TAG, Box::new(Value::Bytes(hash.into())))
69}
70
71pub trait SaltGenerator {
77 fn next_salt(&mut self) -> Result<[u8; 16], Error>;
79}
80
81impl<F> SaltGenerator for F
82where
83 F: FnMut() -> [u8; 16],
84{
85 fn next_salt(&mut self) -> Result<[u8; 16], Error> {
86 Ok(self())
87 }
88}
89
90pub trait RedactionHasher {
92 fn algorithm(&self) -> Label;
94
95 fn digest(&self, data: &[u8]) -> Vec<u8>;
97}
98
99#[derive(Clone, Copy, Debug, Default)]
101pub struct Sha256RedactionHasher;
102
103impl RedactionHasher for Sha256RedactionHasher {
104 fn algorithm(&self) -> Label {
105 Label::Int(ALG_SHA_256)
106 }
107
108 fn digest(&self, data: &[u8]) -> Vec<u8> {
109 Sha256::digest(data).to_vec()
110 }
111}
112
113pub fn default_hasher_for_sd_alg(alg: Option<Label>) -> Result<Sha256RedactionHasher, Error> {
115 match alg {
116 None | Some(Label::Int(ALG_SHA_256)) => Ok(Sha256RedactionHasher),
117 Some(other) => Err(Error::custom(format!(
118 "unsupported SD-CWT hash algorithm {other}"
119 ))),
120 }
121}
122
123pub fn sd_alg(protected: &Header) -> Result<Option<Label>, Error> {
125 protected.get_label(HEADER_SD_ALG)
126}
127
128pub fn set_sd_alg(protected: &mut Header, alg: impl Into<Label>) -> &mut Header {
130 protected.insert(HEADER_SD_ALG, Value::from(alg.into()));
131 protected
132}
133
134pub fn set_sd_cwt_typ(protected: &mut Header) -> &mut Header {
136 protected.insert(HEADER_TYP, CONTENT_FORMAT_SD_CWT);
137 protected
138}
139
140pub fn set_kb_cwt_typ(protected: &mut Header) -> &mut Header {
142 protected.insert(HEADER_TYP, CONTENT_FORMAT_KB_CWT);
143 protected
144}
145
146pub fn sd_aead(protected: &Header) -> Result<Option<u16>, Error> {
148 match protected.get_i64(HEADER_SD_AEAD)? {
149 None => Ok(None),
150 Some(value) => u16::try_from(value)
151 .map(Some)
152 .map_err(|_| Error::UnexpectedType("sd_aead must be a uint .size 2".into())),
153 }
154}
155
156pub fn set_sd_aead(protected: &mut Header, alg: u16) -> &mut Header {
158 protected.insert(HEADER_SD_AEAD, i64::from(alg));
159 protected
160}
161
162#[derive(Clone, Debug, PartialEq)]
164pub enum DisclosureKind {
165 Claim {
167 salt: Vec<u8>,
169 key: Label,
171 value: Value,
173 },
174 Element {
176 salt: Vec<u8>,
178 value: Value,
180 },
181 Decoy {
183 salt: Vec<u8>,
185 },
186}
187
188#[derive(Clone, Debug, PartialEq)]
190pub struct Disclosure {
191 kind: DisclosureKind,
192 encoded: Vec<u8>,
193}
194
195impl Disclosure {
196 pub fn claim(
198 salt: impl Into<Vec<u8>>,
199 key: impl Into<Label>,
200 value: impl Into<Value>,
201 ) -> Result<Self, Error> {
202 Self::from_kind(DisclosureKind::Claim {
203 salt: salt.into(),
204 key: key.into(),
205 value: value.into(),
206 })
207 }
208
209 pub fn element(salt: impl Into<Vec<u8>>, value: impl Into<Value>) -> Result<Self, Error> {
211 Self::from_kind(DisclosureKind::Element {
212 salt: salt.into(),
213 value: value.into(),
214 })
215 }
216
217 pub fn decoy(salt: impl Into<Vec<u8>>) -> Result<Self, Error> {
219 Self::from_kind(DisclosureKind::Decoy { salt: salt.into() })
220 }
221
222 pub fn from_encoded(encoded: impl Into<Vec<u8>>) -> Result<Self, Error> {
224 let encoded = encoded.into();
225 let value: Value = cbor2::from_slice(&encoded)?;
226 let kind = decode_disclosure_value(value)?;
227 validate_disclosure_kind(&kind)?;
228 Ok(Self { kind, encoded })
229 }
230
231 pub fn kind(&self) -> &DisclosureKind {
233 &self.kind
234 }
235
236 pub fn encoded(&self) -> &[u8] {
238 &self.encoded
239 }
240
241 pub fn redacted_hash(&self, hasher: &dyn RedactionHasher) -> Vec<u8> {
243 hasher.digest(&self.encoded)
244 }
245
246 pub fn to_canonical_vec(&self) -> Result<Vec<u8>, Error> {
248 Ok(cbor2::to_canonical_vec(&self.to_value())?)
249 }
250
251 pub fn to_value(&self) -> Value {
253 match &self.kind {
254 DisclosureKind::Claim { salt, key, value } => Value::Array(vec![
255 Value::Bytes(salt.clone()),
256 value.clone(),
257 Value::from(key.clone()),
258 ]),
259 DisclosureKind::Element { salt, value } => {
260 Value::Array(vec![Value::Bytes(salt.clone()), value.clone()])
261 }
262 DisclosureKind::Decoy { salt } => Value::Array(vec![Value::Bytes(salt.clone())]),
263 }
264 }
265
266 fn from_kind(kind: DisclosureKind) -> Result<Self, Error> {
267 validate_disclosure_kind(&kind)?;
268 let encoded = cbor2::to_canonical_vec(&kind_to_value(&kind))?;
269 Ok(Self { kind, encoded })
270 }
271}
272
273#[derive(Clone, Debug, Default, PartialEq)]
275pub struct DisclosureSet {
276 disclosures: Vec<Disclosure>,
277}
278
279impl DisclosureSet {
280 pub fn new() -> Self {
282 Self::default()
283 }
284
285 pub fn from_disclosures<I>(disclosures: I) -> Self
287 where
288 I: IntoIterator<Item = Disclosure>,
289 {
290 Self {
291 disclosures: disclosures.into_iter().collect(),
292 }
293 }
294
295 pub fn from_unprotected(header: &Header) -> Result<Self, Error> {
297 disclosures_from_unprotected(header).map(Self::from_disclosures)
298 }
299
300 pub fn as_slice(&self) -> &[Disclosure] {
302 &self.disclosures
303 }
304
305 pub fn push(&mut self, disclosure: Disclosure) {
307 self.disclosures.push(disclosure);
308 }
309
310 pub fn is_empty(&self) -> bool {
312 self.disclosures.is_empty()
313 }
314
315 pub fn len(&self) -> usize {
317 self.disclosures.len()
318 }
319
320 pub fn write_unprotected(&self, header: &mut Header) {
324 set_disclosures(header, &self.disclosures);
325 }
326}
327
328impl IntoIterator for DisclosureSet {
329 type Item = Disclosure;
330 type IntoIter = std::vec::IntoIter<Disclosure>;
331
332 fn into_iter(self) -> Self::IntoIter {
333 self.disclosures.into_iter()
334 }
335}
336
337impl<'a> IntoIterator for &'a DisclosureSet {
338 type Item = &'a Disclosure;
339 type IntoIter = std::slice::Iter<'a, Disclosure>;
340
341 fn into_iter(self) -> Self::IntoIter {
342 self.disclosures.iter()
343 }
344}
345
346pub fn disclosures_from_unprotected(header: &Header) -> Result<Vec<Disclosure>, Error> {
348 let Some(value) = header.get(HEADER_SD_CLAIMS) else {
349 return Ok(Vec::new());
350 };
351 let Value::Array(items) = value else {
352 return Err(Error::UnexpectedType("sd_claims must be an array".into()));
353 };
354
355 let mut disclosures = Vec::with_capacity(items.len());
356 for item in items {
357 let Value::Bytes(encoded) = item else {
358 return Err(Error::UnexpectedType(
359 "sd_claims entries must be byte strings".into(),
360 ));
361 };
362 disclosures.push(Disclosure::from_encoded(encoded.clone())?);
363 }
364 Ok(disclosures)
365}
366
367pub fn set_disclosures(header: &mut Header, disclosures: &[Disclosure]) {
369 if disclosures.is_empty() {
370 header.remove(HEADER_SD_CLAIMS);
371 return;
372 }
373
374 let values = disclosures
375 .iter()
376 .map(|disclosure| Value::Bytes(disclosure.encoded().to_vec()))
377 .collect::<Vec<_>>();
378 header.insert(HEADER_SD_CLAIMS, Value::Array(values));
379}
380
381#[derive(Clone, Debug, PartialEq)]
383pub enum AeadKeyContext {
384 Uint(u64),
386 Text(String),
388 Thumbprint(Vec<u8>),
390}
391
392#[derive(Clone, Debug, PartialEq)]
394pub struct AeadEncryptedDisclosure {
395 pub nonce: Vec<u8>,
397 pub ciphertext: Vec<u8>,
399 pub tag: Vec<u8>,
401 pub key_context: Option<AeadKeyContext>,
403}
404
405impl AeadEncryptedDisclosure {
406 pub fn to_value(&self) -> Value {
408 let mut values = vec![
409 Value::Bytes(self.nonce.clone()),
410 Value::Bytes(self.ciphertext.clone()),
411 Value::Bytes(self.tag.clone()),
412 ];
413 if let Some(context) = &self.key_context {
414 values.push(match context {
415 AeadKeyContext::Uint(value) => Value::from(*value),
416 AeadKeyContext::Text(value) => Value::Text(value.clone()),
417 AeadKeyContext::Thumbprint(value) => Value::Bytes(value.clone()),
418 });
419 }
420 Value::Array(values)
421 }
422
423 pub fn from_value(value: &Value) -> Result<Self, Error> {
425 let Value::Array(items) = value else {
426 return Err(Error::UnexpectedType(
427 "AEAD encrypted disclosure must be an array".into(),
428 ));
429 };
430 if !(3..=4).contains(&items.len()) {
431 return Err(Error::custom(
432 "AEAD encrypted disclosure must have 3 or 4 elements",
433 ));
434 }
435
436 let nonce = expect_bytes(&items[0], "AEAD nonce")?.to_vec();
437 let ciphertext = expect_bytes(&items[1], "AEAD ciphertext")?.to_vec();
438 let tag = expect_bytes(&items[2], "AEAD tag")?.to_vec();
439 let key_context = if items.len() == 4 {
440 Some(match &items[3] {
441 Value::Integer(value) => {
442 let value = u64::try_from(*value).map_err(|_| {
443 Error::UnexpectedType("AEAD key context uint out of range".into())
444 })?;
445 AeadKeyContext::Uint(value)
446 }
447 Value::Text(value) => AeadKeyContext::Text(value.clone()),
448 Value::Bytes(value) => AeadKeyContext::Thumbprint(value.clone()),
449 _ => {
450 return Err(Error::UnexpectedType(
451 "AEAD key context must be uint, text, or bytes".into(),
452 ));
453 }
454 })
455 } else {
456 None
457 };
458
459 Ok(Self {
460 nonce,
461 ciphertext,
462 tag,
463 key_context,
464 })
465 }
466}
467
468pub fn aead_encrypted_disclosures_from_unprotected(
470 header: &Header,
471) -> Result<Vec<AeadEncryptedDisclosure>, Error> {
472 let Some(value) = header.get(HEADER_SD_AEAD_ENCRYPTED_CLAIMS) else {
473 return Ok(Vec::new());
474 };
475 let Value::Array(items) = value else {
476 return Err(Error::UnexpectedType(
477 "sd_aead_encrypted_claims must be an array".into(),
478 ));
479 };
480 items
481 .iter()
482 .map(AeadEncryptedDisclosure::from_value)
483 .collect()
484}
485
486pub fn set_aead_encrypted_disclosures(
488 header: &mut Header,
489 disclosures: &[AeadEncryptedDisclosure],
490) {
491 if disclosures.is_empty() {
492 header.remove(HEADER_SD_AEAD_ENCRYPTED_CLAIMS);
493 return;
494 }
495
496 header.insert(
497 HEADER_SD_AEAD_ENCRYPTED_CLAIMS,
498 Value::Array(
499 disclosures
500 .iter()
501 .map(AeadEncryptedDisclosure::to_value)
502 .collect(),
503 ),
504 );
505}
506
507#[derive(Clone, Debug, PartialEq)]
509pub struct IssueResult {
510 pub value: Value,
512 pub disclosures: DisclosureSet,
514}
515
516pub fn issue_from_preissuance(
523 value: Value,
524 salts: &mut dyn SaltGenerator,
525 hasher: &dyn RedactionHasher,
526) -> Result<IssueResult, Error> {
527 let mut context = IssueContext {
528 salts,
529 hasher,
530 decoy_ids: HashSet::new(),
531 disclosures: Vec::new(),
532 };
533 let value = issue_value(value, &mut context)?;
534 Ok(IssueResult {
535 value,
536 disclosures: DisclosureSet::from_disclosures(context.disclosures),
537 })
538}
539
540#[derive(Clone, Copy, Debug, PartialEq, Eq)]
542pub enum RestoreMode {
543 Holder,
545 Verifier,
547}
548
549#[derive(Clone, Debug, PartialEq)]
551pub struct RestoreReport {
552 pub value: Value,
554 pub disclosed: usize,
556 pub decoys: usize,
558 pub removed_redactions: usize,
560}
561
562#[derive(Default)]
563struct RestoreStats {
564 disclosed: usize,
565 decoys: usize,
566 removed_redactions: usize,
567}
568
569pub fn restore_for_holder<I>(
571 value: Value,
572 disclosures: I,
573 hasher: &dyn RedactionHasher,
574) -> Result<RestoreReport, Error>
575where
576 I: IntoIterator<Item = Disclosure>,
577{
578 restore(value, disclosures, hasher, RestoreMode::Holder)
579}
580
581pub fn restore_for_verifier<I>(
583 value: Value,
584 disclosures: I,
585 hasher: &dyn RedactionHasher,
586) -> Result<RestoreReport, Error>
587where
588 I: IntoIterator<Item = Disclosure>,
589{
590 restore(value, disclosures, hasher, RestoreMode::Verifier)
591}
592
593pub fn restore<I>(
595 value: Value,
596 disclosures: I,
597 hasher: &dyn RedactionHasher,
598 mode: RestoreMode,
599) -> Result<RestoreReport, Error>
600where
601 I: IntoIterator<Item = Disclosure>,
602{
603 let mut pending = DisclosureMap::new(disclosures, hasher)?;
604 let mut stats = RestoreStats::default();
605 let value = restore_value(value, &mut pending, mode, &mut stats)?;
606 if !pending.is_empty() {
607 return Err(Error::verify(
608 "sd_claims contains a disclosure without a matching redacted claim",
609 ));
610 }
611 Ok(RestoreReport {
612 value,
613 disclosed: stats.disclosed,
614 decoys: stats.decoys,
615 removed_redactions: stats.removed_redactions,
616 })
617}
618
619pub fn restore_payload_from_message(
624 message: &cose2::Sign1Message,
625 mode: RestoreMode,
626) -> Result<RestoreReport, Error> {
627 let hasher = default_hasher_for_sd_alg(sd_alg(&message.protected)?)?;
628 let disclosures = disclosures_from_unprotected(&message.unprotected)?;
629 restore_payload_with_disclosures(message, disclosures, &hasher, mode)
630}
631
632pub fn restore_payload_with_disclosures<I>(
634 message: &cose2::Sign1Message,
635 disclosures: I,
636 hasher: &dyn RedactionHasher,
637 mode: RestoreMode,
638) -> Result<RestoreReport, Error>
639where
640 I: IntoIterator<Item = Disclosure>,
641{
642 let payload = message
643 .payload
644 .as_deref()
645 .ok_or_else(|| Error::custom("SD-CWT message must carry an embedded payload"))?;
646 let value: Value = cbor2::from_slice(payload)?;
647 restore(value, disclosures, hasher, mode)
648}
649
650struct DisclosureMap {
651 entries: HashMap<Vec<u8>, Disclosure>,
652}
653
654impl DisclosureMap {
655 fn new<I>(disclosures: I, hasher: &dyn RedactionHasher) -> Result<Self, Error>
656 where
657 I: IntoIterator<Item = Disclosure>,
658 {
659 let mut entries = HashMap::new();
660 for disclosure in disclosures {
661 let hash = disclosure.redacted_hash(hasher);
662 if entries.insert(hash, disclosure).is_some() {
663 return Err(Error::verify("duplicate SD-CWT disclosure digest"));
664 }
665 }
666 Ok(Self { entries })
667 }
668
669 fn remove(&mut self, hash: &[u8]) -> Option<Disclosure> {
670 self.entries.remove(hash)
671 }
672
673 fn is_empty(&self) -> bool {
674 self.entries.is_empty()
675 }
676}
677
678struct IssueContext<'a> {
679 salts: &'a mut dyn SaltGenerator,
680 hasher: &'a dyn RedactionHasher,
681 decoy_ids: HashSet<u64>,
682 disclosures: Vec<Disclosure>,
683}
684
685fn issue_value(value: Value, context: &mut IssueContext<'_>) -> Result<Value, Error> {
686 match value {
687 Value::Map(entries) => issue_map(entries, context),
688 Value::Array(items) => issue_array(items, context),
689 Value::Tag(tag, inner) if tag == TO_BE_REDACTED_TAG => {
690 let issued = issue_value(*inner, context)?;
691 let disclosure = Disclosure::element(context.salts.next_salt()?, issued)?;
692 let hash = disclosure.redacted_hash(context.hasher);
693 context.disclosures.push(disclosure);
694 Ok(redacted_element(hash))
695 }
696 Value::Tag(tag, inner) if tag == TO_BE_DECOY_TAG => {
697 record_decoy_id(&inner, context)?;
698 let disclosure = Disclosure::decoy(context.salts.next_salt()?)?;
699 let hash = disclosure.redacted_hash(context.hasher);
700 context.disclosures.push(disclosure);
701 Ok(redacted_element(hash))
702 }
703 Value::Tag(tag, _) if tag == REDACTED_ELEMENT_TAG => Err(Error::custom(
704 "pre-issuance value must not already contain redacted element tag 60",
705 )),
706 Value::Tag(tag, inner) => Ok(Value::Tag(tag, Box::new(issue_value(*inner, context)?))),
707 Value::Simple(simple) if simple.value() == REDACTED_CLAIM_KEYS_SIMPLE => Err(
708 Error::custom("pre-issuance value must not contain simple(59) redaction labels"),
709 ),
710 other => Ok(other),
711 }
712}
713
714fn issue_map(entries: Vec<(Value, Value)>, context: &mut IssueContext<'_>) -> Result<Value, Error> {
715 let mut output = Vec::with_capacity(entries.len());
716 let mut normalized_keys = Vec::<Value>::new();
717 let mut redacted_hashes = Vec::<Value>::new();
718
719 for (key, value) in entries {
720 match key {
721 Value::Tag(tag, inner) if tag == TO_BE_REDACTED_TAG => {
722 let claim_key = label_from_value(&inner)?;
723 let normalized_key = Value::from(claim_key.clone());
724 reject_duplicate_raw_key(&normalized_keys, &normalized_key)?;
725 normalized_keys.push(normalized_key);
726
727 let issued_value = issue_value(value, context)?;
728 let disclosure =
729 Disclosure::claim(context.salts.next_salt()?, claim_key, issued_value)?;
730 redacted_hashes.push(Value::Bytes(disclosure.redacted_hash(context.hasher)));
731 context.disclosures.push(disclosure);
732 }
733 Value::Tag(tag, inner) if tag == TO_BE_DECOY_TAG => {
734 record_decoy_id(&inner, context)?;
735 if !matches!(value, Value::Null) {
736 return Err(Error::custom(
737 "map decoy tag 62 entries must have a null value",
738 ));
739 }
740 let disclosure = Disclosure::decoy(context.salts.next_salt()?)?;
741 redacted_hashes.push(Value::Bytes(disclosure.redacted_hash(context.hasher)));
742 context.disclosures.push(disclosure);
743 }
744 key if is_redacted_claim_keys_label(&key) => {
745 return Err(Error::custom(
746 "pre-issuance map must not already contain simple(59)",
747 ));
748 }
749 key => {
750 ensure_preissuance_key(&key)?;
751 reject_duplicate_raw_key(&normalized_keys, &key)?;
752 normalized_keys.push(key.clone());
753 output.push((key, issue_value(value, context)?));
754 }
755 }
756 }
757
758 if !redacted_hashes.is_empty() {
759 output.push((redacted_claim_keys_label(), Value::Array(redacted_hashes)));
760 }
761
762 Ok(Value::Map(output))
763}
764
765fn issue_array(items: Vec<Value>, context: &mut IssueContext<'_>) -> Result<Value, Error> {
766 let mut output = Vec::with_capacity(items.len());
767 for item in items {
768 match item {
769 Value::Tag(tag, inner) if tag == TO_BE_REDACTED_TAG => {
770 let issued = issue_value(*inner, context)?;
771 let disclosure = Disclosure::element(context.salts.next_salt()?, issued)?;
772 let hash = disclosure.redacted_hash(context.hasher);
773 output.push(redacted_element(hash));
774 context.disclosures.push(disclosure);
775 }
776 Value::Tag(tag, inner) if tag == TO_BE_DECOY_TAG => {
777 record_decoy_id(&inner, context)?;
778 let disclosure = Disclosure::decoy(context.salts.next_salt()?)?;
779 let hash = disclosure.redacted_hash(context.hasher);
780 output.push(redacted_element(hash));
781 context.disclosures.push(disclosure);
782 }
783 item => output.push(issue_value(item, context)?),
784 }
785 }
786 Ok(Value::Array(output))
787}
788
789fn record_decoy_id(value: &Value, context: &mut IssueContext<'_>) -> Result<(), Error> {
790 let Value::Integer(id) = value else {
791 return Err(Error::UnexpectedType(
792 "tag 62 decoy payload must be a positive integer".into(),
793 ));
794 };
795 let id = u64::try_from(*id).map_err(|_| {
796 Error::UnexpectedType("tag 62 decoy payload must be a positive integer".into())
797 })?;
798 if id == 0 {
799 return Err(Error::UnexpectedType(
800 "tag 62 decoy payload must be greater than zero".into(),
801 ));
802 }
803 if !context.decoy_ids.insert(id) {
804 return Err(Error::verify("duplicate tag 62 decoy identifier"));
805 }
806 Ok(())
807}
808
809fn ensure_preissuance_key(key: &Value) -> Result<(), Error> {
810 match key {
811 Value::Integer(_) | Value::Text(_) => Ok(()),
812 _ => Err(Error::UnexpectedType(
813 "pre-issuance map keys must be int, text, tag 58, or tag 62".into(),
814 )),
815 }
816}
817
818fn reject_duplicate_raw_key(keys: &[Value], key: &Value) -> Result<(), Error> {
819 if keys.iter().any(|existing| existing == key) {
820 return Err(Error::verify(format!(
821 "duplicate pre-issuance map key {key}"
822 )));
823 }
824 Ok(())
825}
826
827fn restore_value(
828 value: Value,
829 pending: &mut DisclosureMap,
830 mode: RestoreMode,
831 stats: &mut RestoreStats,
832) -> Result<Value, Error> {
833 match value {
834 Value::Map(entries) => restore_map(entries, pending, mode, stats),
835 Value::Array(items) => restore_array(items, pending, mode, stats),
836 Value::Tag(tag, inner) if tag == REDACTED_ELEMENT_TAG => {
837 let hash = expect_bytes(&inner, "redacted array element hash")?;
838 match pending.remove(hash) {
839 Some(disclosure) => restore_element_disclosure(disclosure, pending, mode, stats),
840 None if mode == RestoreMode::Verifier => {
841 stats.removed_redactions += 1;
842 Ok(Value::Null)
843 }
844 None => Err(Error::verify(
845 "holder validation found redacted array element without disclosure",
846 )),
847 }
848 }
849 Value::Tag(tag, inner) => Ok(Value::Tag(
850 tag,
851 Box::new(restore_value(*inner, pending, mode, stats)?),
852 )),
853 Value::Simple(simple) if simple.value() == REDACTED_CLAIM_KEYS_SIMPLE => {
854 Err(Error::UnexpectedType(
855 "simple(59) is only valid as a redacted_claim_keys map key".into(),
856 ))
857 }
858 other => Ok(other),
859 }
860}
861
862fn restore_map(
863 entries: Vec<(Value, Value)>,
864 pending: &mut DisclosureMap,
865 mode: RestoreMode,
866 stats: &mut RestoreStats,
867) -> Result<Value, Error> {
868 let mut output = Vec::with_capacity(entries.len());
869 let mut redacted_hashes = Vec::new();
870 let mut saw_redacted_keys = false;
871
872 for (key, value) in entries {
873 if is_redacted_claim_keys_label(&key) {
874 if saw_redacted_keys {
875 return Err(Error::verify("duplicate redacted_claim_keys entry"));
876 }
877 saw_redacted_keys = true;
878 let Value::Array(hashes) = value else {
879 return Err(Error::UnexpectedType(
880 "redacted_claim_keys value must be an array".into(),
881 ));
882 };
883 for hash in hashes {
884 redacted_hashes.push(expect_owned_bytes(
885 hash,
886 "redacted_claim_keys entries must be byte strings",
887 )?);
888 }
889 continue;
890 }
891
892 reject_duplicate_key(&output, &key)?;
893 let value = restore_value(value, pending, mode, stats)?;
894 output.push((key, value));
895 }
896
897 for hash in redacted_hashes {
898 match pending.remove(&hash) {
899 Some(disclosure) => match disclosure.kind {
900 DisclosureKind::Claim { key, value, .. } => {
901 let key = Value::from(key);
902 reject_duplicate_key(&output, &key)?;
903 let value = restore_value(value, pending, mode, stats)?;
904 output.push((key, value));
905 stats.disclosed += 1;
906 }
907 DisclosureKind::Decoy { .. } => {
908 stats.decoys += 1;
909 }
910 DisclosureKind::Element { .. } => {
911 return Err(Error::verify(
912 "array-element disclosure matched a redacted map claim",
913 ));
914 }
915 },
916 None if mode == RestoreMode::Verifier => {
917 stats.removed_redactions += 1;
918 }
919 None => {
920 return Err(Error::verify(
921 "holder validation found redacted map claim without disclosure",
922 ));
923 }
924 }
925 }
926
927 Ok(Value::Map(output))
928}
929
930fn restore_array(
931 items: Vec<Value>,
932 pending: &mut DisclosureMap,
933 mode: RestoreMode,
934 stats: &mut RestoreStats,
935) -> Result<Value, Error> {
936 let mut output = Vec::with_capacity(items.len());
937 for item in items {
938 match item {
939 Value::Tag(tag, inner) if tag == REDACTED_ELEMENT_TAG => {
940 let hash = expect_bytes(&inner, "redacted array element hash")?.to_vec();
941 match pending.remove(&hash) {
942 Some(disclosure) => match disclosure.kind {
943 DisclosureKind::Element { value, .. } => {
944 output.push(restore_value(value, pending, mode, stats)?);
945 stats.disclosed += 1;
946 }
947 DisclosureKind::Decoy { .. } => {
948 stats.decoys += 1;
949 }
950 DisclosureKind::Claim { .. } => {
951 return Err(Error::verify(
952 "map-claim disclosure matched a redacted array element",
953 ));
954 }
955 },
956 None if mode == RestoreMode::Verifier => {
957 stats.removed_redactions += 1;
958 }
959 None => {
960 return Err(Error::verify(
961 "holder validation found redacted array element without disclosure",
962 ));
963 }
964 }
965 }
966 other => output.push(restore_value(other, pending, mode, stats)?),
967 }
968 }
969 Ok(Value::Array(output))
970}
971
972fn restore_element_disclosure(
973 disclosure: Disclosure,
974 pending: &mut DisclosureMap,
975 mode: RestoreMode,
976 stats: &mut RestoreStats,
977) -> Result<Value, Error> {
978 match disclosure.kind {
979 DisclosureKind::Element { value, .. } => {
980 stats.disclosed += 1;
981 restore_value(value, pending, mode, stats)
982 }
983 DisclosureKind::Decoy { .. } => {
984 stats.decoys += 1;
985 Ok(Value::Null)
986 }
987 DisclosureKind::Claim { .. } => Err(Error::verify(
988 "map-claim disclosure matched a redacted array element",
989 )),
990 }
991}
992
993fn kind_to_value(kind: &DisclosureKind) -> Value {
994 match kind {
995 DisclosureKind::Claim { salt, key, value } => Value::Array(vec![
996 Value::Bytes(salt.clone()),
997 value.clone(),
998 Value::from(key.clone()),
999 ]),
1000 DisclosureKind::Element { salt, value } => {
1001 Value::Array(vec![Value::Bytes(salt.clone()), value.clone()])
1002 }
1003 DisclosureKind::Decoy { salt } => Value::Array(vec![Value::Bytes(salt.clone())]),
1004 }
1005}
1006
1007fn decode_disclosure_value(value: Value) -> Result<DisclosureKind, Error> {
1008 let Value::Array(mut items) = value else {
1009 return Err(Error::UnexpectedType(
1010 "Salted Disclosed Claim must be an array".into(),
1011 ));
1012 };
1013
1014 match items.len() {
1015 1 => {
1016 let salt = expect_owned_bytes(items.remove(0), "disclosure salt must be bytes")?;
1017 Ok(DisclosureKind::Decoy { salt })
1018 }
1019 2 => {
1020 let value = items.pop().expect("len checked");
1021 let salt = expect_owned_bytes(items.remove(0), "disclosure salt must be bytes")?;
1022 Ok(DisclosureKind::Element { salt, value })
1023 }
1024 3 => {
1025 let key_value = items.pop().expect("len checked");
1026 let value = items.pop().expect("len checked");
1027 let salt = expect_owned_bytes(items.remove(0), "disclosure salt must be bytes")?;
1028 let key = label_from_value(&key_value)?;
1029 Ok(DisclosureKind::Claim { salt, key, value })
1030 }
1031 _ => Err(Error::UnexpectedType(
1032 "Salted Disclosed Claim must have 1, 2, or 3 elements".into(),
1033 )),
1034 }
1035}
1036
1037fn validate_disclosure_kind(kind: &DisclosureKind) -> Result<(), Error> {
1038 let salt = match kind {
1039 DisclosureKind::Claim { salt, .. }
1040 | DisclosureKind::Element { salt, .. }
1041 | DisclosureKind::Decoy { salt } => salt,
1042 };
1043 if salt.len() != 16 {
1044 return Err(Error::UnexpectedType(
1045 "Salted Disclosed Claim salt must be 16 bytes".into(),
1046 ));
1047 }
1048 Ok(())
1049}
1050
1051fn label_from_value(value: &Value) -> Result<Label, Error> {
1052 match value {
1053 Value::Integer(value) => i64::try_from(*value)
1054 .map(Label::Int)
1055 .map_err(|_| Error::UnexpectedType("claim key integer out of range".into())),
1056 Value::Text(value) => Ok(Label::Text(value.clone())),
1057 _ => Err(Error::UnexpectedType(
1058 "disclosed claim key must be an integer or text string".into(),
1059 )),
1060 }
1061}
1062
1063fn expect_bytes<'a>(value: &'a Value, name: &str) -> Result<&'a [u8], Error> {
1064 match value {
1065 Value::Bytes(bytes) => Ok(bytes),
1066 _ => Err(Error::UnexpectedType(format!("{name} must be bytes"))),
1067 }
1068}
1069
1070fn expect_owned_bytes(value: Value, name: &str) -> Result<Vec<u8>, Error> {
1071 match value {
1072 Value::Bytes(bytes) => Ok(bytes),
1073 _ => Err(Error::UnexpectedType(name.into())),
1074 }
1075}
1076
1077fn reject_duplicate_key(entries: &[(Value, Value)], key: &Value) -> Result<(), Error> {
1078 if entries.iter().any(|(existing, _)| existing == key) {
1079 return Err(Error::verify(format!("duplicate claim key {key}")));
1080 }
1081 Ok(())
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use super::*;
1087 use cose2::Sign1Message;
1088
1089 fn salt(byte: u8) -> Vec<u8> {
1090 vec![byte; 16]
1091 }
1092
1093 fn hash(disclosure: &Disclosure) -> Vec<u8> {
1094 disclosure.redacted_hash(&Sha256RedactionHasher)
1095 }
1096
1097 fn salt_source() -> impl FnMut() -> [u8; 16] {
1098 let mut next = 1u8;
1099 move || {
1100 let salt = [next; 16];
1101 next += 1;
1102 salt
1103 }
1104 }
1105
1106 #[test]
1107 fn simple_label_and_tagged_array_element_have_expected_wire_shape() {
1108 assert_eq!(
1109 cbor2::to_vec(&redacted_claim_keys_label()).unwrap(),
1110 vec![0xf8, 0x3b]
1111 );
1112
1113 let tagged = redacted_element(vec![0xab; 32]);
1114 let encoded = cbor2::to_vec(&tagged).unwrap();
1115 assert_eq!(encoded[0], 0xd8);
1116 assert_eq!(encoded[1], REDACTED_ELEMENT_TAG as u8);
1117 assert_eq!(encoded[2], 0x58);
1118 assert_eq!(encoded[3], 32);
1119 }
1120
1121 #[test]
1122 fn disclosure_round_trips_and_hashes_encoded_bytes() {
1123 let disclosure = Disclosure::claim(salt(1), 2, "Alice").unwrap();
1124 let encoded = disclosure.encoded().to_vec();
1125
1126 let decoded = Disclosure::from_encoded(encoded.clone()).unwrap();
1127 assert_eq!(decoded.kind(), disclosure.kind());
1128 assert_eq!(decoded.encoded(), encoded.as_slice());
1129 assert_eq!(
1130 decoded.redacted_hash(&Sha256RedactionHasher),
1131 hash(&disclosure)
1132 );
1133
1134 match decoded.kind() {
1135 DisclosureKind::Claim { salt, key, value } => {
1136 assert_eq!(salt, &vec![1; 16]);
1137 assert_eq!(key, &Label::Int(2));
1138 assert_eq!(value, &Value::Text("Alice".into()));
1139 }
1140 _ => panic!("expected claim disclosure"),
1141 }
1142 }
1143
1144 #[test]
1145 fn sd_claims_header_helpers_omit_empty_and_decode_entries() {
1146 let disclosure = Disclosure::element(salt(2), 42).unwrap();
1147 let mut header = Header::new();
1148
1149 set_disclosures(&mut header, &[]);
1150 assert!(!header.contains_key(HEADER_SD_CLAIMS));
1151
1152 set_disclosures(&mut header, std::slice::from_ref(&disclosure));
1153 let decoded = disclosures_from_unprotected(&header).unwrap();
1154 assert_eq!(decoded, vec![disclosure]);
1155
1156 DisclosureSet::new().write_unprotected(&mut header);
1157 assert!(!header.contains_key(HEADER_SD_CLAIMS));
1158 }
1159
1160 #[test]
1161 fn restores_redacted_map_claim_for_holder() {
1162 let disclosure = Disclosure::claim(salt(3), 2, "holder").unwrap();
1163 let payload = Value::Map(vec![
1164 (Value::from(1), Value::from("issuer")),
1165 (
1166 redacted_claim_keys_label(),
1167 Value::Array(vec![Value::Bytes(hash(&disclosure))]),
1168 ),
1169 ]);
1170
1171 let report = restore_for_holder(payload, vec![disclosure], &Sha256RedactionHasher).unwrap();
1172
1173 assert_eq!(report.disclosed, 1);
1174 assert_eq!(
1175 report.value,
1176 Value::Map(vec![
1177 (Value::from(1), Value::from("issuer")),
1178 (Value::from(2), Value::from("holder")),
1179 ])
1180 );
1181 }
1182
1183 #[test]
1184 fn verifier_removes_undisclosed_map_claims_and_array_elements() {
1185 let disclosed = Disclosure::element(salt(4), "visible").unwrap();
1186 let payload = Value::Map(vec![
1187 (
1188 redacted_claim_keys_label(),
1189 Value::Array(vec![Value::Bytes(vec![0xaa; 32])]),
1190 ),
1191 (
1192 Value::from("items"),
1193 Value::Array(vec![
1194 redacted_element(hash(&disclosed)),
1195 redacted_element(vec![0xbb; 32]),
1196 Value::from("plain"),
1197 ]),
1198 ),
1199 ]);
1200
1201 let report =
1202 restore_for_verifier(payload, vec![disclosed], &Sha256RedactionHasher).unwrap();
1203 assert_eq!(report.disclosed, 1);
1204 assert_eq!(report.removed_redactions, 2);
1205 assert_eq!(
1206 report.value,
1207 Value::Map(vec![(
1208 Value::from("items"),
1209 Value::Array(vec![Value::from("visible"), Value::from("plain")]),
1210 )])
1211 );
1212 }
1213
1214 #[test]
1215 fn holder_rejects_undisclosed_redaction() {
1216 let payload = Value::Map(vec![(
1217 redacted_claim_keys_label(),
1218 Value::Array(vec![Value::Bytes(vec![0xaa; 32])]),
1219 )]);
1220
1221 assert!(restore_for_holder(payload, Vec::new(), &Sha256RedactionHasher).is_err());
1222 }
1223
1224 #[test]
1225 fn nested_disclosures_are_matched_in_any_order() {
1226 let child = Disclosure::claim(salt(5), "country", "FR").unwrap();
1227 let parent_value = Value::Map(vec![(
1228 redacted_claim_keys_label(),
1229 Value::Array(vec![Value::Bytes(hash(&child))]),
1230 )]);
1231 let parent = Disclosure::claim(salt(6), "address", parent_value).unwrap();
1232 let payload = Value::Map(vec![(
1233 redacted_claim_keys_label(),
1234 Value::Array(vec![Value::Bytes(hash(&parent))]),
1235 )]);
1236
1237 let report =
1238 restore_for_holder(payload, vec![child, parent], &Sha256RedactionHasher).unwrap();
1239
1240 assert_eq!(report.disclosed, 2);
1241 assert_eq!(
1242 report.value,
1243 Value::Map(vec![(
1244 Value::from("address"),
1245 Value::Map(vec![(Value::from("country"), Value::from("FR"))]),
1246 )])
1247 );
1248 }
1249
1250 #[test]
1251 fn duplicate_disclosed_key_is_invalid() {
1252 let disclosure = Disclosure::claim(salt(7), 1, "redacted").unwrap();
1253 let payload = Value::Map(vec![
1254 (Value::from(1), Value::from("plain")),
1255 (
1256 redacted_claim_keys_label(),
1257 Value::Array(vec![Value::Bytes(hash(&disclosure))]),
1258 ),
1259 ]);
1260
1261 assert!(restore_for_verifier(payload, vec![disclosure], &Sha256RedactionHasher).is_err());
1262 }
1263
1264 #[test]
1265 fn decoys_are_removed_when_their_digest_is_present() {
1266 let decoy = Disclosure::decoy(salt(8)).unwrap();
1267 let payload = Value::Array(vec![redacted_element(hash(&decoy)), Value::from("kept")]);
1268
1269 let report = restore_for_verifier(payload, vec![decoy], &Sha256RedactionHasher).unwrap();
1270 assert_eq!(report.decoys, 1);
1271 assert_eq!(report.value, Value::Array(vec![Value::from("kept")]));
1272 }
1273
1274 #[test]
1275 fn aead_encrypted_disclosures_header_round_trips() {
1276 let encrypted = AeadEncryptedDisclosure {
1277 nonce: vec![1, 2, 3],
1278 ciphertext: vec![4, 5],
1279 tag: vec![6; 16],
1280 key_context: Some(AeadKeyContext::Text("key-a".into())),
1281 };
1282 let mut header = Header::new();
1283 set_aead_encrypted_disclosures(&mut header, std::slice::from_ref(&encrypted));
1284
1285 assert_eq!(
1286 aead_encrypted_disclosures_from_unprotected(&header).unwrap(),
1287 vec![encrypted]
1288 );
1289 }
1290
1291 #[test]
1292 fn issue_from_preissuance_redacts_map_keys_and_array_elements() {
1293 let preissued = Value::Map(vec![
1294 (Value::from(1), Value::from("issuer")),
1295 (
1296 Value::Tag(TO_BE_REDACTED_TAG, Box::new(Value::from("name"))),
1297 Value::from("Alice"),
1298 ),
1299 (
1300 Value::from("countries"),
1301 Value::Array(vec![
1302 Value::Tag(TO_BE_REDACTED_TAG, Box::new(Value::from("de"))),
1303 Value::from("fr"),
1304 ]),
1305 ),
1306 ]);
1307 let mut salts = salt_source();
1308
1309 let issued = issue_from_preissuance(preissued, &mut salts, &Sha256RedactionHasher).unwrap();
1310
1311 assert_eq!(issued.disclosures.len(), 2);
1312 let restored =
1313 restore_for_holder(issued.value, issued.disclosures, &Sha256RedactionHasher).unwrap();
1314 assert_eq!(
1315 restored.value,
1316 Value::Map(vec![
1317 (Value::from(1), Value::from("issuer")),
1318 (
1319 Value::from("countries"),
1320 Value::Array(vec![Value::from("de"), Value::from("fr")]),
1321 ),
1322 (Value::from("name"), Value::from("Alice")),
1323 ])
1324 );
1325 }
1326
1327 #[test]
1328 fn issue_from_preissuance_inserts_and_restores_decoys() {
1329 let preissued = Value::Map(vec![
1330 (
1331 Value::Tag(TO_BE_DECOY_TAG, Box::new(Value::from(1))),
1332 Value::Null,
1333 ),
1334 (
1335 Value::from("items"),
1336 Value::Array(vec![Value::Tag(TO_BE_DECOY_TAG, Box::new(Value::from(2)))]),
1337 ),
1338 ]);
1339 let mut salts = salt_source();
1340
1341 let issued = issue_from_preissuance(preissued, &mut salts, &Sha256RedactionHasher).unwrap();
1342
1343 assert_eq!(issued.disclosures.len(), 2);
1344 let restored =
1345 restore_for_verifier(issued.value, issued.disclosures, &Sha256RedactionHasher).unwrap();
1346 assert_eq!(restored.decoys, 2);
1347 assert_eq!(
1348 restored.value,
1349 Value::Map(vec![(Value::from("items"), Value::Array(vec![]))])
1350 );
1351 }
1352
1353 #[test]
1354 fn issue_from_preissuance_rejects_duplicate_normalized_keys_and_decoy_ids() {
1355 let duplicate_key = Value::Map(vec![
1356 (Value::from("name"), Value::from("plain")),
1357 (
1358 Value::Tag(TO_BE_REDACTED_TAG, Box::new(Value::from("name"))),
1359 Value::from("redacted"),
1360 ),
1361 ]);
1362 let duplicate_decoy = Value::Array(vec![
1363 Value::Tag(TO_BE_DECOY_TAG, Box::new(Value::from(1))),
1364 Value::Tag(TO_BE_DECOY_TAG, Box::new(Value::from(1))),
1365 ]);
1366 let mut salts = salt_source();
1367 assert!(issue_from_preissuance(duplicate_key, &mut salts, &Sha256RedactionHasher).is_err());
1368
1369 let mut salts = salt_source();
1370 assert!(
1371 issue_from_preissuance(duplicate_decoy, &mut salts, &Sha256RedactionHasher).is_err()
1372 );
1373 }
1374
1375 #[test]
1376 fn restore_payload_from_message_uses_headers() {
1377 let disclosure = Disclosure::claim(salt(9), "name", "Alice").unwrap();
1378 let payload = Value::Map(vec![(
1379 redacted_claim_keys_label(),
1380 Value::Array(vec![Value::Bytes(hash(&disclosure))]),
1381 )]);
1382 let mut message = Sign1Message::new(Some(cbor2::to_vec(&payload).unwrap()));
1383 set_disclosures(&mut message.unprotected, std::slice::from_ref(&disclosure));
1384
1385 let report = restore_payload_from_message(&message, RestoreMode::Holder).unwrap();
1386 assert_eq!(
1387 report.value,
1388 Value::Map(vec![(Value::from("name"), Value::from("Alice"),)])
1389 );
1390 }
1391}