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