identity_credential/sd_jwt_vc/
builder.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(clippy::vec_init_then_push)]
5use std::sync::LazyLock;
6
7use identity_core::common::StringOrUrl;
8use identity_core::common::Timestamp;
9use identity_core::common::Url;
10use identity_core::convert::ToJson;
11use sd_jwt::Hasher;
12use sd_jwt::JsonObject;
13use sd_jwt::JwsSigner;
14use sd_jwt::RequiredKeyBinding;
15use sd_jwt::SdJwtBuilder;
16use sd_jwt::Sha256Hasher;
17use serde::Serialize;
18use serde_json::json;
19use serde_json::Value;
20
21use crate::credential::Credential;
22use crate::credential::CredentialJwtClaims;
23
24use super::Error;
25use super::Result;
26use super::SdJwtVc;
27use super::Status;
28use super::SD_JWT_VC_TYP;
29
30static DEFAULT_HEADER: LazyLock<JsonObject> = LazyLock::new(|| {
31  let mut object = JsonObject::default();
32  object.insert("typ".to_string(), SD_JWT_VC_TYP.into());
33  object
34});
35
36macro_rules! claim_to_key_value_pair {
37  ( $( $claim:ident ),+ ) => {
38    {
39      let mut claim_list = Vec::<(&'static str, serde_json::Value)>::new();
40      $(
41        claim_list.push((stringify!($claim), serde_json::to_value($claim).unwrap()));
42      )*
43      claim_list
44    }
45  };
46}
47
48/// A structure to ease the creation of an [`SdJwtVc`].
49#[derive(Debug)]
50pub struct SdJwtVcBuilder<H = Sha256Hasher> {
51  inner_builder: SdJwtBuilder<H>,
52  header: JsonObject,
53  iss: Option<Url>,
54  nbf: Option<i64>,
55  exp: Option<i64>,
56  iat: Option<i64>,
57  vct: Option<StringOrUrl>,
58  sub: Option<StringOrUrl>,
59  status: Option<Status>,
60}
61
62impl Default for SdJwtVcBuilder {
63  fn default() -> Self {
64    Self {
65      inner_builder: SdJwtBuilder::<Sha256Hasher>::new(json!({})).unwrap(),
66      header: DEFAULT_HEADER.clone(),
67      iss: None,
68      nbf: None,
69      exp: None,
70      iat: None,
71      vct: None,
72      sub: None,
73      status: None,
74    }
75  }
76}
77
78impl SdJwtVcBuilder {
79  /// Creates a new [`SdJwtVcBuilder`] using `object` JSON representation and default
80  /// `sha-256` hasher.
81  pub fn new<T: Serialize>(object: T) -> Result<Self> {
82    let inner_builder = SdJwtBuilder::<Sha256Hasher>::new(object)?;
83    Ok(Self {
84      header: DEFAULT_HEADER.clone(),
85      inner_builder,
86      ..Default::default()
87    })
88  }
89}
90
91impl<H: Hasher> SdJwtVcBuilder<H> {
92  /// Creates a new [`SdJwtVcBuilder`] using `object` JSON representation and a given
93  /// hasher `hasher`.
94  pub fn new_with_hasher<T: Serialize>(object: T, hasher: H) -> Result<Self> {
95    let inner_builder = SdJwtBuilder::new_with_hasher(object, hasher)?;
96    Ok(Self {
97      inner_builder,
98      header: DEFAULT_HEADER.clone(),
99      iss: None,
100      nbf: None,
101      exp: None,
102      iat: None,
103      vct: None,
104      sub: None,
105      status: None,
106    })
107  }
108
109  /// Creates a new [`SdJwtVcBuilder`] starting from a [`Credential`] that is converted to a JWT claim set.
110  pub fn new_from_credential(credential: Credential, hasher: H) -> std::result::Result<Self, crate::Error> {
111    let mut vc_jwt_claims = CredentialJwtClaims::new(&credential, None)?
112      .to_json_value()
113      .map_err(|e| crate::Error::JwtClaimsSetSerializationError(Box::new(e)))?;
114    // When converting a VC to its JWT claims representation, some VC specific claims are putted into a `vc` object
115    // property. Flatten out `vc`, keeping the other JWT claims intact.
116    {
117      let claims = vc_jwt_claims.as_object_mut().expect("serialized VC is a JSON object");
118      let Value::Object(vc_properties) = claims.remove("vc").expect("serialized VC has `vc` property") else {
119        unreachable!("`vc` property's value is a JSON object");
120      };
121      for (key, value) in vc_properties {
122        claims.insert(key, value);
123      }
124    }
125    Ok(Self::new_with_hasher(vc_jwt_claims, hasher)?)
126  }
127
128  /// Substitutes a value with the digest of its disclosure.
129  ///
130  /// ## Notes
131  /// - `path` indicates the pointer to the value that will be concealed using the syntax of [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901).
132  ///
133  /// ## Example
134  /// ```rust
135  /// use serde_json::json;  
136  /// use identity_credential::sd_jwt_vc::SdJwtVcBuilder;
137  ///
138  /// let obj = json!({
139  ///   "id": "did:value",
140  ///   "claim1": {
141  ///      "abc": true
142  ///   },
143  ///   "claim2": ["val_1", "val_2"]
144  /// });
145  /// let builder = SdJwtVcBuilder::new(obj)
146  ///   .unwrap()
147  ///   .make_concealable("/id").unwrap() //conceals "id": "did:value"
148  ///   .make_concealable("/claim1/abc").unwrap() //"abc": true
149  ///   .make_concealable("/claim2/0").unwrap(); //conceals "val_1"
150  /// ```
151  pub fn make_concealable(mut self, path: &str) -> Result<Self> {
152    self.inner_builder = self.inner_builder.make_concealable(path)?;
153    Ok(self)
154  }
155
156  /// Sets the JWT headers.
157  /// ## Notes
158  /// - if [`SdJwtVcBuilder::headers`] is not called, the default header is used:
159  /// ```json
160  /// {
161  ///   "typ": "sd-jwt",
162  ///   "alg": "<algorithm used in SdJwtBuilder::finish>"
163  /// }
164  /// ```
165  /// - `alg` is always replaced with the value passed to [`SdJwtVcBuilder::finish`].
166  pub fn headers(mut self, header: JsonObject) -> Self {
167    self.header = header;
168    self
169  }
170
171  /// Sets a single JWT header.
172  pub fn header(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
173    self.header.insert(key.into(), value.into());
174    self
175  }
176
177  /// Adds a decoy digest to the specified path.
178  ///
179  /// `path` indicates the pointer to the value that will be concealed using the syntax of
180  /// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901).
181  ///
182  /// Use `path` = "" to add decoys to the top level.
183  pub fn add_decoys(mut self, path: &str, number_of_decoys: usize) -> Result<Self> {
184    self.inner_builder = self.inner_builder.add_decoys(path, number_of_decoys)?;
185
186    Ok(self)
187  }
188
189  /// Require a proof of possession of a given key from the holder.
190  ///
191  /// This operation adds a JWT confirmation (`cnf`) claim as specified in
192  /// [RFC8300](https://www.rfc-editor.org/rfc/rfc7800.html#section-3).
193  pub fn require_key_binding(mut self, key_bind: RequiredKeyBinding) -> Self {
194    self.inner_builder = self.inner_builder.require_key_binding(key_bind);
195    self
196  }
197
198  /// Inserts an `iss` claim. See [`super::SdJwtVcClaims::iss`].
199  pub fn iss(mut self, issuer: Url) -> Self {
200    self.iss = Some(issuer);
201    self
202  }
203
204  /// Inserts a `nbf` claim. See [`super::SdJwtVcClaims::nbf`].
205  pub fn nbf(mut self, nbf: Timestamp) -> Self {
206    self.nbf = Some(nbf.to_unix());
207    self
208  }
209
210  /// Inserts a `exp` claim. See [`super::SdJwtVcClaims::exp`].
211  pub fn exp(mut self, exp: Timestamp) -> Self {
212    self.exp = Some(exp.to_unix());
213    self
214  }
215
216  /// Inserts a `iat` claim. See [`super::SdJwtVcClaims::iat`].
217  pub fn iat(mut self, iat: Timestamp) -> Self {
218    self.iat = Some(iat.to_unix());
219    self
220  }
221
222  /// Inserts a `vct` claim. See [`super::SdJwtVcClaims::vct`].
223  pub fn vct(mut self, vct: impl Into<StringOrUrl>) -> Self {
224    self.vct = Some(vct.into());
225    self
226  }
227
228  /// Inserts a `sub` claim. See [`super::SdJwtVcClaims::sub`].
229  #[allow(clippy::should_implement_trait)]
230  pub fn sub(mut self, sub: impl Into<StringOrUrl>) -> Self {
231    self.sub = Some(sub.into());
232    self
233  }
234
235  /// Inserts a `status` claim. See [`super::SdJwtVcClaims::status`].
236  pub fn status(mut self, status: Status) -> Self {
237    self.status = Some(status);
238    self
239  }
240
241  /// Creates an [`SdJwtVc`] with the provided data.
242  pub async fn finish<S>(self, signer: &S, alg: &str) -> Result<SdJwtVc>
243  where
244    S: JwsSigner,
245  {
246    let Self {
247      inner_builder,
248      mut header,
249      iss,
250      nbf,
251      exp,
252      iat,
253      vct,
254      sub,
255      status,
256    } = self;
257    // Check header.
258    header
259      .entry("typ")
260      .or_insert_with(|| SD_JWT_VC_TYP.to_owned().into())
261      .as_str()
262      .filter(|typ| typ.contains(SD_JWT_VC_TYP))
263      .ok_or_else(|| Error::InvalidJoseType(String::default()))?;
264
265    let builder = inner_builder.headers(header);
266
267    // Insert SD-JWT VC claims into object.
268    let builder = claim_to_key_value_pair![iss, nbf, exp, iat, vct, sub, status]
269      .into_iter()
270      .filter(|(_, value)| !value.is_null())
271      .fold(builder, |builder, (key, value)| {
272        builder.insert_claim(key, value).expect("value is a JSON Value")
273      });
274
275    let sd_jwt = builder.finish(signer, alg).await?;
276    SdJwtVc::try_from(sd_jwt)
277  }
278}
279
280#[cfg(test)]
281mod tests {
282
283  use super::*;
284  use crate::credential::CredentialBuilder;
285  use crate::credential::Subject;
286  use crate::sd_jwt_vc::tests::TestSigner;
287
288  #[tokio::test]
289  async fn building_valid_vc_works() -> anyhow::Result<()> {
290    let credential = json!({
291      "name": "John Doe",
292      "birthdate": "1970-01-01"
293    });
294
295    SdJwtVcBuilder::new(credential)?
296      .vct("https://bmi.bund.example/credential/pid/1.0".parse::<Url>()?)
297      .iat(Timestamp::now_utc())
298      .iss("https://example.com/".parse()?)
299      .make_concealable("/birthdate")?
300      .finish(&TestSigner, "HS256")
301      .await?;
302
303    Ok(())
304  }
305
306  #[tokio::test]
307  async fn building_vc_with_missing_mandatory_claims_fails() -> anyhow::Result<()> {
308    let credential = json!({
309      "name": "John Doe",
310      "birthdate": "1970-01-01"
311    });
312
313    let err = SdJwtVcBuilder::new(credential)?
314      .iat(Timestamp::now_utc())
315      .make_concealable("/birthdate")?
316      .finish(&TestSigner, "HS256")
317      .await
318      .unwrap_err();
319    assert!(matches!(err, Error::MissingClaim("vct")));
320
321    Ok(())
322  }
323
324  #[tokio::test]
325  async fn building_vc_with_invalid_mandatory_claims_fails() -> anyhow::Result<()> {
326    let credential = json!({
327      "name": "John Doe",
328      "birthdate": "1970-01-01",
329      "vct": { "id": 1234567890 }
330    });
331
332    let err = SdJwtVcBuilder::new(credential)?
333      .iat(Timestamp::now_utc())
334      .iss("https://example.com".parse()?)
335      .make_concealable("/birthdate")?
336      .finish(&TestSigner, "HS256")
337      .await
338      .unwrap_err();
339
340    assert!(matches!(err, Error::InvalidClaimValue { name: "vct", .. }));
341
342    Ok(())
343  }
344
345  #[tokio::test]
346  async fn building_vc_with_disclosed_mandatory_claim_fails() -> anyhow::Result<()> {
347    let credential = json!({
348      "name": "John Doe",
349      "birthdate": "1970-01-01",
350      "vct": { "id": 1234567890 }
351    });
352
353    let err = SdJwtVcBuilder::new(credential)?
354      .iat(Timestamp::now_utc())
355      .iss("https://example.com".parse()?)
356      .make_concealable("/birthdate")?
357      .make_concealable("/vct")?
358      .finish(&TestSigner, "HS256")
359      .await
360      .unwrap_err();
361
362    assert!(matches!(err, Error::DisclosedClaim("vct")));
363
364    Ok(())
365  }
366
367  #[tokio::test]
368  async fn building_sd_jwt_vc_from_credential_works() -> anyhow::Result<()> {
369    let credential = CredentialBuilder::default()
370      .id(Url::parse("https://example.com/credentials/42")?)
371      .issuance_date(Timestamp::now_utc())
372      .issuer(Url::parse("https://example.com/issuers/42")?)
373      .subject(Subject::with_id(Url::parse("https://example.com/subjects/42")?))
374      .build()?;
375
376    let sd_jwt_vc = SdJwtVcBuilder::new_from_credential(credential.clone(), Sha256Hasher)?
377      .vct(Url::parse("https://example.com/types/0")?)
378      .finish(&TestSigner, "HS256")
379      .await?;
380
381    assert_eq!(sd_jwt_vc.claims().nbf.as_ref(), Some(&credential.issuance_date));
382    assert_eq!(sd_jwt_vc.claims().iss.as_ref(), Some(credential.issuer.url()));
383    assert_eq!(
384      sd_jwt_vc.claims().sub.as_ref().unwrap().as_url(),
385      credential.credential_subject.first().unwrap().id.as_ref()
386    );
387    assert_eq!(
388      sd_jwt_vc.claims().get("jti"),
389      Some(&json!(credential.id.as_ref().unwrap()))
390    );
391    assert_eq!(sd_jwt_vc.claims().get("type"), Some(&json!("VerifiableCredential")));
392
393    Ok(())
394  }
395}