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_payload_rework::Hasher;
12use sd_jwt_payload_rework::JsonObject;
13use sd_jwt_payload_rework::JwsSigner;
14use sd_jwt_payload_rework::RequiredKeyBinding;
15use sd_jwt_payload_rework::SdJwtBuilder;
16use sd_jwt_payload_rework::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 header(mut self, header: JsonObject) -> Self {
162 self.header = header;
163 self
164 }
165
166 pub fn add_decoys(mut self, path: &str, number_of_decoys: usize) -> Result<Self> {
173 self.inner_builder = self.inner_builder.add_decoys(path, number_of_decoys)?;
174
175 Ok(self)
176 }
177
178 pub fn require_key_binding(mut self, key_bind: RequiredKeyBinding) -> Self {
183 self.inner_builder = self.inner_builder.require_key_binding(key_bind);
184 self
185 }
186
187 pub fn iss(mut self, issuer: Url) -> Self {
189 self.iss = Some(issuer);
190 self
191 }
192
193 pub fn nbf(mut self, nbf: Timestamp) -> Self {
195 self.nbf = Some(nbf.to_unix());
196 self
197 }
198
199 pub fn exp(mut self, exp: Timestamp) -> Self {
201 self.exp = Some(exp.to_unix());
202 self
203 }
204
205 pub fn iat(mut self, iat: Timestamp) -> Self {
207 self.iat = Some(iat.to_unix());
208 self
209 }
210
211 pub fn vct(mut self, vct: impl Into<StringOrUrl>) -> Self {
213 self.vct = Some(vct.into());
214 self
215 }
216
217 #[allow(clippy::should_implement_trait)]
219 pub fn sub(mut self, sub: impl Into<StringOrUrl>) -> Self {
220 self.sub = Some(sub.into());
221 self
222 }
223
224 pub fn status(mut self, status: Status) -> Self {
226 self.status = Some(status);
227 self
228 }
229
230 pub async fn finish<S>(self, signer: &S, alg: &str) -> Result<SdJwtVc>
232 where
233 S: JwsSigner,
234 {
235 let Self {
236 inner_builder,
237 mut header,
238 iss,
239 nbf,
240 exp,
241 iat,
242 vct,
243 sub,
244 status,
245 } = self;
246 header
248 .entry("typ")
249 .or_insert_with(|| SD_JWT_VC_TYP.to_owned().into())
250 .as_str()
251 .filter(|typ| typ.contains(SD_JWT_VC_TYP))
252 .ok_or_else(|| Error::InvalidJoseType(String::default()))?;
253
254 let builder = inner_builder.header(header);
255
256 let builder = claim_to_key_value_pair![iss, nbf, exp, iat, vct, sub, status]
258 .into_iter()
259 .filter(|(_, value)| !value.is_null())
260 .fold(builder, |builder, (key, value)| {
261 builder.insert_claim(key, value).expect("value is a JSON Value")
262 });
263
264 let sd_jwt = builder.finish(signer, alg).await?;
265 SdJwtVc::try_from(sd_jwt)
266 }
267}
268
269#[cfg(test)]
270mod tests {
271
272 use super::*;
273 use crate::credential::CredentialBuilder;
274 use crate::credential::Subject;
275 use crate::sd_jwt_vc::tests::TestSigner;
276
277 #[tokio::test]
278 async fn building_valid_vc_works() -> anyhow::Result<()> {
279 let credential = json!({
280 "name": "John Doe",
281 "birthdate": "1970-01-01"
282 });
283
284 SdJwtVcBuilder::new(credential)?
285 .vct("https://bmi.bund.example/credential/pid/1.0".parse::<Url>()?)
286 .iat(Timestamp::now_utc())
287 .iss("https://example.com/".parse()?)
288 .make_concealable("/birthdate")?
289 .finish(&TestSigner, "HS256")
290 .await?;
291
292 Ok(())
293 }
294
295 #[tokio::test]
296 async fn building_vc_with_missing_mandatory_claims_fails() -> anyhow::Result<()> {
297 let credential = json!({
298 "name": "John Doe",
299 "birthdate": "1970-01-01"
300 });
301
302 let err = SdJwtVcBuilder::new(credential)?
303 .vct("https://bmi.bund.example/credential/pid/1.0".parse::<Url>()?)
304 .iat(Timestamp::now_utc())
305 .make_concealable("/birthdate")?
307 .finish(&TestSigner, "HS256")
308 .await
309 .unwrap_err();
310 assert!(matches!(err, Error::MissingClaim("iss")));
311
312 Ok(())
313 }
314
315 #[tokio::test]
316 async fn building_vc_with_invalid_mandatory_claims_fails() -> anyhow::Result<()> {
317 let credential = json!({
318 "name": "John Doe",
319 "birthdate": "1970-01-01",
320 "vct": { "id": 1234567890 }
321 });
322
323 let err = SdJwtVcBuilder::new(credential)?
324 .iat(Timestamp::now_utc())
325 .iss("https://example.com".parse()?)
326 .make_concealable("/birthdate")?
327 .finish(&TestSigner, "HS256")
328 .await
329 .unwrap_err();
330
331 assert!(matches!(err, Error::InvalidClaimValue { name: "vct", .. }));
332
333 Ok(())
334 }
335
336 #[tokio::test]
337 async fn building_vc_with_disclosed_mandatory_claim_fails() -> anyhow::Result<()> {
338 let credential = json!({
339 "name": "John Doe",
340 "birthdate": "1970-01-01",
341 "vct": { "id": 1234567890 }
342 });
343
344 let err = SdJwtVcBuilder::new(credential)?
345 .iat(Timestamp::now_utc())
346 .iss("https://example.com".parse()?)
347 .make_concealable("/birthdate")?
348 .make_concealable("/vct")?
349 .finish(&TestSigner, "HS256")
350 .await
351 .unwrap_err();
352
353 assert!(matches!(err, Error::DisclosedClaim("vct")));
354
355 Ok(())
356 }
357
358 #[tokio::test]
359 async fn building_sd_jwt_vc_from_credential_works() -> anyhow::Result<()> {
360 let credential = CredentialBuilder::default()
361 .id(Url::parse("https://example.com/credentials/42")?)
362 .issuance_date(Timestamp::now_utc())
363 .issuer(Url::parse("https://example.com/issuers/42")?)
364 .subject(Subject::with_id(Url::parse("https://example.com/subjects/42")?))
365 .build()?;
366
367 let sd_jwt_vc = SdJwtVcBuilder::new_from_credential(credential.clone(), Sha256Hasher)?
368 .vct(Url::parse("https://example.com/types/0")?)
369 .finish(&TestSigner, "HS256")
370 .await?;
371
372 assert_eq!(sd_jwt_vc.claims().nbf.as_ref().unwrap(), &credential.issuance_date);
373 assert_eq!(&sd_jwt_vc.claims().iss, credential.issuer.url());
374 assert_eq!(
375 sd_jwt_vc.claims().sub.as_ref().unwrap().as_url(),
376 credential.credential_subject.first().unwrap().id.as_ref()
377 );
378 assert_eq!(
379 sd_jwt_vc.claims().get("jti"),
380 Some(&json!(credential.id.as_ref().unwrap()))
381 );
382 assert_eq!(sd_jwt_vc.claims().get("type"), Some(&json!("VerifiableCredential")));
383
384 Ok(())
385 }
386}