Skip to main content

sd_jwt_payload/
sd_jwt.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashSet;
5use std::fmt::Display;
6use std::ops::Deref;
7use std::ops::DerefMut;
8use std::str::FromStr;
9
10use crate::jwt::Jwt;
11use crate::Disclosure;
12use crate::Error;
13use crate::Hasher;
14use crate::JsonObject;
15use crate::KeyBindingJwt;
16use crate::RequiredKeyBinding;
17use crate::Result;
18use crate::SdObjectDecoder;
19use crate::ARRAY_DIGEST_KEY;
20use crate::DIGESTS_KEY;
21use crate::SHA_ALG_NAME;
22use indexmap::IndexMap;
23use itertools::Either;
24use itertools::Itertools;
25use serde::Deserialize;
26use serde::Serialize;
27use serde_json::Value;
28
29#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Default)]
30pub struct SdJwtClaims {
31  #[serde(skip_serializing_if = "Vec::is_empty", default)]
32  pub _sd: Vec<String>,
33  #[serde(skip_serializing_if = "Option::is_none")]
34  pub _sd_alg: Option<String>,
35  #[serde(skip_serializing_if = "Option::is_none")]
36  pub cnf: Option<RequiredKeyBinding>,
37  #[serde(flatten)]
38  properties: JsonObject,
39}
40
41impl Deref for SdJwtClaims {
42  type Target = JsonObject;
43  fn deref(&self) -> &Self::Target {
44    &self.properties
45  }
46}
47
48impl DerefMut for SdJwtClaims {
49  fn deref_mut(&mut self) -> &mut Self::Target {
50    &mut self.properties
51  }
52}
53
54/// Representation of an SD-JWT of the format
55/// `<Issuer-signed JWT>~<D.1>~<D.2>~...~<D.N>~<optional KB-JWT>`.
56#[derive(Debug, Clone, Eq, PartialEq)]
57pub struct SdJwt {
58  /// The JWT part.
59  jwt: Jwt<SdJwtClaims>,
60  /// The disclosures part.
61  disclosures: Vec<Disclosure>,
62  /// The optional key binding JWT.
63  key_binding_jwt: Option<KeyBindingJwt>,
64}
65
66impl SdJwt {
67  /// Creates a new [`SdJwt`] from its components.
68  pub(crate) fn new(
69    jwt: Jwt<SdJwtClaims>,
70    disclosures: Vec<Disclosure>,
71    key_binding_jwt: Option<KeyBindingJwt>,
72  ) -> Self {
73    Self {
74      jwt,
75      disclosures,
76      key_binding_jwt,
77    }
78  }
79
80  pub fn headers(&self) -> &JsonObject {
81    &self.jwt.header
82  }
83
84  pub fn claims(&self) -> &SdJwtClaims {
85    &self.jwt.claims
86  }
87
88  /// Returns a mutable reference to this SD-JWT's claims.
89  /// ## Warning
90  /// Modifying the claims might invalidate the signature.
91  /// Use this method carefully.
92  pub fn claims_mut(&mut self) -> &mut SdJwtClaims {
93    &mut self.jwt.claims
94  }
95
96  /// Returns the disclosures of this SD-JWT.
97  pub fn disclosures(&self) -> &[Disclosure] {
98    &self.disclosures
99  }
100
101  /// Returns the required key binding of this SD-JWT, if any.
102  pub fn required_key_bind(&self) -> Option<&RequiredKeyBinding> {
103    self.claims().cnf.as_ref()
104  }
105
106  /// Returns the key binding JWT of this SD-JWT, if any.
107  pub fn key_binding_jwt(&self) -> Option<&KeyBindingJwt> {
108    self.key_binding_jwt.as_ref()
109  }
110
111  /// Attaches a [KeyBindingJwt] to this SD-JWT.
112  /// ## Notes
113  /// This method overwrites any existing [KeyBindingJwt] and does **not**
114  /// perform any sort of validation of the passed KB-JWT.
115  pub fn attach_key_binding_jwt(&mut self, kb_jwt: KeyBindingJwt) {
116    self.key_binding_jwt = Some(kb_jwt);
117  }
118
119  /// Serializes the components into the final SD-JWT.
120  pub fn presentation(&self) -> String {
121    let disclosures = self.disclosures.iter().map(ToString::to_string).join("~");
122    let key_bindings = self
123      .key_binding_jwt
124      .as_ref()
125      .map(ToString::to_string)
126      .unwrap_or_default();
127    if disclosures.is_empty() {
128      format!("{}~{}", self.jwt, key_bindings)
129    } else {
130      format!("{}~{}~{}", self.jwt, disclosures, key_bindings)
131    }
132  }
133
134  /// Parses an SD-JWT into its components as [`SdJwt`].
135  pub fn parse(sd_jwt: &str) -> Result<Self> {
136    let sd_segments: Vec<&str> = sd_jwt.split('~').collect();
137    let num_of_segments = sd_segments.len();
138    if num_of_segments < 2 {
139      return Err(Error::DeserializationError(
140        "SD-JWT format is invalid, less than 2 segments".to_string(),
141      ));
142    }
143
144    let jwt = sd_segments.first().unwrap().parse()?;
145
146    let disclosures = sd_segments[1..num_of_segments - 1]
147      .iter()
148      .map(|s| Disclosure::parse(s))
149      .try_collect()?;
150
151    let key_binding_jwt = sd_segments
152      .last()
153      .filter(|segment| !segment.is_empty())
154      .map(|segment| segment.parse())
155      .transpose()?;
156
157    Ok(Self {
158      jwt,
159      disclosures,
160      key_binding_jwt,
161    })
162  }
163
164  /// Prepares this [`SdJwt`] for a presentation, returning an [`SdJwtPresentationBuilder`].
165  /// ## Errors
166  /// - [`Error::InvalidHasher`] is returned if the provided `hasher`'s algorithm doesn't match the algorithm specified
167  ///   by SD-JWT's `_sd_alg` claim. "sha-256" is used if the claim is missing.
168  pub fn into_presentation(self, hasher: &dyn Hasher) -> Result<SdJwtPresentationBuilder> {
169    SdJwtPresentationBuilder::new(self, hasher)
170  }
171
172  /// Returns the JSON object obtained by replacing all disclosures into their
173  /// corresponding JWT concealable claims.
174  pub fn into_disclosed_object(self, hasher: &dyn Hasher) -> Result<JsonObject> {
175    let decoder = SdObjectDecoder;
176    let object = serde_json::to_value(self.claims()).unwrap();
177
178    let disclosure_map = self
179      .disclosures
180      .into_iter()
181      .map(|disclosure| (hasher.encoded_digest(disclosure.as_str()), disclosure))
182      .collect();
183
184    decoder.decode(object.as_object().unwrap(), &disclosure_map)
185  }
186}
187
188impl Display for SdJwt {
189  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190    f.write_str(&(self.presentation()))
191  }
192}
193
194impl FromStr for SdJwt {
195  type Err = Error;
196  fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
197    Self::parse(s)
198  }
199}
200
201#[derive(Debug, Clone)]
202pub struct SdJwtPresentationBuilder {
203  sd_jwt: SdJwt,
204  disclosures: IndexMap<String, Disclosure>,
205  disclosures_to_omit: HashSet<usize>,
206  object: Value,
207}
208
209impl Deref for SdJwtPresentationBuilder {
210  type Target = SdJwt;
211  fn deref(&self) -> &Self::Target {
212    &self.sd_jwt
213  }
214}
215
216impl SdJwtPresentationBuilder {
217  pub fn new(mut sd_jwt: SdJwt, hasher: &dyn Hasher) -> Result<Self> {
218    let required_hasher = sd_jwt.claims()._sd_alg.as_deref().unwrap_or(SHA_ALG_NAME);
219    if required_hasher != hasher.alg_name() {
220      return Err(Error::InvalidHasher(format!(
221        "hasher \"{}\" was provided, but \"{required_hasher} is required\"",
222        hasher.alg_name()
223      )));
224    }
225    let disclosures = std::mem::take(&mut sd_jwt.disclosures)
226      .into_iter()
227      .map(|disclosure| (hasher.encoded_digest(disclosure.as_str()), disclosure))
228      .collect();
229    let object = {
230      let sd = std::mem::take(&mut sd_jwt.jwt.claims._sd)
231        .into_iter()
232        .map(Value::String)
233        .collect();
234      let mut object = Value::Object(std::mem::take(&mut sd_jwt.jwt.claims.properties));
235      object
236        .as_object_mut()
237        .unwrap()
238        .insert(DIGESTS_KEY.to_string(), Value::Array(sd));
239
240      object
241    };
242    Ok(Self {
243      sd_jwt,
244      disclosures,
245      disclosures_to_omit: HashSet::default(),
246      object,
247    })
248  }
249
250  /// Removes the disclosure for the property at `path`, concealing it.
251  ///
252  /// ## Notes
253  /// - When concealing a claim more than one disclosure may be removed: the disclosure for the claim itself and the
254  ///   disclosures for any concealable sub-claim.
255  pub fn conceal(mut self, path: &str) -> Result<Self> {
256    self
257      .disclosures_to_omit
258      .extend(find_disclosure_and_sub_disclosures_for_value_at_path(
259        &self.object,
260        path,
261        &self.disclosures,
262      )?);
263    Ok(self)
264  }
265
266  /// Removes all disclosures from this SD-JWT, resulting in a token that,
267  /// when presented, will have *all* selectively-disclosable properties
268  /// omitted.
269  pub fn conceal_all(mut self) -> Self {
270    self.disclosures_to_omit.extend(0..self.disclosures.len());
271    self
272  }
273
274  /// Discloses a value that was previously concealed.
275  /// # Notes
276  /// - This method may disclose multiple values, if the given path references a disclosable value stored within another
277  ///   disclosable value. That is, [disclose](Self::disclose) will unconceal the selectively disclosable value at
278  ///   `path` together with *all* its parents that are disclosable values themselves.
279  /// - By default *all* disclosable claims are disclosed, therefore this method can only be used to *undo* any
280  ///   concealment operations previously performed by either [Self::conceal] or [Self::conceal_all].
281  pub fn disclose(mut self, path: &str) -> Result<Self> {
282    let disclosing = find_disclosure_and_parent_disclosures_for_value_at_path(&self.object, path, &self.disclosures)?;
283    for idx in disclosing {
284      self.disclosures_to_omit.remove(&idx);
285    }
286    Ok(self)
287  }
288
289  /// Returns the resulting [`SdJwt`] together with all removed disclosures.
290  pub fn finish(self) -> (SdJwt, Vec<Disclosure>) {
291    // Put everything back in its place.
292    let SdJwtPresentationBuilder {
293      mut sd_jwt,
294      disclosures,
295      disclosures_to_omit,
296      object,
297      ..
298    } = self;
299
300    let (disclosures_to_keep, omitted_disclosures) =
301      disclosures
302        .into_values()
303        .enumerate()
304        .partition_map(|(idx, disclosure)| {
305          if disclosures_to_omit.contains(&idx) {
306            Either::Right(disclosure)
307          } else {
308            Either::Left(disclosure)
309          }
310        });
311
312    let Value::Object(mut obj) = object else {
313      unreachable!();
314    };
315    let Value::Array(sd) = obj.remove(DIGESTS_KEY).unwrap_or(Value::Array(vec![])) else {
316      unreachable!()
317    };
318    sd_jwt.jwt.claims._sd = sd
319      .into_iter()
320      .map(|value| {
321        if let Value::String(s) = value {
322          s
323        } else {
324          unreachable!()
325        }
326      })
327      .collect();
328    sd_jwt.jwt.claims.properties = obj;
329    sd_jwt.disclosures = disclosures_to_keep;
330
331    (sd_jwt, omitted_disclosures)
332  }
333}
334
335fn find_disclosure_and_sub_disclosures_for_value_at_path<'a>(
336  value: &'a Value,
337  path: &str,
338  disclosures: &'a IndexMap<String, Disclosure>,
339) -> Result<Vec<usize>> {
340  let path_segments = path.trim_start_matches('/').split('/').collect_vec();
341  let (value, mut visited_disclosures) = traverse_disclosable_object(value, &path_segments, disclosures)
342    .ok_or_else(|| Error::InvalidPath("the referenced element doesn't exist or is not concealable".to_owned()))?;
343  let path_referenced_disclosure = visited_disclosures
344    .pop()
345    .ok_or_else(|| Error::InvalidPath("the referenced element doesn't exist or is not concealable".to_owned()))?;
346
347  let mut disclosures_to_omit = get_all_sub_disclosures(value, disclosures);
348  disclosures_to_omit.push(path_referenced_disclosure);
349
350  Ok(disclosures_to_omit)
351}
352
353fn find_disclosure_and_parent_disclosures_for_value_at_path<'a>(
354  value: &'a Value,
355  path: &str,
356  disclosures: &'a IndexMap<String, Disclosure>,
357) -> Result<Vec<usize>> {
358  let path_segments = path.trim_start_matches('/').split('/').collect_vec();
359  traverse_disclosable_object(value, &path_segments, disclosures)
360    .map(|(_, disclosures)| disclosures)
361    .ok_or_else(|| Error::InvalidPath("the referenced element doesn't exist or is not concealable".to_owned()))
362}
363
364fn find_disclosure(object: &JsonObject, key: &str, disclosures: &IndexMap<String, Disclosure>) -> Option<usize> {
365  // Try to find the digest for disclosable property `key` in
366  // the `_sd` field of `object`.
367  object
368    .get(DIGESTS_KEY)
369    .and_then(|value| value.as_array())
370    .iter()
371    .flat_map(|values| values.iter())
372    .flat_map(|value| value.as_str())
373    .find(|digest| {
374      disclosures
375        .get(*digest)
376        .and_then(|disclosure| disclosure.claim_name.as_deref())
377        .is_some_and(|name| name == key)
378    })
379    .and_then(|digest| disclosures.get_index_of(digest))
380}
381
382fn traverse_disclosable_object<'a>(
383  mut value: &'a Value,
384  path: &[&str],
385  disclosures: &'a IndexMap<String, Disclosure>,
386) -> Option<(&'a Value, Vec<usize>)> {
387  let mut visited_disclosures = vec![];
388  for path_segment in path {
389    let step = traverse_disclosable_object_step(value, path_segment, disclosures)?;
390    value = step.value;
391    if let Some(disclosure) = step.disclosure {
392      visited_disclosures.push(disclosure)
393    }
394  }
395
396  Some((value, visited_disclosures))
397}
398
399fn traverse_disclosable_object_step<'a>(
400  value: &'a Value,
401  path_fragment: &str,
402  disclosures: &'a IndexMap<String, Disclosure>,
403) -> Option<TraversalResult<'a>> {
404  match value {
405    // Object has an entry for the element we are searching.
406    Value::Object(object) if object.contains_key(path_fragment) => {
407      Some(TraversalResult::new_value(object.get(path_fragment).unwrap()))
408    }
409    // No entry for path fragment, searching object's disclosures.
410    Value::Object(object) => {
411      let idx = find_disclosure(object, path_fragment, disclosures)?;
412      let (_, disclosure) = disclosures.get_index(idx).unwrap();
413      Some(TraversalResult::new_from_disclosure(idx, disclosure))
414    }
415    Value::Array(array) => {
416      let arr_idx = path_fragment.parse::<usize>().ok()?;
417      let value = array.get(arr_idx)?;
418
419      // Check if the value is a disclosable value.
420      if let Some(digest) = value.get(ARRAY_DIGEST_KEY).and_then(|value| value.as_str()) {
421        disclosures
422          .get_full(digest)
423          .map(|(idx, _, disclosure)| TraversalResult::new_from_disclosure(idx, disclosure))
424      } else {
425        Some(TraversalResult::new_value(value))
426      }
427    }
428    _ => None,
429  }
430}
431
432/// The result of a step in the traversal of a disclosable value.
433#[derive(Debug)]
434struct TraversalResult<'a> {
435  /// The reached value.
436  value: &'a Value,
437  /// The index of the disclosure we had to walk through to reach `value`.
438  disclosure: Option<usize>,
439}
440
441impl<'a> TraversalResult<'a> {
442  fn new_value(value: &'a Value) -> Self {
443    Self {
444      value,
445      disclosure: None,
446    }
447  }
448
449  fn new_from_disclosure(idx: usize, disclosure: &'a Disclosure) -> Self {
450    Self {
451      value: &disclosure.claim_value,
452      disclosure: Some(idx),
453    }
454  }
455}
456
457fn get_all_sub_disclosures<'a>(value: &'a Value, disclosures: &'a IndexMap<String, Disclosure>) -> Vec<usize> {
458  let mut sub_disclosures = vec![];
459  match value {
460    Value::Object(object) => {
461      // Check object's "_sd" entry.
462      object
463        .get(DIGESTS_KEY)
464        .and_then(|sd| sd.as_array())
465        .map(|sd| sd.iter())
466        .unwrap_or_default()
467        .flat_map(|value| value.as_str())
468        .filter_map(|digest| disclosures.get_index_of(digest))
469        .for_each(|idx| sub_disclosures.push(idx));
470      // Recursively check all object's property.
471      object.values().for_each(|value| {
472        let found_sub_disclosures = get_all_sub_disclosures(value, disclosures);
473        sub_disclosures.extend(found_sub_disclosures);
474      });
475    }
476    Value::Array(arr) => {
477      for value in arr.iter().filter(|value| value.is_object()) {
478        if let Some(idx) = value
479          .get(ARRAY_DIGEST_KEY)
480          .and_then(|value| value.as_str())
481          .and_then(|digest| disclosures.get_index_of(digest))
482        {
483          sub_disclosures.push(idx);
484        } else {
485          sub_disclosures.extend(get_all_sub_disclosures(value, disclosures));
486        }
487      }
488    }
489    _ => (),
490  }
491
492  sub_disclosures
493}
494
495#[cfg(test)]
496mod test {
497  use crate::SdJwt;
498  const SD_JWT: &str = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1FyazFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZzZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZkx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSLWFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzIiwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTSjFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1cFJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZXd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2bmNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4anZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25VbGRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0MG9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.gR6rSL7urX79CNEvTQnP1MH5xthG11ucIV44SqKFZ4Pvlu_u16RfvXQd4k4CAIBZNKn2aTI18TfvFwV97gJFoA~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~";
499
500  #[test]
501  fn parse() {
502    let sd_jwt = SdJwt::parse(SD_JWT).unwrap();
503    assert_eq!(sd_jwt.disclosures.len(), 2);
504    assert!(sd_jwt.key_binding_jwt.is_none());
505  }
506
507  #[test]
508  fn round_trip_ser_des() {
509    let sd_jwt = SdJwt::parse(SD_JWT).unwrap();
510    assert_eq!(&sd_jwt.to_string(), SD_JWT);
511  }
512}