identity_credential/sd_jwt_vc/
token.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
36/// SD-JWT VC's JOSE header `typ`'s value.
37pub const SD_JWT_VC_TYP: &str = "dc+sd-jwt";
38/// SD-JWT VC's alternative JOSE header `typ`'s value.
39/// This value is accepted for interoperability with older implementations.
40pub const SD_JWT_VC_TYP_ALTERNATIVE: &str = "vc+sd-jwt";
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43/// An SD-JWT carrying a verifiable credential as described in
44/// [SD-JWT VC specification](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-13.html).
45pub 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  /// Parses a string into an [`SdJwtVc`].
66  pub fn parse(s: &str) -> Result<Self> {
67    s.parse()
68  }
69
70  /// Returns a reference to this [`SdJwtVc`]'s JWT claims.
71  pub fn claims(&self) -> &SdJwtVcClaims {
72    &self.parsed_claims
73  }
74
75  /// Attaches a KB-JWT to this [`SdJwtVc`].
76  pub fn attach_key_binding_jwt(&mut self, kb_jwt: KeyBindingJwt) {
77    self.sd_jwt.attach_key_binding_jwt(kb_jwt);
78  }
79
80  /// Prepares this [`SdJwtVc`] for a presentation, returning an [`SdJwtVcPresentationBuilder`].
81  /// ## Errors
82  /// - [`Error::SdJwt`] is returned if the provided `hasher`'s algorithm doesn't match the algorithm specified by
83  ///   SD-JWT's `_sd_alg` claim. "sha-256" is used if the claim is missing.
84  pub fn into_presentation(self, hasher: &dyn Hasher) -> Result<SdJwtVcPresentationBuilder> {
85    SdJwtVcPresentationBuilder::new(self, hasher)
86  }
87
88  /// Returns the JSON object obtained by replacing all disclosures into their
89  /// corresponding JWT concealable claims.
90  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  /// Retrieves this SD-JWT VC's issuer's metadata by querying its default location.
95  /// ## Notes
96  /// This method doesn't perform any validation of the retrieved [`IssuerMetadata`]
97  /// besides its syntactical validity.
98  /// To check if the retrieved [`IssuerMetadata`] is valid use [`IssuerMetadata::validate`].
99  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  /// Retrieve this SD-JWT VC credential's type metadata [`TypeMetadata`].
124  ///
125  /// Returns the parsed [`TypeMetadata`] along with the raw [`Resolver`]'s response.
126  /// The latter can be used to validate the `vct#integrity` claim if present.
127  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  /// Resolves the issuer's public key in JWK format.
142  /// The issuer's JWK is first fetched through the issuer's metadata,
143  /// if this attempt fails `resolver` is used to query the key directly
144  /// through `kid`'s value.
145  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    // Try to find the key among issuer metadata jwk set.
156    if let jwk @ Ok(_) = self.issuer_jwk_from_iss_metadata(resolver, kid).await {
157      jwk
158    } else {
159      // Issuer has no metadata that can lead to its JWK. Let's see if it can be resolved directly.
160      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  /// Verifies this [`SdJwtVc`] JWT's signature.
207  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  /// Checks the disclosability of this [`SdJwtVc`]'s claims against a list of [`ClaimMetadata`].
226  /// ## Notes
227  /// This check should be performed by the token's holder in order to assert the issuer's compliance with
228  /// the credential's type.
229  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  /// Check whether this [`SdJwtVc`] is valid.
237  ///
238  /// This method checks:
239  /// - JWS signature
240  /// - credential's type
241  /// - claims' disclosability
242  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    // Signature verification.
250    // Fetch issuer's JWK.
251    let jwk = self.issuer_jwk(resolver).await?;
252    self.verify_signature(jws_verifier, &jwk)?;
253
254    // Credential type.
255    // Fetch type metadata. Skip integrity check.
256    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    // Claims' disclosability.
263    self.validate_claims_disclosability(type_metadata.claim_metadata())?;
264
265    Ok(())
266  }
267
268  /// Verify the signature of this [`SdJwtVc`]'s [sd_jwt::KeyBindingJwt].
269  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  /// Check the validity of this [`SdJwtVc`]'s [sd_jwt::KeyBindingJwt].
282  /// # Notes
283  /// Validation of the required key binding (specified through the `cnf` JWT's claim)
284  /// is only partially validated - custom and "jwe" requirement are not checked.
285  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    // Validate SD-JWT digest.
366    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    // Validate claims.
388    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    // Validate Header's typ.
394    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    // Put back `parsed_claims`.
430    *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}