identity_credential/sd_jwt_vc/
builder.rs1#![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#[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 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 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 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 {
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 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 pub fn headers(mut self, header: JsonObject) -> Self {
167 self.header = header;
168 self
169 }
170
171 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 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 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 pub fn iss(mut self, issuer: Url) -> Self {
200 self.iss = Some(issuer);
201 self
202 }
203
204 pub fn nbf(mut self, nbf: Timestamp) -> Self {
206 self.nbf = Some(nbf.to_unix());
207 self
208 }
209
210 pub fn exp(mut self, exp: Timestamp) -> Self {
212 self.exp = Some(exp.to_unix());
213 self
214 }
215
216 pub fn iat(mut self, iat: Timestamp) -> Self {
218 self.iat = Some(iat.to_unix());
219 self
220 }
221
222 pub fn vct(mut self, vct: impl Into<StringOrUrl>) -> Self {
224 self.vct = Some(vct.into());
225 self
226 }
227
228 #[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 pub fn status(mut self, status: Status) -> Self {
237 self.status = Some(status);
238 self
239 }
240
241 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 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 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}