Skip to main content

identity_kit/sd_jwt_vc/
sd_jwt.rs

1//! The SD-JWT and SD-JWT+KB data formats (RFC 9901 §4) and their processing (§7).
2
3use core::fmt;
4use core::str::FromStr;
5use std::collections::{HashMap, HashSet};
6
7use chrono::{DateTime, Utc};
8use indexmap::IndexMap;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use serde_json::{Map, Value};
11
12use super::{ClaimPath, Disclosure, HashAlgorithm, SdJwtBuilder, SdJwtError};
13use crate::jose::{JwsHeader, Signer};
14use crate::vc::{Jwt, JwtClaims};
15
16/// The `_sd` key: the digests of the disclosable properties of an object.
17pub const SD_KEY: &str = "_sd";
18
19/// The `...` key: the digest of a disclosable array element.
20pub const ELLIPSIS_KEY: &str = "...";
21
22/// The `typ` of a key binding JWT.
23pub const KB_JWT_TYP: &str = "kb+jwt";
24
25/// An SD-JWT: an issuer-signed JWT, its disclosures and, in an SD-JWT+KB, a key binding JWT,
26/// as the compact serialization `<JWT>~<Disclosure 1>~…~<Disclosure N>~[<KB-JWT>]`.
27///
28/// Parsing checks the structure only. [`SdJwt::process`] matches the disclosures to the
29/// digests and yields the processed payload; the signature is the validator's business
30/// ([`SdJwtCredentialValidator`](super::SdJwtCredentialValidator) for an SD-JWT VC).
31///
32/// # Examples
33///
34/// The presentation of RFC 9901 §5.2, processed.
35///
36/// ```
37/// use identity_kit::sd_jwt_vc::SdJwt;
38/// use serde_json::json;
39///
40/// let presentation = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkNyUWU3UzVr\
41///     cUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVa\
42///     dTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFV\
43///     QTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAi\
44///     WFFfM2tQS3QxWHlYN0tBTmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242\
45///     Q0pEYzZ2Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFrb2I5\
46///     aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpnbGhRRzBEcGZheVF3\
47///     TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAw\
48///     MDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjogInVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7\
49///     Ii4uLiI6ICJwRm5kamtaX1ZDem15VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4u\
50///     IjogIjdDZjZKa1B1ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxn\
51///     IjogInNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0yNTYiLCAi\
52///     eCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VHZW1jIiwgInkiOiAiWnhq\
53///     aVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlGMkhaUSJ9fX0.xJUoRLl1sVRxz57ralck\
54///     9kBWbzbq0soK6dQ5liBXuge3IY8vVYq2xi1tywYwvlRTtPIM2A-XDzyta-SaJLhexQ~WyJlbHVWNU9nM\
55///     2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9x\
56///     Uk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5\
57///     IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyIyR0xD\
58///     NDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJsa2x4RjVqTVlsR1RQVW9\
59///     2TU5JdkNBIiwgIlVTIl0~eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImtiK2p3dCJ9.eyJub25jZSI6IC\
60///     IxMjM0NTY3ODkwIiwgImF1ZCI6ICJodHRwczovL3ZlcmlmaWVyLmV4YW1wbGUub3JnIiwgImlhdCI6ID\
61///     E3NDg1MzY4NjUsICJzZF9oYXNoIjogImZNVjA1dnVNQVJzM3VHMERnMEJZZF83bVFSOUVHQmtSb2YwY1\
62///     RaeXVxWEUifQ.76YniH11s4EDLnqNXKzYtIU1yXEBNzY8wfTd1QkpKzcJ0JY_KXbURL8qQBJA7xpZiA8\
63///     vXYrlpPpd4yfHJTfhQA";
64/// let sd_jwt: SdJwt = presentation.parse()?;
65/// assert_eq!(sd_jwt.disclosures().len(), 4);
66/// assert!(sd_jwt.key_binding_jwt().is_some());
67///
68/// let processed = sd_jwt.process()?;
69/// assert_eq!(processed.payload["given_name"], "John");
70/// assert_eq!(processed.payload["nationalities"], json!(["US"]));
71/// assert!(!processed.payload.contains_key("email"));
72/// # Ok::<(), identity_kit::sd_jwt_vc::SdJwtError>(())
73/// ```
74#[derive(Clone, PartialEq, Eq)]
75pub struct SdJwt {
76    jwt: Jwt,
77    disclosures: Vec<Disclosure>,
78    key_binding: Option<Jwt>,
79}
80
81impl SdJwt {
82    /// An SD-JWT of `jwt` and `disclosures`, without key binding.
83    pub fn new(jwt: Jwt, disclosures: Vec<Disclosure>) -> Self {
84        Self {
85            jwt,
86            disclosures,
87            key_binding: None,
88        }
89    }
90
91    /// Parses the compact serialization.
92    ///
93    /// # Errors
94    ///
95    /// [`SdJwtError::Format`] without a `~` or with an empty disclosure, [`SdJwtError::Jws`]
96    /// when the JWT or the key binding JWT does not decode,
97    /// [`SdJwtError::InvalidDisclosure`] and [`SdJwtError::DuplicateDisclosure`].
98    pub fn parse(s: &str) -> Result<Self, SdJwtError> {
99        let parts: Vec<&str> = s.split('~').collect();
100        let [jwt, middle @ .., last] = parts.as_slice() else {
101            return Err(SdJwtError::Format(
102                "no `~` after the issuer-signed JWT".into(),
103            ));
104        };
105        let jwt = Jwt::new(*jwt)?;
106        let key_binding = if last.is_empty() {
107            None
108        } else {
109            Some(Jwt::new(*last)?)
110        };
111        let mut disclosures = Vec::with_capacity(middle.len());
112        let mut seen = HashSet::new();
113        for part in middle {
114            if part.is_empty() {
115                return Err(SdJwtError::Format("empty disclosure".into()));
116            }
117            if !seen.insert(*part) {
118                return Err(SdJwtError::DuplicateDisclosure((*part).to_owned()));
119            }
120            disclosures.push(Disclosure::parse(part)?);
121        }
122        Ok(Self {
123            jwt,
124            disclosures,
125            key_binding,
126        })
127    }
128
129    /// The issuer-signed JWT.
130    pub fn jwt(&self) -> &Jwt {
131        &self.jwt
132    }
133
134    /// The disclosures, in order.
135    pub fn disclosures(&self) -> &[Disclosure] {
136        &self.disclosures
137    }
138
139    /// The key binding JWT, when this is an SD-JWT+KB.
140    pub fn key_binding_jwt(&self) -> Option<&Jwt> {
141        self.key_binding.as_ref()
142    }
143
144    /// This SD-JWT with `key_binding` as its key binding JWT.
145    #[must_use]
146    pub fn with_key_binding(mut self, key_binding: Jwt) -> Self {
147        self.key_binding = Some(key_binding);
148        self
149    }
150
151    /// This SD-JWT without its key binding JWT.
152    #[must_use]
153    pub fn without_key_binding(mut self) -> Self {
154        self.key_binding = None;
155        self
156    }
157
158    /// The issuer-signed JWT payload as issued: digests in place, nothing disclosed, nothing
159    /// verified.
160    ///
161    /// # Errors
162    ///
163    /// [`SdJwtError::Payload`] when the payload is not a JSON object.
164    pub fn payload(&self) -> Result<Map<String, Value>, SdJwtError> {
165        let jws = self.jwt.decode()?;
166        serde_json::from_slice(jws.payload())
167            .map_err(|e| SdJwtError::Payload(format!("not a JSON object: {e}")))
168    }
169
170    /// The hash algorithm, from `_sd_alg`; SHA-256 when absent.
171    ///
172    /// # Errors
173    ///
174    /// [`SdJwtError::Payload`] when `_sd_alg` is not a string,
175    /// [`SdJwtError::UnsupportedHashAlgorithm`] when it names an algorithm this crate does not
176    /// implement.
177    pub fn hash_algorithm(&self) -> Result<HashAlgorithm, SdJwtError> {
178        hash_algorithm(&self.payload()?)
179    }
180
181    /// The compact serialization without the key binding JWT: the input of `sd_hash`.
182    pub fn to_string_without_key_binding(&self) -> String {
183        let mut s = String::from(self.jwt.as_str());
184        s.push('~');
185        for disclosure in &self.disclosures {
186            s.push_str(disclosure.as_str());
187            s.push('~');
188        }
189        s
190    }
191
192    /// The `sd_hash` a key binding JWT over this SD-JWT carries (RFC 9901 §4.3.1): the digest
193    /// under `alg` of the serialization without the key binding JWT.
194    pub fn sd_hash(&self, alg: HashAlgorithm) -> String {
195        alg.digest(self.to_string_without_key_binding().as_bytes())
196    }
197
198    /// Processes the disclosures into the payload (RFC 9901 §7.1, steps 3 to 5): every
199    /// disclosure must match a digest, no digest may appear twice, no disclosure may name a
200    /// reserved or existing claim.
201    ///
202    /// The claims may nest at most [`SdJwtBuilder::MAX_DEPTH`] containers deep, counting the
203    /// payload and those a disclosure's value adds alike, so a chain of disclosures, each
204    /// referencing the next, cannot nest them without bound. Whatever the builder issues stays
205    /// within the limit.
206    ///
207    /// # Errors
208    ///
209    /// [`SdJwtError::Payload`], [`SdJwtError::UnsupportedHashAlgorithm`],
210    /// [`SdJwtError::DuplicateDigest`], [`SdJwtError::UnusedDisclosure`],
211    /// [`SdJwtError::WrongDisclosureKind`], [`SdJwtError::ReservedClaimName`],
212    /// [`SdJwtError::DuplicateClaim`] or [`SdJwtError::TooDeep`].
213    pub fn process(&self) -> Result<Processed, SdJwtError> {
214        let mut payload = self.payload()?;
215        let alg = hash_algorithm(&payload)?;
216        payload.shift_remove(HashAlgorithm::CLAIM);
217        let mut processor = Processor::new(alg, &self.disclosures);
218        let issued = processor.object(payload, &ClaimPath::root(), None, 1)?;
219        if let Some(unused) = processor.used.iter().position(|used| !used) {
220            return Err(SdJwtError::UnusedDisclosure(
221                self.disclosures[unused].as_str().to_owned(),
222            ));
223        }
224        let mut payload = Value::Object(issued.clone());
225        strip_placeholders(&mut payload);
226        let Value::Object(payload) = payload else {
227            unreachable!("an object stays an object")
228        };
229        Ok(Processed {
230            payload,
231            disclosed: processor.disclosed,
232            undisclosed: processor.undisclosed,
233            issued,
234            parents: processor.parents,
235        })
236    }
237
238    /// This SD-JWT with only the disclosures `keep` selects and no key binding JWT, for
239    /// presentation (RFC 9901 §7.2). A kept disclosure nested in another needs that one kept
240    /// too.
241    ///
242    /// # Errors
243    ///
244    /// As [`process`](Self::process), plus [`SdJwtError::MissingParentDisclosure`].
245    pub fn select(&self, mut keep: impl FnMut(&Disclosure) -> bool) -> Result<Self, SdJwtError> {
246        let processed = self.process()?;
247        let kept: Vec<bool> = self.disclosures.iter().map(&mut keep).collect();
248        self.selected(&processed, &kept)
249    }
250
251    /// This SD-JWT with only the disclosures at `paths` and beneath them, plus the disclosures
252    /// they are nested in, and no key binding JWT. A path may use `null` for every element.
253    ///
254    /// # Errors
255    ///
256    /// As [`process`](Self::process), plus [`SdJwtError::NoSuchClaim`] for a path selecting
257    /// no disclosure.
258    pub fn disclose<'a>(
259        &self,
260        paths: impl IntoIterator<Item = &'a ClaimPath>,
261    ) -> Result<Self, SdJwtError> {
262        let processed = self.process()?;
263        let mut kept = vec![false; self.disclosures.len()];
264        for path in paths {
265            let mut found = false;
266            for claim in &processed.disclosed {
267                if path.is_prefix_of(&claim.path) {
268                    found = true;
269                    let mut index = Some(claim.disclosure);
270                    while let Some(i) = index {
271                        kept[i] = true;
272                        index = processed.parents[i];
273                    }
274                }
275            }
276            if !found {
277                return Err(SdJwtError::NoSuchClaim(path.clone()));
278            }
279        }
280        self.selected(&processed, &kept)
281    }
282
283    /// The SD-JWT with the disclosures flagged in `kept`, checking their parents are kept.
284    fn selected(&self, processed: &Processed, kept: &[bool]) -> Result<Self, SdJwtError> {
285        for (i, disclosure) in self.disclosures.iter().enumerate() {
286            if !kept[i] {
287                continue;
288            }
289            if let Some(parent) = processed.parents[i]
290                && !kept[parent]
291            {
292                return Err(SdJwtError::MissingParentDisclosure {
293                    disclosure: disclosure.as_str().to_owned(),
294                    parent: self.disclosures[parent].as_str().to_owned(),
295                });
296            }
297        }
298        let disclosures = self
299            .disclosures
300            .iter()
301            .zip(kept)
302            .filter(|(_, kept)| **kept)
303            .map(|(d, _)| d.clone())
304            .collect();
305        Ok(Self::new(self.jwt.clone(), disclosures))
306    }
307
308    /// This SD-JWT as an SD-JWT+KB: `binding` signed by the holder with `signer` over the
309    /// `sd_hash` of this SD-JWT (RFC 9901 §4.3), replacing any key binding JWT present.
310    ///
311    /// # Errors
312    ///
313    /// As [`hash_algorithm`](Self::hash_algorithm), or [`SdJwtError::Jws`] when signing
314    /// fails.
315    pub async fn sign_key_binding(
316        self,
317        binding: &KeyBinding,
318        signer: &dyn Signer,
319    ) -> Result<Self, SdJwtError> {
320        let sd_jwt = self.without_key_binding();
321        let alg = sd_jwt.hash_algorithm()?;
322        let claims = binding.claims(sd_jwt.sd_hash(alg));
323        let header = JwsHeader::new(signer.alg()).with_typ(KB_JWT_TYP);
324        let key_binding = Jwt::sign(&header, &claims, signer).await?;
325        Ok(sd_jwt.with_key_binding(key_binding))
326    }
327}
328
329/// The `_sd_alg` of `payload`.
330fn hash_algorithm(payload: &Map<String, Value>) -> Result<HashAlgorithm, SdJwtError> {
331    match payload.get(HashAlgorithm::CLAIM) {
332        None => Ok(HashAlgorithm::default()),
333        Some(Value::String(name)) => HashAlgorithm::parse(name),
334        Some(_) => Err(SdJwtError::Payload("`_sd_alg` is not a string".into())),
335    }
336}
337
338impl fmt::Debug for SdJwt {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        f.debug_struct("SdJwt")
341            .field("jwt", &self.jwt)
342            .field("disclosures", &self.disclosures)
343            .field("key_binding", &self.key_binding)
344            .finish()
345    }
346}
347
348impl fmt::Display for SdJwt {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        f.write_str(&self.to_string_without_key_binding())?;
351        if let Some(key_binding) = &self.key_binding {
352            f.write_str(key_binding.as_str())?;
353        }
354        Ok(())
355    }
356}
357
358impl FromStr for SdJwt {
359    type Err = SdJwtError;
360
361    fn from_str(s: &str) -> Result<Self, Self::Err> {
362        Self::parse(s)
363    }
364}
365
366impl Serialize for SdJwt {
367    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
368        serializer.collect_str(self)
369    }
370}
371
372impl<'de> Deserialize<'de> for SdJwt {
373    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
374        let s = String::deserialize(deserializer)?;
375        Self::parse(&s).map_err(serde::de::Error::custom)
376    }
377}
378
379/// The outcome of [`SdJwt::process`]: the processed payload and where the disclosures went.
380#[derive(Debug, Clone, PartialEq, Eq)]
381#[non_exhaustive]
382pub struct Processed {
383    /// The processed SD-JWT payload: disclosed claims in place, `_sd`, `...` and `_sd_alg`
384    /// gone, undisclosed array elements removed.
385    pub payload: Map<String, Value>,
386    /// Every disclosure that was matched, with the path of the claim it disclosed.
387    pub disclosed: Vec<DisclosedClaim>,
388    /// The paths of array elements whose digest matched no disclosure, in the coordinates of
389    /// the payload as issued.
390    pub undisclosed: Vec<ClaimPath>,
391    issued: Map<String, Value>,
392    parents: Vec<Option<usize>>,
393}
394
395impl Processed {
396    /// The payload in the coordinates of the issued credential: as [`payload`](Self::payload),
397    /// except that an undisclosed array element stays in place as the `{"...": "<digest>"}`
398    /// object that referenced it. Claim paths of type metadata are evaluated against this.
399    pub fn as_issued(&self) -> &Map<String, Value> {
400        &self.issued
401    }
402
403    /// The disclosed claim paths, in the coordinates of the issued credential.
404    pub fn disclosed_paths(&self) -> impl Iterator<Item = &ClaimPath> {
405        self.disclosed.iter().map(|claim| &claim.path)
406    }
407
408    /// Whether the claim at the concrete `path` was disclosed by a disclosure of its own.
409    pub fn is_disclosed(&self, path: &ClaimPath) -> bool {
410        self.disclosed.iter().any(|claim| claim.path == *path)
411    }
412}
413
414/// A disclosure matched to its place in the payload.
415#[derive(Debug, Clone, PartialEq, Eq)]
416#[non_exhaustive]
417pub struct DisclosedClaim {
418    /// The path of the claim, in the coordinates of the issued credential.
419    pub path: ClaimPath,
420    /// The index of the disclosure in [`SdJwt::disclosures`].
421    pub disclosure: usize,
422}
423
424/// Whether `value` is an object with one key `...` referring to a string: the form a digest
425/// takes in an array.
426fn digest_placeholder(value: &Value) -> Option<&str> {
427    let object = value.as_object()?;
428    if object.len() != 1 {
429        return None;
430    }
431    object.get(ELLIPSIS_KEY)?.as_str()
432}
433
434/// Removes every undisclosed array element placeholder from `value`.
435fn strip_placeholders(value: &mut Value) {
436    match value {
437        Value::Object(object) => object.values_mut().for_each(strip_placeholders),
438        Value::Array(array) => {
439            array.retain(|element| digest_placeholder(element).is_none());
440            array.iter_mut().for_each(strip_placeholders);
441        }
442        _ => {}
443    }
444}
445
446/// The state of one [`SdJwt::process`] run.
447struct Processor<'a> {
448    disclosures: &'a [Disclosure],
449    digests: HashMap<String, usize>,
450    used: Vec<bool>,
451    parents: Vec<Option<usize>>,
452    seen: HashSet<String>,
453    disclosed: Vec<DisclosedClaim>,
454    undisclosed: Vec<ClaimPath>,
455}
456
457impl<'a> Processor<'a> {
458    fn new(alg: HashAlgorithm, disclosures: &'a [Disclosure]) -> Self {
459        let digests = disclosures
460            .iter()
461            .enumerate()
462            .map(|(i, d)| (d.digest(alg), i))
463            .collect();
464        Self {
465            disclosures,
466            digests,
467            used: vec![false; disclosures.len()],
468            parents: vec![None; disclosures.len()],
469            seen: HashSet::new(),
470            disclosed: Vec::new(),
471            undisclosed: Vec::new(),
472        }
473    }
474
475    /// The disclosure `digest` references, if any; rejects a digest seen before.
476    fn lookup(&mut self, digest: &str, owner: Option<usize>) -> Result<Option<usize>, SdJwtError> {
477        if !self.seen.insert(digest.to_owned()) {
478            return Err(SdJwtError::DuplicateDigest(digest.to_owned()));
479        }
480        let Some(&index) = self.digests.get(digest) else {
481            return Ok(None);
482        };
483        self.used[index] = true;
484        self.parents[index] = owner;
485        Ok(Some(index))
486    }
487
488    /// Processes `value`, a container `depth` containers deep counting itself, or a scalar.
489    fn value(
490        &mut self,
491        value: Value,
492        path: &ClaimPath,
493        owner: Option<usize>,
494        depth: usize,
495    ) -> Result<Value, SdJwtError> {
496        match value {
497            Value::Object(_) | Value::Array(_) if depth > SdJwtBuilder::MAX_DEPTH => {
498                Err(SdJwtError::TooDeep {
499                    max: SdJwtBuilder::MAX_DEPTH,
500                })
501            }
502            Value::Object(object) => self.object(object, path, owner, depth).map(Value::Object),
503            Value::Array(array) => self.array(array, path, owner, depth).map(Value::Array),
504            other => Ok(other),
505        }
506    }
507
508    fn object(
509        &mut self,
510        object: Map<String, Value>,
511        path: &ClaimPath,
512        owner: Option<usize>,
513        depth: usize,
514    ) -> Result<Map<String, Value>, SdJwtError> {
515        let mut out = Map::new();
516        let mut digests = None;
517        for (name, value) in object {
518            if name == SD_KEY {
519                let Value::Array(array) = value else {
520                    return Err(SdJwtError::Payload(format!(
521                        "`_sd` at {path} is not an array"
522                    )));
523                };
524                digests = Some(array);
525                continue;
526            }
527            let value = self.value(value, &path.join(name.as_str()), owner, depth + 1)?;
528            out.insert(name, value);
529        }
530        for digest in digests.unwrap_or_default() {
531            let Value::String(digest) = digest else {
532                return Err(SdJwtError::Payload(format!(
533                    "`_sd` at {path} holds a non-string"
534                )));
535            };
536            let Some(index) = self.lookup(&digest, owner)? else {
537                continue;
538            };
539            let disclosure = &self.disclosures[index];
540            let Some(name) = disclosure.name() else {
541                return Err(SdJwtError::WrongDisclosureKind {
542                    disclosure: disclosure.as_str().to_owned(),
543                    expected: "an object property disclosure",
544                });
545            };
546            if name == SD_KEY || name == ELLIPSIS_KEY {
547                return Err(SdJwtError::ReservedClaimName(name.to_owned()));
548            }
549            if out.contains_key(name) {
550                return Err(SdJwtError::DuplicateClaim {
551                    name: name.to_owned(),
552                    path: path.clone(),
553                });
554            }
555            let name = name.to_owned();
556            let claim_path = path.join(name.as_str());
557            let value = self.value(
558                disclosure.value().clone(),
559                &claim_path,
560                Some(index),
561                depth + 1,
562            )?;
563            out.insert(name, value);
564            self.disclosed.push(DisclosedClaim {
565                path: claim_path,
566                disclosure: index,
567            });
568        }
569        Ok(out)
570    }
571
572    fn array(
573        &mut self,
574        array: Vec<Value>,
575        path: &ClaimPath,
576        owner: Option<usize>,
577        depth: usize,
578    ) -> Result<Vec<Value>, SdJwtError> {
579        let mut out = Vec::with_capacity(array.len());
580        for (i, element) in array.into_iter().enumerate() {
581            let element_path = path.join(i);
582            let Some(digest) = digest_placeholder(&element).map(str::to_owned) else {
583                out.push(self.value(element, &element_path, owner, depth + 1)?);
584                continue;
585            };
586            let Some(index) = self.lookup(&digest, owner)? else {
587                self.undisclosed.push(element_path);
588                out.push(element);
589                continue;
590            };
591            let disclosure = &self.disclosures[index];
592            if !disclosure.is_element() {
593                return Err(SdJwtError::WrongDisclosureKind {
594                    disclosure: disclosure.as_str().to_owned(),
595                    expected: "an array element disclosure",
596                });
597            }
598            let value = self.value(
599                disclosure.value().clone(),
600                &element_path,
601                Some(index),
602                depth + 1,
603            )?;
604            out.push(value);
605            self.disclosed.push(DisclosedClaim {
606                path: element_path,
607                disclosure: index,
608            });
609        }
610        Ok(out)
611    }
612}
613
614/// What a key binding JWT asserts (RFC 9901 §4.3): the verifier it is for, the nonce of the
615/// transaction and when it was made; `sd_hash` is computed when the SD-JWT is signed.
616///
617/// # Examples
618///
619/// ```
620/// use chrono::{TimeZone as _, Utc};
621/// use identity_kit::sd_jwt_vc::KeyBinding;
622///
623/// let binding = KeyBinding::new(
624///     "https://verifier.example.org",
625///     "1234567890",
626///     Utc.timestamp_opt(1748536865, 0).unwrap(),
627/// );
628/// let claims = binding.claims("fMV05vuMARs3uG0Dg0BYd_7mQR9EGBkRof0cTZyuqXE".to_owned());
629/// assert_eq!(claims.iat, Some(1748536865));
630/// assert_eq!(claims.additional["nonce"], "1234567890");
631/// ```
632#[derive(Debug, Clone, PartialEq, Eq)]
633#[non_exhaustive]
634pub struct KeyBinding {
635    /// `aud`: the verifier the presentation is for.
636    pub audience: String,
637    /// `nonce`: the value binding the presentation to the transaction.
638    pub nonce: String,
639    /// `iat`: when the key binding JWT is issued.
640    pub issued_at: i64,
641    /// Further claims.
642    pub additional: IndexMap<String, Value>,
643}
644
645impl KeyBinding {
646    /// A key binding for `audience` with `nonce`, issued at `issued_at`.
647    pub fn new(
648        audience: impl Into<String>,
649        nonce: impl Into<String>,
650        issued_at: DateTime<Utc>,
651    ) -> Self {
652        Self {
653            audience: audience.into(),
654            nonce: nonce.into(),
655            issued_at: JwtClaims::numeric_date(issued_at),
656            additional: IndexMap::new(),
657        }
658    }
659
660    /// This binding with the claim `name` set to `value`.
661    #[must_use]
662    pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
663        self.additional.insert(name.into(), value.into());
664        self
665    }
666
667    /// The claims set of the key binding JWT, with `sd_hash`.
668    pub fn claims(&self, sd_hash: String) -> JwtClaims {
669        let mut claims = JwtClaims::new().with_aud(self.audience.clone());
670        claims.iat = Some(self.issued_at);
671        claims
672            .additional
673            .insert("nonce".to_owned(), Value::String(self.nonce.clone()));
674        claims
675            .additional
676            .insert("sd_hash".to_owned(), Value::String(sd_hash));
677        claims.additional.extend(self.additional.clone());
678        claims
679    }
680}