Skip to main content

sd_cwt/
lib.rs

1//! Selective Disclosure CBOR Web Token helpers.
2//!
3//! This crate implements the SD-CWT disclosure and redaction mechanics from
4//! `draft-ietf-spice-sd-cwt-08` on top of [`cose2`]. It deliberately reuses
5//! `cose2` for COSE signing, verification and header storage; the APIs here
6//! cover SD-CWT-specific header parameters, salted disclosures, redacted claim
7//! markers, AEAD-encrypted disclosure wire structures, and restoration of a
8//! presented claims set.
9
10use std::collections::{HashMap, HashSet};
11
12use cbor2::{Simple, Value};
13use cose2::{Error, Header, Label};
14use sha2::{Digest, Sha256};
15
16/// COSE header parameter `sd_claims`.
17pub const HEADER_SD_CLAIMS: i64 = 17;
18/// COSE header parameter `sd_alg`.
19pub const HEADER_SD_ALG: i64 = 170;
20/// COSE header parameter `sd_aead_encrypted_claims`.
21pub const HEADER_SD_AEAD_ENCRYPTED_CLAIMS: i64 = 171;
22/// COSE header parameter `sd_aead`.
23pub const HEADER_SD_AEAD: i64 = 172;
24/// COSE header parameter `kcwt` used by Key Binding Tokens.
25pub const HEADER_KCWT: i64 = 13;
26/// COSE header parameter `CWT_Claims`.
27pub const HEADER_CWT_CLAIMS: i64 = 15;
28/// COSE header parameter `typ`.
29pub const HEADER_TYP: i64 = 16;
30
31/// CoAP content-format number for `application/sd-cwt`.
32pub const CONTENT_FORMAT_SD_CWT: i64 = 293;
33/// CoAP content-format number for `application/kb+cwt`.
34pub const CONTENT_FORMAT_KB_CWT: i64 = 294;
35
36/// SD-CWT `vct` CWT claim key.
37pub const CWT_CLAIM_VCT: i64 = 11;
38/// SD-CWT `cnonce` CWT claim key.
39pub const CWT_CLAIM_CNONCE: i64 = 39;
40
41/// The CBOR simple value used as the `redacted_claim_keys` map key.
42pub const REDACTED_CLAIM_KEYS_SIMPLE: u8 = 59;
43/// The CBOR tag used for redacted array elements.
44pub const REDACTED_ELEMENT_TAG: u64 = 60;
45/// The CBOR tag used in pre-issuance maps to mark a key or element for redaction.
46pub const TO_BE_REDACTED_TAG: u64 = 58;
47/// The CBOR tag used in pre-issuance maps to request decoys.
48pub const TO_BE_DECOY_TAG: u64 = 62;
49
50/// The COSE algorithm identifier for SHA-256.
51pub const ALG_SHA_256: i64 = cose2::iana::AlgorithmSHA_256;
52
53/// Returns the SD-CWT `redacted_claim_keys` map label, `simple(59)`.
54pub 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
58/// Returns true when `value` is the SD-CWT `redacted_claim_keys` map label.
59pub 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
66/// Wraps a redacted array-element hash as CBOR tag 60.
67pub fn redacted_element(hash: impl Into<Vec<u8>>) -> Value {
68    Value::Tag(REDACTED_ELEMENT_TAG, Box::new(Value::Bytes(hash.into())))
69}
70
71/// Source of 128-bit salts for issuance helpers.
72///
73/// The crate does not generate randomness. Issuers pass an implementation that
74/// returns one fresh, unpredictable 16-byte salt for each redacted claim,
75/// redacted array element, or decoy.
76pub trait SaltGenerator {
77    /// Returns the next 16-byte salt.
78    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
90/// Redaction hash algorithm used for Salted Disclosed Claims.
91pub trait RedactionHasher {
92    /// Returns the COSE algorithm identifier advertised in `sd_alg`.
93    fn algorithm(&self) -> Label;
94
95    /// Computes the digest of one bstr-encoded Salted Disclosed Claim.
96    fn digest(&self, data: &[u8]) -> Vec<u8>;
97}
98
99/// SHA-256 redaction hasher, the SD-CWT default when `sd_alg` is omitted.
100#[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
113/// Returns the built-in SHA-256 hasher for `sd_alg = -16` or an omitted `sd_alg`.
114pub 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
123/// Reads the `sd_alg` protected header parameter.
124pub fn sd_alg(protected: &Header) -> Result<Option<Label>, Error> {
125    protected.get_label(HEADER_SD_ALG)
126}
127
128/// Sets the `sd_alg` protected header parameter.
129pub 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
134/// Sets the SD-CWT content type (`typ`) header to `application/sd-cwt` (293).
135pub fn set_sd_cwt_typ(protected: &mut Header) -> &mut Header {
136    protected.insert(HEADER_TYP, CONTENT_FORMAT_SD_CWT);
137    protected
138}
139
140/// Sets the Key Binding Token content type (`typ`) header to `application/kb+cwt` (294).
141pub fn set_kb_cwt_typ(protected: &mut Header) -> &mut Header {
142    protected.insert(HEADER_TYP, CONTENT_FORMAT_KB_CWT);
143    protected
144}
145
146/// Reads the `sd_aead` protected header parameter.
147pub 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
156/// Sets the `sd_aead` protected header parameter.
157pub 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/// A decoded Salted Disclosed Claim entry.
163#[derive(Clone, Debug, PartialEq)]
164pub enum DisclosureKind {
165    /// A redacted map claim: `[salt, claim value, claim key]`.
166    Claim {
167        /// The 16-byte salt.
168        salt: Vec<u8>,
169        /// The disclosed claim key.
170        key: Label,
171        /// The disclosed claim value.
172        value: Value,
173    },
174    /// A redacted array element: `[salt, claim value]`.
175    Element {
176        /// The 16-byte salt.
177        salt: Vec<u8>,
178        /// The disclosed element value.
179        value: Value,
180    },
181    /// A decoy entry: `[salt]`.
182    Decoy {
183        /// The 16-byte salt.
184        salt: Vec<u8>,
185    },
186}
187
188/// One Salted Disclosed Claim plus the exact bstr-encoded bytes used for hashing.
189#[derive(Clone, Debug, PartialEq)]
190pub struct Disclosure {
191    kind: DisclosureKind,
192    encoded: Vec<u8>,
193}
194
195impl Disclosure {
196    /// Builds a redacted map-claim disclosure.
197    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    /// Builds a redacted array-element disclosure.
210    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    /// Builds a decoy disclosure.
218    pub fn decoy(salt: impl Into<Vec<u8>>) -> Result<Self, Error> {
219        Self::from_kind(DisclosureKind::Decoy { salt: salt.into() })
220    }
221
222    /// Decodes and validates one bstr-encoded Salted Disclosed Claim.
223    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    /// Returns the decoded disclosure kind.
232    pub fn kind(&self) -> &DisclosureKind {
233        &self.kind
234    }
235
236    /// Returns the exact bstr-encoded Salted Disclosed Claim bytes.
237    pub fn encoded(&self) -> &[u8] {
238        &self.encoded
239    }
240
241    /// Computes this disclosure's Redacted Claim Hash.
242    pub fn redacted_hash(&self, hasher: &dyn RedactionHasher) -> Vec<u8> {
243        hasher.digest(&self.encoded)
244    }
245
246    /// Encodes the decoded disclosure value canonically.
247    pub fn to_canonical_vec(&self) -> Result<Vec<u8>, Error> {
248        Ok(cbor2::to_canonical_vec(&self.to_value())?)
249    }
250
251    /// Converts this disclosure to its decoded CBOR value.
252    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/// A collection of Salted Disclosed Claims.
274#[derive(Clone, Debug, Default, PartialEq)]
275pub struct DisclosureSet {
276    disclosures: Vec<Disclosure>,
277}
278
279impl DisclosureSet {
280    /// Creates an empty disclosure set.
281    pub fn new() -> Self {
282        Self::default()
283    }
284
285    /// Creates a disclosure set from an iterator.
286    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    /// Decodes the `sd_claims` unprotected header parameter.
296    pub fn from_unprotected(header: &Header) -> Result<Self, Error> {
297        disclosures_from_unprotected(header).map(Self::from_disclosures)
298    }
299
300    /// Returns the disclosures.
301    pub fn as_slice(&self) -> &[Disclosure] {
302        &self.disclosures
303    }
304
305    /// Appends a disclosure.
306    pub fn push(&mut self, disclosure: Disclosure) {
307        self.disclosures.push(disclosure);
308    }
309
310    /// Returns true when there are no disclosures.
311    pub fn is_empty(&self) -> bool {
312        self.disclosures.is_empty()
313    }
314
315    /// Returns the number of disclosures.
316    pub fn len(&self) -> usize {
317        self.disclosures.len()
318    }
319
320    /// Writes this set to the `sd_claims` unprotected header parameter.
321    ///
322    /// Per the draft, an empty disclosure set omits `sd_claims`.
323    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
346/// Reads `sd_claims` from an unprotected header.
347pub 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
367/// Writes `sd_claims` to an unprotected header, omitting the parameter when empty.
368pub 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/// AEAD encrypted disclosure key context.
382#[derive(Clone, Debug, PartialEq)]
383pub enum AeadKeyContext {
384    /// Unsigned integer key context.
385    Uint(u64),
386    /// Text key context.
387    Text(String),
388    /// COSE key thumbprint key context.
389    Thumbprint(Vec<u8>),
390}
391
392/// One `sd_aead_encrypted_claims` entry.
393#[derive(Clone, Debug, PartialEq)]
394pub struct AeadEncryptedDisclosure {
395    /// Nonce of N_MIN octets for the selected AEAD.
396    pub nonce: Vec<u8>,
397    /// Ciphertext output for one bstr-encoded Salted Disclosed Claim.
398    pub ciphertext: Vec<u8>,
399    /// AEAD authentication tag.
400    pub tag: Vec<u8>,
401    /// Optional context used by profiles to select the correct AEAD key.
402    pub key_context: Option<AeadKeyContext>,
403}
404
405impl AeadEncryptedDisclosure {
406    /// Converts this encrypted disclosure to its CBOR array value.
407    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    /// Decodes one encrypted disclosure from its CBOR array value.
424    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
468/// Reads `sd_aead_encrypted_claims` from an unprotected header.
469pub 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
486/// Writes `sd_aead_encrypted_claims` to an unprotected header.
487pub 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/// Result of converting a pre-issued claims value into an issued SD-CWT value.
508#[derive(Clone, Debug, PartialEq)]
509pub struct IssueResult {
510    /// The issued value with tag 58/62 requests replaced by redacted hashes.
511    pub value: Value,
512    /// The Salted Disclosed Claims created while issuing.
513    pub disclosures: DisclosureSet,
514}
515
516/// Converts a pre-issued claims value containing tag 58/62 requests into an issued value.
517///
518/// Tag 58 around a map key redacts that key/value pair. Tag 58 around an
519/// array element redacts that element. Tag 62 inserts a decoy redaction at
520/// that map or array position; the tag payload must be a positive integer
521/// that is unique within the SD-CWT being issued.
522pub 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/// Controls how unmatched Redacted Claim Hashes are handled during restoration.
541#[derive(Clone, Copy, Debug, PartialEq, Eq)]
542pub enum RestoreMode {
543    /// Holder validation: every redaction must have a matching disclosure.
544    Holder,
545    /// Verifier validation: undisclosed redactions and decoys are removed.
546    Verifier,
547}
548
549/// Result of restoring disclosed SD-CWT claims.
550#[derive(Clone, Debug, PartialEq)]
551pub struct RestoreReport {
552    /// The restored claims value.
553    pub value: Value,
554    /// Number of map claims or array elements restored from disclosures.
555    pub disclosed: usize,
556    /// Number of matching decoy redactions removed.
557    pub decoys: usize,
558    /// Number of redactions removed because no disclosure was presented.
559    pub removed_redactions: usize,
560}
561
562#[derive(Default)]
563struct RestoreStats {
564    disclosed: usize,
565    decoys: usize,
566    removed_redactions: usize,
567}
568
569/// Restores a claims value using Holder validation rules.
570pub 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
581/// Restores a claims value using Verifier validation rules.
582pub 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
593/// Restores a claims value using the selected validation mode.
594pub 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
619/// Decodes a COSE payload as CBOR and restores it using disclosures in the message header.
620///
621/// This helper supports the SD-CWT default hash algorithm, SHA-256. Use
622/// [`restore_payload_with_disclosures`] when a profile uses another hash.
623pub 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
632/// Decodes a COSE payload as CBOR and restores it with caller-supplied disclosures.
633pub 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}