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()).map_err(|e| SdJwtError::Payload {
167            reason: "not a JSON object".to_owned(),
168            source: Some(e),
169        })
170    }
171
172    /// The hash algorithm, from `_sd_alg`; SHA-256 when absent.
173    ///
174    /// # Errors
175    ///
176    /// [`SdJwtError::Payload`] when `_sd_alg` is not a string,
177    /// [`SdJwtError::UnsupportedHashAlgorithm`] when it names an algorithm this crate does not
178    /// implement.
179    pub fn hash_algorithm(&self) -> Result<HashAlgorithm, SdJwtError> {
180        hash_algorithm(&self.payload()?)
181    }
182
183    /// The compact serialization without the key binding JWT: the input of `sd_hash`.
184    pub fn to_string_without_key_binding(&self) -> String {
185        let mut s = String::from(self.jwt.as_str());
186        s.push('~');
187        for disclosure in &self.disclosures {
188            s.push_str(disclosure.as_str());
189            s.push('~');
190        }
191        s
192    }
193
194    /// The `sd_hash` a key binding JWT over this SD-JWT carries (RFC 9901 §4.3.1): the digest
195    /// under `alg` of the serialization without the key binding JWT.
196    pub fn sd_hash(&self, alg: HashAlgorithm) -> String {
197        alg.digest(self.to_string_without_key_binding().as_bytes())
198    }
199
200    /// Processes the disclosures into the payload (RFC 9901 §7.1, steps 3 to 5): every
201    /// disclosure must match a digest, no digest may appear twice, no disclosure may name a
202    /// reserved or existing claim.
203    ///
204    /// The claims may nest at most [`SdJwtBuilder::MAX_DEPTH`] containers deep, counting the
205    /// payload and those a disclosure's value adds alike, so a chain of disclosures, each
206    /// referencing the next, cannot nest them without bound. Whatever the builder issues stays
207    /// within the limit.
208    ///
209    /// # Errors
210    ///
211    /// [`SdJwtError::Payload`], [`SdJwtError::UnsupportedHashAlgorithm`],
212    /// [`SdJwtError::DuplicateDigest`], [`SdJwtError::UnusedDisclosure`],
213    /// [`SdJwtError::WrongDisclosureKind`], [`SdJwtError::ReservedClaimName`],
214    /// [`SdJwtError::DuplicateClaim`] or [`SdJwtError::TooDeep`].
215    pub fn process(&self) -> Result<Processed, SdJwtError> {
216        let mut payload = self.payload()?;
217        let alg = hash_algorithm(&payload)?;
218        payload.shift_remove(HashAlgorithm::CLAIM);
219        let mut processor = Processor::new(alg, &self.disclosures);
220        let issued = processor.object(payload, &ClaimPath::root(), None, 1)?;
221        if let Some(unused) = processor.used.iter().position(|used| !used) {
222            return Err(SdJwtError::UnusedDisclosure(
223                self.disclosures[unused].as_str().to_owned(),
224            ));
225        }
226        let mut payload = Value::Object(issued.clone());
227        strip_placeholders(&mut payload);
228        let Value::Object(payload) = payload else {
229            unreachable!("an object stays an object")
230        };
231        Ok(Processed {
232            payload,
233            disclosed: processor.disclosed,
234            undisclosed: processor.undisclosed,
235            issued,
236            parents: processor.parents,
237        })
238    }
239
240    /// This SD-JWT with only the disclosures `keep` selects and no key binding JWT, for
241    /// presentation (RFC 9901 §7.2). A kept disclosure nested in another needs that one kept
242    /// too.
243    ///
244    /// # Errors
245    ///
246    /// As [`process`](Self::process), plus [`SdJwtError::MissingParentDisclosure`].
247    pub fn select(&self, mut keep: impl FnMut(&Disclosure) -> bool) -> Result<Self, SdJwtError> {
248        let processed = self.process()?;
249        let kept: Vec<bool> = self.disclosures.iter().map(&mut keep).collect();
250        self.selected(&processed, &kept)
251    }
252
253    /// This SD-JWT with only the disclosures at `paths` and beneath them, plus the disclosures
254    /// they are nested in, and no key binding JWT. A path may use `null` for every element.
255    ///
256    /// # Errors
257    ///
258    /// As [`process`](Self::process), plus [`SdJwtError::NoSuchClaim`] for a path selecting
259    /// no disclosure.
260    pub fn disclose<'a>(
261        &self,
262        paths: impl IntoIterator<Item = &'a ClaimPath>,
263    ) -> Result<Self, SdJwtError> {
264        let processed = self.process()?;
265        let mut kept = vec![false; self.disclosures.len()];
266        for path in paths {
267            let mut found = false;
268            for claim in &processed.disclosed {
269                if path.is_prefix_of(&claim.path) {
270                    found = true;
271                    let mut index = Some(claim.disclosure);
272                    while let Some(i) = index {
273                        kept[i] = true;
274                        index = processed.parents[i];
275                    }
276                }
277            }
278            if !found {
279                return Err(SdJwtError::NoSuchClaim(path.clone()));
280            }
281        }
282        self.selected(&processed, &kept)
283    }
284
285    /// The SD-JWT with the disclosures flagged in `kept`, checking their parents are kept.
286    fn selected(&self, processed: &Processed, kept: &[bool]) -> Result<Self, SdJwtError> {
287        for (i, disclosure) in self.disclosures.iter().enumerate() {
288            if !kept[i] {
289                continue;
290            }
291            if let Some(parent) = processed.parents[i]
292                && !kept[parent]
293            {
294                return Err(SdJwtError::MissingParentDisclosure {
295                    disclosure: disclosure.as_str().to_owned(),
296                    parent: self.disclosures[parent].as_str().to_owned(),
297                });
298            }
299        }
300        let disclosures = self
301            .disclosures
302            .iter()
303            .zip(kept)
304            .filter(|(_, kept)| **kept)
305            .map(|(d, _)| d.clone())
306            .collect();
307        Ok(Self::new(self.jwt.clone(), disclosures))
308    }
309
310    /// This SD-JWT as an SD-JWT+KB: `binding` signed by the holder with `signer` over the
311    /// `sd_hash` of this SD-JWT (RFC 9901 §4.3), replacing any key binding JWT present.
312    ///
313    /// # Errors
314    ///
315    /// As [`hash_algorithm`](Self::hash_algorithm), or [`SdJwtError::Jws`] when signing
316    /// fails.
317    pub async fn sign_key_binding(
318        self,
319        binding: &KeyBinding,
320        signer: &dyn Signer,
321    ) -> Result<Self, SdJwtError> {
322        let sd_jwt = self.without_key_binding();
323        let alg = sd_jwt.hash_algorithm()?;
324        let claims = binding.claims(sd_jwt.sd_hash(alg));
325        let header = JwsHeader::new(signer.alg()).with_typ(KB_JWT_TYP);
326        let key_binding = Jwt::sign(&header, &claims, signer).await?;
327        Ok(sd_jwt.with_key_binding(key_binding))
328    }
329}
330
331/// The `_sd_alg` of `payload`.
332fn hash_algorithm(payload: &Map<String, Value>) -> Result<HashAlgorithm, SdJwtError> {
333    match payload.get(HashAlgorithm::CLAIM) {
334        None => Ok(HashAlgorithm::default()),
335        Some(Value::String(name)) => HashAlgorithm::parse(name),
336        Some(_) => Err(SdJwtError::payload("`_sd_alg` is not a string")),
337    }
338}
339
340impl fmt::Debug for SdJwt {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        f.debug_struct("SdJwt")
343            .field("jwt", &self.jwt)
344            .field("disclosures", &self.disclosures)
345            .field("key_binding", &self.key_binding)
346            .finish()
347    }
348}
349
350impl fmt::Display for SdJwt {
351    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352        f.write_str(&self.to_string_without_key_binding())?;
353        if let Some(key_binding) = &self.key_binding {
354            f.write_str(key_binding.as_str())?;
355        }
356        Ok(())
357    }
358}
359
360impl FromStr for SdJwt {
361    type Err = SdJwtError;
362
363    fn from_str(s: &str) -> Result<Self, Self::Err> {
364        Self::parse(s)
365    }
366}
367
368impl Serialize for SdJwt {
369    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
370        serializer.collect_str(self)
371    }
372}
373
374impl<'de> Deserialize<'de> for SdJwt {
375    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
376        let s = String::deserialize(deserializer)?;
377        Self::parse(&s).map_err(serde::de::Error::custom)
378    }
379}
380
381/// The outcome of [`SdJwt::process`]: the processed payload and where the disclosures went.
382#[derive(Debug, Clone, PartialEq, Eq)]
383#[non_exhaustive]
384pub struct Processed {
385    /// The processed SD-JWT payload: disclosed claims in place, `_sd`, `...` and `_sd_alg`
386    /// gone, undisclosed array elements removed.
387    pub payload: Map<String, Value>,
388    /// Every disclosure that was matched, with the path of the claim it disclosed.
389    pub disclosed: Vec<DisclosedClaim>,
390    /// The paths of array elements whose digest matched no disclosure, in the coordinates of
391    /// the payload as issued.
392    pub undisclosed: Vec<ClaimPath>,
393    issued: Map<String, Value>,
394    parents: Vec<Option<usize>>,
395}
396
397impl Processed {
398    /// The payload in the coordinates of the issued credential: as [`payload`](Self::payload),
399    /// except that an undisclosed array element stays in place as the `{"...": "<digest>"}`
400    /// object that referenced it. Claim paths of type metadata are evaluated against this.
401    pub fn as_issued(&self) -> &Map<String, Value> {
402        &self.issued
403    }
404
405    /// The disclosed claim paths, in the coordinates of the issued credential.
406    pub fn disclosed_paths(&self) -> impl Iterator<Item = &ClaimPath> {
407        self.disclosed.iter().map(|claim| &claim.path)
408    }
409
410    /// Whether the claim at the concrete `path` was disclosed by a disclosure of its own.
411    pub fn is_disclosed(&self, path: &ClaimPath) -> bool {
412        self.disclosed.iter().any(|claim| claim.path == *path)
413    }
414}
415
416/// A disclosure matched to its place in the payload.
417#[derive(Debug, Clone, PartialEq, Eq)]
418#[non_exhaustive]
419pub struct DisclosedClaim {
420    /// The path of the claim, in the coordinates of the issued credential.
421    pub path: ClaimPath,
422    /// The index of the disclosure in [`SdJwt::disclosures`].
423    pub disclosure: usize,
424}
425
426/// Whether `value` is an object with one key `...` referring to a string: the form a digest
427/// takes in an array.
428fn digest_placeholder(value: &Value) -> Option<&str> {
429    let object = value.as_object()?;
430    if object.len() != 1 {
431        return None;
432    }
433    object.get(ELLIPSIS_KEY)?.as_str()
434}
435
436/// Removes every undisclosed array element placeholder from `value`.
437fn strip_placeholders(value: &mut Value) {
438    match value {
439        Value::Object(object) => object.values_mut().for_each(strip_placeholders),
440        Value::Array(array) => {
441            array.retain(|element| digest_placeholder(element).is_none());
442            array.iter_mut().for_each(strip_placeholders);
443        }
444        _ => {}
445    }
446}
447
448/// The state of one [`SdJwt::process`] run.
449struct Processor<'a> {
450    disclosures: &'a [Disclosure],
451    digests: HashMap<String, usize>,
452    used: Vec<bool>,
453    parents: Vec<Option<usize>>,
454    seen: HashSet<String>,
455    disclosed: Vec<DisclosedClaim>,
456    undisclosed: Vec<ClaimPath>,
457}
458
459impl<'a> Processor<'a> {
460    fn new(alg: HashAlgorithm, disclosures: &'a [Disclosure]) -> Self {
461        let digests = disclosures
462            .iter()
463            .enumerate()
464            .map(|(i, d)| (d.digest(alg), i))
465            .collect();
466        Self {
467            disclosures,
468            digests,
469            used: vec![false; disclosures.len()],
470            parents: vec![None; disclosures.len()],
471            seen: HashSet::new(),
472            disclosed: Vec::new(),
473            undisclosed: Vec::new(),
474        }
475    }
476
477    /// The disclosure `digest` references, if any; rejects a digest seen before.
478    fn lookup(&mut self, digest: &str, owner: Option<usize>) -> Result<Option<usize>, SdJwtError> {
479        if !self.seen.insert(digest.to_owned()) {
480            return Err(SdJwtError::DuplicateDigest(digest.to_owned()));
481        }
482        let Some(&index) = self.digests.get(digest) else {
483            return Ok(None);
484        };
485        self.used[index] = true;
486        self.parents[index] = owner;
487        Ok(Some(index))
488    }
489
490    /// Processes `value`, a container `depth` containers deep counting itself, or a scalar.
491    fn value(
492        &mut self,
493        value: Value,
494        path: &ClaimPath,
495        owner: Option<usize>,
496        depth: usize,
497    ) -> Result<Value, SdJwtError> {
498        match value {
499            Value::Object(_) | Value::Array(_) if depth > SdJwtBuilder::MAX_DEPTH => {
500                Err(SdJwtError::TooDeep {
501                    max: SdJwtBuilder::MAX_DEPTH,
502                })
503            }
504            Value::Object(object) => self.object(object, path, owner, depth).map(Value::Object),
505            Value::Array(array) => self.array(array, path, owner, depth).map(Value::Array),
506            other => Ok(other),
507        }
508    }
509
510    fn object(
511        &mut self,
512        object: Map<String, Value>,
513        path: &ClaimPath,
514        owner: Option<usize>,
515        depth: usize,
516    ) -> Result<Map<String, Value>, SdJwtError> {
517        let mut out = Map::new();
518        let mut digests = None;
519        for (name, value) in object {
520            if name == SD_KEY {
521                let Value::Array(array) = value else {
522                    return Err(SdJwtError::payload(format!(
523                        "`_sd` at {path} is not an array"
524                    )));
525                };
526                digests = Some(array);
527                continue;
528            }
529            let value = self.value(value, &path.join(name.as_str()), owner, depth + 1)?;
530            out.insert(name, value);
531        }
532        for digest in digests.unwrap_or_default() {
533            let Value::String(digest) = digest else {
534                return Err(SdJwtError::payload(format!(
535                    "`_sd` at {path} holds a non-string"
536                )));
537            };
538            let Some(index) = self.lookup(&digest, owner)? else {
539                continue;
540            };
541            let disclosure = &self.disclosures[index];
542            let Some(name) = disclosure.name() else {
543                return Err(SdJwtError::WrongDisclosureKind {
544                    disclosure: disclosure.as_str().to_owned(),
545                    expected: "an object property disclosure",
546                });
547            };
548            if name == SD_KEY || name == ELLIPSIS_KEY {
549                return Err(SdJwtError::ReservedClaimName(name.to_owned()));
550            }
551            if out.contains_key(name) {
552                return Err(SdJwtError::DuplicateClaim {
553                    name: name.to_owned(),
554                    path: path.clone(),
555                });
556            }
557            let name = name.to_owned();
558            let claim_path = path.join(name.as_str());
559            let value = self.value(
560                disclosure.value().clone(),
561                &claim_path,
562                Some(index),
563                depth + 1,
564            )?;
565            out.insert(name, value);
566            self.disclosed.push(DisclosedClaim {
567                path: claim_path,
568                disclosure: index,
569            });
570        }
571        Ok(out)
572    }
573
574    fn array(
575        &mut self,
576        array: Vec<Value>,
577        path: &ClaimPath,
578        owner: Option<usize>,
579        depth: usize,
580    ) -> Result<Vec<Value>, SdJwtError> {
581        let mut out = Vec::with_capacity(array.len());
582        for (i, element) in array.into_iter().enumerate() {
583            let element_path = path.join(i);
584            let Some(digest) = digest_placeholder(&element).map(str::to_owned) else {
585                out.push(self.value(element, &element_path, owner, depth + 1)?);
586                continue;
587            };
588            let Some(index) = self.lookup(&digest, owner)? else {
589                self.undisclosed.push(element_path);
590                out.push(element);
591                continue;
592            };
593            let disclosure = &self.disclosures[index];
594            if !disclosure.is_element() {
595                return Err(SdJwtError::WrongDisclosureKind {
596                    disclosure: disclosure.as_str().to_owned(),
597                    expected: "an array element disclosure",
598                });
599            }
600            let value = self.value(
601                disclosure.value().clone(),
602                &element_path,
603                Some(index),
604                depth + 1,
605            )?;
606            out.push(value);
607            self.disclosed.push(DisclosedClaim {
608                path: element_path,
609                disclosure: index,
610            });
611        }
612        Ok(out)
613    }
614}
615
616/// What a key binding JWT asserts (RFC 9901 §4.3): the verifier it is for, the nonce of the
617/// transaction and when it was made; `sd_hash` is computed when the SD-JWT is signed.
618///
619/// # Examples
620///
621/// ```
622/// use chrono::{TimeZone as _, Utc};
623/// use identity_kit::sd_jwt_vc::KeyBinding;
624///
625/// let binding = KeyBinding::new(
626///     "https://verifier.example.org",
627///     "1234567890",
628///     Utc.timestamp_opt(1748536865, 0).unwrap(),
629/// );
630/// let claims = binding.claims("fMV05vuMARs3uG0Dg0BYd_7mQR9EGBkRof0cTZyuqXE".to_owned());
631/// assert_eq!(claims.iat, Some(1748536865));
632/// assert_eq!(claims.additional["nonce"], "1234567890");
633/// ```
634#[derive(Debug, Clone, PartialEq, Eq)]
635#[non_exhaustive]
636pub struct KeyBinding {
637    /// `aud`: the verifier the presentation is for.
638    pub audience: String,
639    /// `nonce`: the value binding the presentation to the transaction.
640    pub nonce: String,
641    /// `iat`: when the key binding JWT is issued.
642    pub issued_at: i64,
643    /// Further claims.
644    pub additional: IndexMap<String, Value>,
645}
646
647impl KeyBinding {
648    /// A key binding for `audience` with `nonce`, issued at `issued_at`.
649    pub fn new(
650        audience: impl Into<String>,
651        nonce: impl Into<String>,
652        issued_at: DateTime<Utc>,
653    ) -> Self {
654        Self {
655            audience: audience.into(),
656            nonce: nonce.into(),
657            issued_at: JwtClaims::numeric_date(issued_at),
658            additional: IndexMap::new(),
659        }
660    }
661
662    /// This binding with the claim `name` set to `value`.
663    #[must_use]
664    pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
665        self.additional.insert(name.into(), value.into());
666        self
667    }
668
669    /// The claims set of the key binding JWT, with `sd_hash`.
670    pub fn claims(&self, sd_hash: String) -> JwtClaims {
671        let mut claims = JwtClaims::new().with_aud(self.audience.clone());
672        claims.iat = Some(self.issued_at);
673        claims
674            .additional
675            .insert("nonce".to_owned(), Value::String(self.nonce.clone()));
676        claims
677            .additional
678            .insert("sd_hash".to_owned(), Value::String(sd_hash));
679        claims.additional.extend(self.additional.clone());
680        claims
681    }
682}