identity_credential/sd_jwt_vc/
token.rs1use std::fmt::Display;
5use std::ops::Deref;
6use std::str::FromStr;
7
8use super::claims::SdJwtVcClaims;
9use super::metadata::ClaimMetadata;
10use super::metadata::IssuerMetadata;
11use super::metadata::Jwks;
12use super::metadata::TypeMetadata;
13use super::metadata::WELL_KNOWN_VC_ISSUER;
14use super::resolver::Error as ResolverErr;
15use super::Error;
16use super::Resolver;
17use super::Result;
18use super::SdJwtVcPresentationBuilder;
19use crate::validator::JwtCredentialValidator as JwsUtils;
20use crate::validator::KeyBindingJwtValidationOptions;
21use anyhow::anyhow;
22use identity_core::common::Timestamp;
23use identity_core::common::Url;
24use identity_core::convert::ToJson as _;
25use identity_verification::jwk::Jwk;
26use identity_verification::jwk::JwkSet;
27use identity_verification::jws::JwsVerifier;
28use sd_jwt::Hasher;
29use sd_jwt::JsonObject;
30use sd_jwt::KeyBindingJwt;
31use sd_jwt::RequiredKeyBinding;
32use sd_jwt::SdJwt;
33use sd_jwt::SHA_ALG_NAME;
34use serde_json::Value;
35
36pub const SD_JWT_VC_TYP: &str = "dc+sd-jwt";
38pub const SD_JWT_VC_TYP_ALTERNATIVE: &str = "vc+sd-jwt";
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct SdJwtVc {
46 pub(crate) sd_jwt: SdJwt,
47 pub(crate) parsed_claims: SdJwtVcClaims,
48}
49
50impl Deref for SdJwtVc {
51 type Target = SdJwt;
52 fn deref(&self) -> &Self::Target {
53 &self.sd_jwt
54 }
55}
56
57impl SdJwtVc {
58 pub(crate) fn new(sd_jwt: SdJwt, claims: SdJwtVcClaims) -> Self {
59 Self {
60 sd_jwt,
61 parsed_claims: claims,
62 }
63 }
64
65 pub fn parse(s: &str) -> Result<Self> {
67 s.parse()
68 }
69
70 pub fn claims(&self) -> &SdJwtVcClaims {
72 &self.parsed_claims
73 }
74
75 pub fn attach_key_binding_jwt(&mut self, kb_jwt: KeyBindingJwt) {
77 self.sd_jwt.attach_key_binding_jwt(kb_jwt);
78 }
79
80 pub fn into_presentation(self, hasher: &dyn Hasher) -> Result<SdJwtVcPresentationBuilder> {
85 SdJwtVcPresentationBuilder::new(self, hasher)
86 }
87
88 pub fn into_disclosed_object(self, hasher: &dyn Hasher) -> Result<JsonObject> {
91 SdJwt::from(self).into_disclosed_object(hasher).map_err(Error::SdJwt)
92 }
93
94 pub async fn issuer_metadata<R>(&self, resolver: &R) -> Result<Option<IssuerMetadata>>
100 where
101 R: Resolver<Url, Vec<u8>>,
102 {
103 let Some(iss) = self.claims().iss.as_ref() else {
104 return Ok(None);
105 };
106 let metadata_url = {
107 let origin = iss.origin().ascii_serialization();
108 let path = iss.path();
109 format!("{origin}{WELL_KNOWN_VC_ISSUER}{path}").parse().unwrap()
110 };
111 match resolver.resolve(&metadata_url).await {
112 Err(ResolverErr::NotFound(_)) => Ok(None),
113 Err(e) => Err(Error::Resolution {
114 input: metadata_url.to_string(),
115 source: e,
116 }),
117 Ok(json_res) => serde_json::from_slice(&json_res)
118 .map_err(|e| Error::InvalidIssuerMetadata(e.into()))
119 .map(Some),
120 }
121 }
122
123 pub async fn type_metadata<R>(&self, resolver: &R) -> Result<(TypeMetadata, Vec<u8>)>
128 where
129 R: Resolver<String, Vec<u8>>,
130 {
131 let vct = self.claims().vct.clone();
132 let raw = resolver
133 .resolve(&vct)
134 .await
135 .map_err(|e| Error::Resolution { input: vct, source: e })?;
136 let metadata = serde_json::from_slice(&raw).map_err(|e| Error::InvalidTypeMetadata(e.into()))?;
137
138 Ok((metadata, raw))
139 }
140
141 pub async fn issuer_jwk<R>(&self, resolver: &R) -> Result<Jwk>
146 where
147 R: Resolver<Url, Vec<u8>>,
148 {
149 let kid = self
150 .headers()
151 .get("kid")
152 .and_then(|value| value.as_str())
153 .ok_or_else(|| Error::Verification(anyhow!("missing header claim `kid`")))?;
154
155 if let jwk @ Ok(_) = self.issuer_jwk_from_iss_metadata(resolver, kid).await {
157 jwk
158 } else {
159 let jwk_uri = kid.parse::<Url>().map_err(|_| {
161 Error::Verification(anyhow!(
162 "JWK's kid \"{kid}\" could not be found in JKW set and cannot be resolved"
163 ))
164 })?;
165 resolver
166 .resolve(&jwk_uri)
167 .await
168 .map_err(|e| Error::Resolution {
169 input: jwk_uri.to_string(),
170 source: e,
171 })
172 .and_then(|bytes| {
173 serde_json::from_slice(&bytes).map_err(|e| Error::Verification(anyhow!("invalid JWK: {}", e)))
174 })
175 }
176 }
177
178 async fn issuer_jwk_from_iss_metadata<R>(&self, resolver: &R, kid: &str) -> Result<Jwk>
179 where
180 R: Resolver<Url, Vec<u8>>,
181 {
182 let metadata = self
183 .issuer_metadata(resolver)
184 .await?
185 .ok_or_else(|| Error::Verification(anyhow!("missing issuer metadata")))?;
186 metadata.validate(self)?;
187
188 let jwks = match metadata.jwks {
189 Jwks::Object(jwks) => jwks,
190 Jwks::Uri(jwks_uri) => resolver
191 .resolve(&jwks_uri)
192 .await
193 .map_err(|e| Error::Resolution {
194 input: jwks_uri.into_string(),
195 source: e,
196 })
197 .and_then(|bytes| serde_json::from_slice::<JwkSet>(&bytes).map_err(|e| Error::Verification(e.into())))?,
198 };
199 jwks
200 .iter()
201 .find(|jwk| jwk.kid() == Some(kid))
202 .cloned()
203 .ok_or_else(|| Error::Verification(anyhow!("missing key \"{kid}\" in issuer JWK set")))
204 }
205
206 pub fn verify_signature<V>(&self, jws_verifier: &V, jwk: &Jwk) -> Result<()>
208 where
209 V: JwsVerifier,
210 {
211 let sd_jwt_str = self.sd_jwt.to_string();
212 let jws_input = {
213 let jwt_str = sd_jwt_str
214 .split_once('~')
215 .expect("SD-JWT must contain a '~' separator")
216 .0;
217 JwsUtils::<V>::decode(jwt_str).map_err(|e| Error::Verification(e.into()))?
218 };
219
220 JwsUtils::<V>::verify_signature_raw(jws_input, jwk, jws_verifier)
221 .map_err(|e| Error::Verification(e.into()))
222 .and(Ok(()))
223 }
224
225 pub fn validate_claims_disclosability(&self, claims_metadata: &[ClaimMetadata]) -> Result<()> {
230 let claims = Value::Object(self.parsed_claims.sd_jwt_claims.deref().clone());
231 claims_metadata
232 .iter()
233 .try_fold((), |_, meta| meta.check_value_disclosability(&claims))
234 }
235
236 pub async fn validate<R, V>(&self, resolver: &R, jws_verifier: &V, hasher: &dyn Hasher) -> Result<()>
243 where
244 R: Resolver<Url, Vec<u8>>,
245 R: Resolver<String, Vec<u8>>,
246 R: Resolver<Url, Value>,
247 V: JwsVerifier,
248 {
249 let jwk = self.issuer_jwk(resolver).await?;
252 self.verify_signature(jws_verifier, &jwk)?;
253
254 let fully_disclosed_token = self.clone().into_disclosed_object(hasher).map(Value::Object)?;
257 let (type_metadata, _) = self.type_metadata(resolver).await?;
258 type_metadata
259 .validate_credential_with_resolver(&fully_disclosed_token, resolver)
260 .await?;
261
262 self.validate_claims_disclosability(type_metadata.claim_metadata())?;
264
265 Ok(())
266 }
267
268 pub fn verify_key_binding<V: JwsVerifier>(&self, jws_verifier: &V, jwk: &Jwk) -> Result<()> {
270 let Some(kb_jwt) = self.key_binding_jwt() else {
271 return Ok(());
272 };
273 let kb_jwt_str = kb_jwt.to_string();
274 let jws_input = JwsUtils::<V>::decode(&kb_jwt_str).map_err(|e| Error::Verification(e.into()))?;
275
276 JwsUtils::<V>::verify_signature_raw(jws_input, jwk, jws_verifier)
277 .map_err(|e| Error::Verification(e.into()))
278 .and(Ok(()))
279 }
280
281 pub fn validate_key_binding<V: JwsVerifier>(
286 &self,
287 jws_verifier: &V,
288 jwk: &Jwk,
289 hasher: &dyn Hasher,
290 options: &KeyBindingJwtValidationOptions,
291 ) -> Result<()> {
292 self.verify_key_binding(jws_verifier, jwk)?;
293
294 if let Some(requirement) = self.required_key_bind() {
295 if self.key_binding_jwt().is_none() {
296 return Err(Error::Validation(anyhow!(
297 "a key binding was required but none was provided"
298 )));
299 }
300 match requirement {
301 RequiredKeyBinding::Jwk(json_jwk) => {
302 if jwk.to_json_value().unwrap().as_object().unwrap() != json_jwk {
303 return Err(Error::Validation(anyhow!(
304 "key used for signing KB-JWT does not match the key required in this SD-JWT"
305 )));
306 }
307 }
308 RequiredKeyBinding::Kid(kid) | RequiredKeyBinding::Jwu { kid, .. } => jwk
309 .kid()
310 .filter(|id| id == kid)
311 .ok_or_else(|| {
312 Error::Validation(anyhow::anyhow!(
313 "the provided JWK doesn't have required `kid` \"{kid}\""
314 ))
315 })
316 .map(|_| ())?,
317 _ => (),
318 }
319 }
320
321 let Some(kb_jwt) = self.key_binding_jwt() else {
322 return Ok(());
323 };
324 let KeyBindingJwtValidationOptions {
325 nonce,
326 aud,
327 earliest_issuance_date,
328 latest_issuance_date,
329 ..
330 } = options;
331
332 let issuance_date =
333 Timestamp::from_unix(kb_jwt.claims().iat).map_err(|_| Error::Validation(anyhow!("invalid `iat` value")))?;
334
335 if let Some(earliest_issuance_date) = earliest_issuance_date {
336 if issuance_date < *earliest_issuance_date {
337 return Err(Error::Validation(anyhow!(
338 "this KB-JWT has been created earlier than `earliest_issuance_date`"
339 )));
340 }
341 }
342
343 if let Some(latest_issuance_date) = latest_issuance_date {
344 if issuance_date > *latest_issuance_date {
345 return Err(Error::Validation(anyhow!(
346 "this KB-JWT has been created later than `latest_issuance_date`"
347 )));
348 }
349 } else if issuance_date > Timestamp::now_utc() {
350 return Err(Error::Validation(anyhow!("this KB-JWT has been created in the future")));
351 }
352
353 if let Some(nonce) = nonce {
354 if nonce != &kb_jwt.claims().nonce {
355 return Err(Error::Validation(anyhow!("invalid KB-JWT's nonce: expected {nonce}")));
356 }
357 }
358
359 if let Some(aud) = aud {
360 if aud != &kb_jwt.claims().aud {
361 return Err(Error::Validation(anyhow!("invalid KB-JWT's `aud`: expected \"{aud}\"")));
362 }
363 }
364
365 if self.claims()._sd_alg.as_deref().unwrap_or(SHA_ALG_NAME) != hasher.alg_name() {
367 return Err(Error::Validation(anyhow!("invalid hasher")));
368 }
369 let encoded_sd_jwt = self.to_string();
370 let digest = {
371 let last_tilde_idx = encoded_sd_jwt.rfind('~').expect("SD-JWT has a '~'");
372 let sd_jwt_no_kb = &encoded_sd_jwt[..=last_tilde_idx];
373
374 hasher.encoded_digest(sd_jwt_no_kb)
375 };
376 if kb_jwt.claims().sd_hash != digest {
377 return Err(Error::Validation(anyhow!("invalid KB-JWT's `sd_hash`")));
378 }
379
380 Ok(())
381 }
382}
383
384impl TryFrom<SdJwt> for SdJwtVc {
385 type Error = Error;
386 fn try_from(mut sd_jwt: SdJwt) -> std::result::Result<Self, Self::Error> {
387 let claims = {
389 let claims = std::mem::take(sd_jwt.claims_mut());
390 SdJwtVcClaims::try_from_sd_jwt_claims(claims, sd_jwt.disclosures())?
391 };
392
393 let typ = sd_jwt
395 .headers()
396 .get("typ")
397 .and_then(Value::as_str)
398 .ok_or_else(|| Error::InvalidJoseType("null".to_string()))?;
399 if !typ.contains(SD_JWT_VC_TYP) && !typ.contains(SD_JWT_VC_TYP_ALTERNATIVE) {
400 return Err(Error::InvalidJoseType(typ.to_string()));
401 }
402
403 Ok(Self {
404 sd_jwt,
405 parsed_claims: claims,
406 })
407 }
408}
409
410impl FromStr for SdJwtVc {
411 type Err = Error;
412 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
413 s.parse::<SdJwt>().map_err(Error::SdJwt).and_then(TryInto::try_into)
414 }
415}
416
417impl Display for SdJwtVc {
418 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419 write!(f, "{}", self.sd_jwt)
420 }
421}
422
423impl From<SdJwtVc> for SdJwt {
424 fn from(value: SdJwtVc) -> Self {
425 let SdJwtVc {
426 mut sd_jwt,
427 parsed_claims,
428 } = value;
429 *sd_jwt.claims_mut() = parsed_claims.into();
431
432 sd_jwt
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use std::sync::LazyLock;
439
440 use identity_core::common::Url;
441
442 use super::*;
443
444 const EXAMPLE_SD_JWT_VC: &str = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogInZjK3NkLWp3dCJ9.eyJfc2QiOiBbIjBIWm1uU0lQejMzN2tTV2U3QzM0bC0tODhnekppLWVCSjJWel9ISndBVGciLCAiOVpicGxDN1RkRVc3cWFsNkJCWmxNdHFKZG1lRU9pWGV2ZEpsb1hWSmRSUSIsICJJMDBmY0ZVb0RYQ3VjcDV5eTJ1anFQc3NEVkdhV05pVWxpTnpfYXdEMGdjIiwgIklFQllTSkdOaFhJbHJRbzU4eWtYbTJaeDN5bGw5WmxUdFRvUG8xN1FRaVkiLCAiTGFpNklVNmQ3R1FhZ1hSN0F2R1RyblhnU2xkM3o4RUlnX2Z2M2ZPWjFXZyIsICJodkRYaHdtR2NKUXNCQ0EyT3RqdUxBY3dBTXBEc2FVMG5rb3ZjS09xV05FIiwgImlrdXVyOFE0azhxM1ZjeUE3ZEMtbU5qWkJrUmVEVFUtQ0c0bmlURTdPVFUiLCAicXZ6TkxqMnZoOW80U0VYT2ZNaVlEdXZUeWtkc1dDTmcwd1RkbHIwQUVJTSIsICJ3elcxNWJoQ2t2a3N4VnZ1SjhSRjN4aThpNjRsbjFqb183NkJDMm9hMXVnIiwgInpPZUJYaHh2SVM0WnptUWNMbHhLdUVBT0dHQnlqT3FhMXoySW9WeF9ZRFEiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNjgzMDAwMDAwLCAiZXhwIjogMTg4MzAwMDAwMCwgInZjdCI6ICJodHRwczovL2JtaS5idW5kLmV4YW1wbGUvY3JlZGVudGlhbC9waWQvMS4wIiwgImFnZV9lcXVhbF9vcl9vdmVyIjogeyJfc2QiOiBbIkZjOElfMDdMT2NnUHdyREpLUXlJR085N3dWc09wbE1Makh2UkM0UjQtV2ciLCAiWEx0TGphZFVXYzl6Tl85aE1KUm9xeTQ2VXNDS2IxSXNoWnV1cVVGS1NDQSIsICJhb0NDenNDN3A0cWhaSUFoX2lkUkNTQ2E2NDF1eWNuYzh6UGZOV3o4bngwIiwgImYxLVAwQTJkS1dhdnYxdUZuTVgyQTctRVh4dmhveHY1YUhodUVJTi1XNjQiLCAiazVoeTJyMDE4dnJzSmpvLVZqZDZnNnl0N0Fhb25Lb25uaXVKOXplbDNqbyIsICJxcDdaX0t5MVlpcDBzWWdETzN6VnVnMk1GdVBOakh4a3NCRG5KWjRhSS1jIl19LCAiX3NkX2FsZyI6ICJzaGEtMjU2IiwgImNuZiI6IHsiandrIjogeyJrdHkiOiAiRUMiLCAiY3J2IjogIlAtMjU2IiwgIngiOiAiVENBRVIxOVp2dTNPSEY0ajRXNHZmU1ZvSElQMUlMaWxEbHM3dkNlR2VtYyIsICJ5IjogIlp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.CaXec2NNooWAy4eTxYbGWI--UeUL0jpC7Zb84PP_09Z655BYcXUTvfj6GPk4mrNqZUU5GT6QntYR8J9rvcBjvA~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIm5hdGlvbmFsaXRpZXMiLCBbIkRFIl1d~WyJNMEpiNTd0NDF1YnJrU3V5ckRUM3hBIiwgIjE4IiwgdHJ1ZV0~eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImtiK2p3dCJ9.eyJub25jZSI6ICIxMjM0NTY3ODkwIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE3MjA0NTQyOTUsICJzZF9oYXNoIjogIlZFejN0bEtqOVY0UzU3TTZoRWhvVjRIc19SdmpXZWgzVHN1OTFDbmxuZUkifQ.GqtiTKNe3O95GLpdxFK_2FZULFk6KUscFe7RPk8OeVLiJiHsGvtPyq89e_grBplvGmnDGHoy8JAt1wQqiwktSg";
445 static EXAMPLE_ISSUER: LazyLock<Url> = LazyLock::new(|| "https://example.com/issuer".parse().unwrap());
446 const EXAMPLE_VCT: &str = "https://bmi.bund.example/credential/pid/1.0";
447
448 #[test]
449 fn simple_sd_jwt_is_not_a_valid_sd_jwt_vc() {
450 let sd_jwt: SdJwt = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1FyazFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZzZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZkx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSLWFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzIiwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTSjFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1cFJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZXd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2bmNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4anZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25VbGRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0MG9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.gR6rSL7urX79CNEvTQnP1MH5xthG11ucIV44SqKFZ4Pvlu_u16RfvXQd4k4CAIBZNKn2aTI18TfvFwV97gJFoA~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~"
451 .parse().unwrap();
452 let err = SdJwtVc::try_from(sd_jwt).unwrap_err();
453 assert!(matches!(err, Error::MissingClaim("vct")))
454 }
455
456 #[test]
457 fn parsing_a_valid_sd_jwt_vc_works() {
458 let sd_jwt_vc: SdJwtVc = EXAMPLE_SD_JWT_VC.parse().unwrap();
459 assert_eq!(sd_jwt_vc.claims().iss, Some(EXAMPLE_ISSUER.clone()));
460 assert_eq!(sd_jwt_vc.claims().vct.as_str(), EXAMPLE_VCT);
461 }
462}