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_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
37/// SD-JWT VC's JOSE header `typ`'s value.
38pub const SD_JWT_VC_TYP: &str = "vc+sd-jwt";
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41/// An SD-JWT carrying a verifiable credential as described in
42/// [SD-JWT VC specification](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html).
43pub 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  /// Parses a string into an [`SdJwtVc`].
64  pub fn parse(s: &str) -> Result<Self> {
65    s.parse()
66  }
67
68  /// Returns a reference to this [`SdJwtVc`]'s JWT claims.
69  pub fn claims(&self) -> &SdJwtVcClaims {
70    &self.parsed_claims
71  }
72
73  /// Prepares this [`SdJwtVc`] for a presentation, returning an [`SdJwtVcPresentationBuilder`].
74  /// ## Errors
75  /// - [`Error::SdJwt`] is returned if the provided `hasher`'s algorithm doesn't match the algorithm specified by
76  ///   SD-JWT's `_sd_alg` claim. "sha-256" is used if the claim is missing.
77  pub fn into_presentation(self, hasher: &dyn Hasher) -> Result<SdJwtVcPresentationBuilder> {
78    SdJwtVcPresentationBuilder::new(self, hasher)
79  }
80
81  /// Returns the JSON object obtained by replacing all disclosures into their
82  /// corresponding JWT concealable claims.
83  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  /// Retrieves this SD-JWT VC's issuer's metadata by querying its default location.
88  /// ## Notes
89  /// This method doesn't perform any validation of the retrieved [`IssuerMetadata`]
90  /// besides its syntactical validity.
91  /// To check if the retrieved [`IssuerMetadata`] is valid use [`IssuerMetadata::validate`].
92  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  /// Retrieve this SD-JWT VC credential's type metadata [`TypeMetadata`].
114  /// ## Notes
115  /// `resolver` is fed with whatever value [`SdJwtVc`]'s `vct` might have.
116  /// If `vct` is a URI with scheme `https`, `resolver` must fetch the [`TypeMetadata`]
117  /// resource by combining `vct`'s value with [`WELL_KNOWN_VCT`]. To simplify this process
118  /// the utility function [`vct_to_url`] is provided.
119  ///
120  /// Returns the parsed [`TypeMetadata`] along with the raw [`Resolver`]'s response.
121  /// The latter can be used to validate the `vct#integrity` claim if present.
122  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  /// Resolves the issuer's public key in JWK format.
140  /// The issuer's JWK is first fetched through the issuer's metadata,
141  /// if this attempt fails `resolver` is used to query the key directly
142  /// through `kid`'s value.
143  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    // Try to find the key among issuer metadata jwk set.
154    if let jwk @ Ok(_) = self.issuer_jwk_from_iss_metadata(resolver, kid).await {
155      jwk
156    } else {
157      // Issuer has no metadata that can lead to its JWK. Let's see if it can be resolved directly.
158      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  /// Verifies this [`SdJwtVc`] JWT's signature.
205  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  /// Checks the disclosability of this [`SdJwtVc`]'s claims against a list of [`ClaimMetadata`].
221  /// ## Notes
222  /// This check should be performed by the token's holder in order to assert the issuer's compliance with
223  /// the credential's type.
224  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  /// Check whether this [`SdJwtVc`] is valid.
232  ///
233  /// This method checks:
234  /// - JWS signature
235  /// - credential's type
236  /// - claims' disclosability
237  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    // Signature verification.
245    // Fetch issuer's JWK.
246    let jwk = self.issuer_jwk(resolver).await?;
247    self.verify_signature(jws_verifier, &jwk)?;
248
249    // Credential type.
250    // Fetch type metadata. Skip integrity check.
251    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    // Claims' disclosability.
258    self.validate_claims_disclosability(type_metadata.claim_metadata())?;
259
260    Ok(())
261  }
262
263  /// Verify the signature of this [`SdJwtVc`]'s [sd_jwt_payload_rework::KeyBindingJwt].
264  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  /// Check the validity of this [`SdJwtVc`]'s [sd_jwt_payload_rework::KeyBindingJwt].
277  /// # Notes
278  /// Validation of the required key binding (specified through the `cnf` JWT's claim)
279  /// is only partially validated - custom and "jwe" requirement are not checked.
280  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    // Validate SD-JWT digest.
361    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
379/// Converts `vct` claim's URI value into the appropriate well-known URL.
380/// ## Warnings
381/// Returns an [`Option::None`] if the URI's scheme is not `https`.
382pub 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    // Validate claims.
396    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    // Validate Header's typ.
402    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    // Put back `parsed_claims`.
438    *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}