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