sd_jwt_payload/
builder.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::borrow::Cow;
5
6use anyhow::Context as _;
7use itertools::Itertools;
8use serde::Serialize;
9use serde_json::Value;
10
11use crate::jwt::Jwt;
12use crate::Disclosure;
13use crate::Error;
14use crate::Hasher;
15use crate::JsonObject;
16use crate::JwsSigner;
17use crate::RequiredKeyBinding;
18use crate::Result;
19use crate::SdJwt;
20use crate::SdJwtClaims;
21use crate::SdObjectEncoder;
22#[cfg(feature = "sha")]
23use crate::Sha256Hasher;
24use crate::DEFAULT_SALT_SIZE;
25use crate::HEADER_TYP;
26
27/// Builder structure to create an issuable SD-JWT.
28#[derive(Debug)]
29pub struct SdJwtBuilder<H> {
30  encoder: SdObjectEncoder<H>,
31  header: JsonObject,
32  disclosures: Vec<Disclosure>,
33  key_bind: Option<RequiredKeyBinding>,
34}
35
36#[cfg(feature = "sha")]
37impl SdJwtBuilder<Sha256Hasher> {
38  /// Creates a new [`SdJwtBuilder`] with `sha-256` hash function.
39  ///
40  /// ## Error
41  /// Returns [`Error::DataTypeMismatch`] if `object` is not a valid JSON object.
42  pub fn new<T: Serialize>(object: T) -> Result<Self> {
43    Self::new_with_hasher(object, Sha256Hasher::new())
44  }
45}
46
47impl<H: Hasher> SdJwtBuilder<H> {
48  /// Creates a new [`SdJwtBuilder`] with custom hash function to create digests.
49  pub fn new_with_hasher<T: Serialize>(object: T, hasher: H) -> Result<Self> {
50    Self::new_with_hasher_and_salt_size(object, hasher, DEFAULT_SALT_SIZE)
51  }
52
53  /// Creates a new [`SdJwtBuilder`] with custom hash function to create digests, and custom salt size.
54  pub fn new_with_hasher_and_salt_size<T: Serialize>(object: T, hasher: H, salt_size: usize) -> Result<Self> {
55    let object = serde_json::to_value(object).map_err(|e| Error::Unspecified(e.to_string()))?;
56    let encoder = SdObjectEncoder::with_custom_hasher_and_salt_size(object, hasher, salt_size)?;
57    Ok(Self {
58      encoder,
59      disclosures: vec![],
60      key_bind: None,
61      header: JsonObject::default(),
62    })
63  }
64
65  /// Substitutes a value with the digest of its disclosure.
66  ///
67  /// ## Notes
68  /// - `path` indicates the pointer to the value that will be concealed using the syntax of [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901).
69  ///
70  ///
71  /// ## Example
72  ///  ```rust
73  ///  use sd_jwt_payload::SdJwtBuilder;
74  ///  use sd_jwt_payload::json;
75  ///
76  ///  let obj = json!({
77  ///   "id": "did:value",
78  ///   "claim1": {
79  ///      "abc": true
80  ///   },
81  ///   "claim2": ["val_1", "val_2"]
82  /// });
83  /// let builder = SdJwtBuilder::new(obj)
84  ///   .unwrap()
85  ///   .make_concealable("/id").unwrap() //conceals "id": "did:value"
86  ///   .make_concealable("/claim1/abc").unwrap() //"abc": true
87  ///   .make_concealable("/claim2/0").unwrap(); //conceals "val_1"
88  /// ```
89  /// 
90  /// ## Error
91  /// * [`Error::InvalidPath`] if pointer is invalid.
92  /// * [`Error::DataTypeMismatch`] if existing SD format is invalid.
93  pub fn make_concealable(mut self, path: &str) -> Result<Self> {
94    let disclosure = self.encoder.conceal(path)?;
95    self.disclosures.push(disclosure);
96
97    Ok(self)
98  }
99
100  /// Sets the JWT header.
101  /// ## Notes
102  /// - if [`SdJwtBuilder::header`] is not called, the default header is used: ```json { "typ": "sd-jwt", "alg":
103  ///   "<algorithm used in SdJwtBuilder::finish>" } ```
104  /// - `alg` is always replaced with the value passed to [`SdJwtBuilder::finish`].
105  pub fn header(mut self, header: JsonObject) -> Self {
106    self.header = header;
107    self
108  }
109
110  /// Adds a new claim to the underlying object.
111  pub fn insert_claim<'a, K, V>(mut self, key: K, value: V) -> Result<Self>
112  where
113    K: Into<Cow<'a, str>>,
114    V: Serialize,
115  {
116    let key = key.into().into_owned();
117    let value = serde_json::to_value(value).map_err(|e| Error::DeserializationError(e.to_string()))?;
118    self
119      .encoder
120      .object
121      .as_object_mut()
122      .expect("encoder::object is a JSON Object")
123      .insert(key, value);
124
125    Ok(self)
126  }
127
128  /// Adds a decoy digest to the specified path.
129  ///
130  /// `path` indicates the pointer to the value that will be concealed using the syntax of
131  /// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901).
132  ///
133  /// Use `path` = "" to add decoys to the top level.
134  pub fn add_decoys(mut self, path: &str, number_of_decoys: usize) -> Result<Self> {
135    self.encoder.add_decoys(path, number_of_decoys)?;
136
137    Ok(self)
138  }
139
140  /// Require a proof of possession of a given key from the holder.
141  ///
142  /// This operation adds a JWT confirmation (`cnf`) claim as specified in
143  /// [RFC8300](https://www.rfc-editor.org/rfc/rfc7800.html#section-3).
144  pub fn require_key_binding(mut self, key_bind: RequiredKeyBinding) -> Self {
145    self.key_bind = Some(key_bind);
146    self
147  }
148
149  /// Creates an SD-JWT with the provided data.
150  pub async fn finish<S>(self, signer: &S, alg: &str) -> Result<SdJwt>
151  where
152    S: JwsSigner,
153  {
154    let SdJwtBuilder {
155      mut encoder,
156      disclosures,
157      key_bind,
158      mut header,
159    } = self;
160    encoder.add_sd_alg_property();
161    let mut object = encoder.object;
162    // Add key binding requirement as `cnf`.
163    if let Some(key_bind) = key_bind {
164      let key_bind = serde_json::to_value(key_bind).map_err(|e| Error::DeserializationError(e.to_string()))?;
165      object
166        .as_object_mut()
167        .expect("encoder::object is a JSON Object")
168        .insert("cnf".to_string(), key_bind);
169    }
170
171    // Check mandatory header properties or insert them.
172    if let Some(Value::String(typ)) = header.get("typ") {
173      if !typ.split('+').contains(&HEADER_TYP) {
174        return Err(Error::DataTypeMismatch(
175          "invalid header: \"typ\" must contain type \"sd-jwt\"".to_string(),
176        ));
177      }
178    } else {
179      header.insert("typ".to_string(), Value::String(HEADER_TYP.to_string()));
180    }
181    header.insert("alg".to_string(), Value::String(alg.to_string()));
182
183    let jws = signer
184      .sign(&header, object.as_object().expect("encoder::object is a JSON Object"))
185      .await
186      .map_err(|e| anyhow::anyhow!("jws failed: {e}"))
187      .and_then(|jws_bytes| String::from_utf8(jws_bytes).context("invalid JWS"))
188      .map_err(|e| Error::JwsSignerFailure(e.to_string()))?;
189
190    let claims = serde_json::from_value::<SdJwtClaims>(object)
191      .map_err(|e| Error::DeserializationError(format!("invalid SD-JWT claims: {e}")))?;
192    let jwt = Jwt { header, claims, jws };
193
194    Ok(SdJwt::new(jwt, disclosures, None))
195  }
196}
197
198#[cfg(test)]
199mod test {
200  use serde_json::json;
201
202  use super::*;
203
204  mod marking_properties_as_concealable {
205    use super::*;
206
207    mod that_exist {
208      use super::*;
209
210      mod on_top_level {
211        use super::*;
212
213        #[test]
214        fn can_be_done_for_object_values() {
215          let result = SdJwtBuilder::new(json!({ "address": {} }))
216            .unwrap()
217            .make_concealable("/address");
218
219          assert!(result.is_ok());
220        }
221
222        #[test]
223        fn can_be_done_for_array_elements() {
224          let result = SdJwtBuilder::new(json!({ "nationalities": ["US", "DE"] }))
225            .unwrap()
226            .make_concealable("/nationalities");
227
228          assert!(result.is_ok());
229        }
230      }
231
232      mod as_subproperties {
233        use super::*;
234
235        #[test]
236        fn can_be_done_for_object_values() {
237          let result = SdJwtBuilder::new(json!({ "address": { "country": "US" } }))
238            .unwrap()
239            .make_concealable("/address/country");
240
241          assert!(result.is_ok());
242        }
243
244        #[test]
245        fn can_be_done_for_array_elements() {
246          let result = SdJwtBuilder::new(json!({
247            "address": { "contact_person": [ "Jane Dow", "John Doe" ] }
248          }))
249          .unwrap()
250          .make_concealable("/address/contact_person/0");
251
252          assert!(result.is_ok());
253        }
254      }
255    }
256
257    mod that_do_not_exist {
258      use super::*;
259      mod on_top_level {
260        use super::*;
261
262        #[test]
263        fn returns_an_error_for_nonexistant_object_paths() {
264          let result = SdJwtBuilder::new(json!({})).unwrap().make_concealable("/email");
265
266          assert_eq!(result.unwrap_err(), Error::InvalidPath("/email".to_string()),);
267        }
268
269        #[test]
270        fn returns_an_error_for_nonexistant_array_paths() {
271          let result = SdJwtBuilder::new(json!({}))
272            .unwrap()
273            .make_concealable("/nationalities/0");
274
275          assert_eq!(result.unwrap_err(), Error::InvalidPath("/nationalities/0".to_string()),);
276        }
277
278        #[test]
279        fn returns_an_error_for_nonexistant_array_entries() {
280          let result = SdJwtBuilder::new(json!({
281            "nationalities": ["US", "DE"]
282          }))
283          .unwrap()
284          .make_concealable("/nationalities/2");
285
286          assert_eq!(result.unwrap_err(), Error::InvalidPath("/nationalities/2".to_string()),);
287        }
288      }
289
290      mod as_subproperties {
291        use super::*;
292
293        #[test]
294        fn returns_an_error_for_nonexistant_object_paths() {
295          let result = SdJwtBuilder::new(json!({
296            "address": {}
297          }))
298          .unwrap()
299          .make_concealable("/address/region");
300
301          assert_eq!(result.unwrap_err(), Error::InvalidPath("/address/region".to_string()),);
302        }
303
304        #[test]
305        fn returns_an_error_for_nonexistant_array_paths() {
306          let result = SdJwtBuilder::new(json!({
307            "address": {}
308          }))
309          .unwrap()
310          .make_concealable("/address/contact_person/2");
311
312          assert_eq!(
313            result.unwrap_err(),
314            Error::InvalidPath("/address/contact_person/2".to_string()),
315          );
316        }
317
318        #[test]
319        fn returns_an_error_for_nonexistant_array_entries() {
320          let result = SdJwtBuilder::new(json!({
321            "address": { "contact_person": [ "Jane Dow", "John Doe" ] }
322          }))
323          .unwrap()
324          .make_concealable("/address/contact_person/2");
325
326          assert_eq!(
327            result.unwrap_err(),
328            Error::InvalidPath("/address/contact_person/2".to_string()),
329          );
330        }
331      }
332    }
333  }
334
335  mod adding_decoys {
336    use super::*;
337
338    mod on_top_level {
339      use super::*;
340
341      #[test]
342      fn can_add_zero_object_value_decoys_for_a_path() {
343        let result = SdJwtBuilder::new(json!({})).unwrap().add_decoys("", 0);
344
345        assert!(result.is_ok());
346      }
347
348      #[test]
349      fn can_add_object_value_decoys_for_a_path() {
350        let result = SdJwtBuilder::new(json!({})).unwrap().add_decoys("", 2);
351
352        assert!(result.is_ok());
353      }
354    }
355
356    mod for_subproperties {
357      use super::*;
358
359      #[test]
360      fn can_add_zero_object_value_decoys_for_a_path() {
361        let result = SdJwtBuilder::new(json!({ "address": {} }))
362          .unwrap()
363          .add_decoys("/address", 0);
364
365        assert!(result.is_ok());
366      }
367
368      #[test]
369      fn can_add_object_value_decoys_for_a_path() {
370        let result = SdJwtBuilder::new(json!({ "address": {} }))
371          .unwrap()
372          .add_decoys("/address", 2);
373
374        assert!(result.is_ok());
375      }
376
377      #[test]
378      fn can_add_zero_array_element_decoys_for_a_path() {
379        let result = SdJwtBuilder::new(json!({ "nationalities": ["US", "DE"] }))
380          .unwrap()
381          .add_decoys("/nationalities", 0);
382
383        assert!(result.is_ok());
384      }
385
386      #[test]
387      fn can_add_array_element_decoys_for_a_path() {
388        let result = SdJwtBuilder::new(json!({ "nationalities": ["US", "DE"] }))
389          .unwrap()
390          .add_decoys("/nationalities", 2);
391
392        assert!(result.is_ok());
393      }
394    }
395  }
396}