identity_credential/credential/
jwt_serialization.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::borrow::Cow;
5
6#[cfg(feature = "jpt-bbs-plus")]
7use jsonprooftoken::jpt::claims::JptClaims;
8use serde::Deserialize;
9use serde::Serialize;
10
11use identity_core::common::Context;
12use identity_core::common::Object;
13use identity_core::common::OneOrMany;
14use identity_core::common::Timestamp;
15use identity_core::common::Url;
16use serde::de::DeserializeOwned;
17
18use crate::credential::Credential;
19use crate::credential::Evidence;
20use crate::credential::Issuer;
21use crate::credential::Policy;
22use crate::credential::Proof;
23use crate::credential::RefreshService;
24use crate::credential::Schema;
25use crate::credential::Status;
26use crate::credential::Subject;
27use crate::Error;
28use crate::Result;
29
30/// Implementation of JWT Encoding/Decoding according to [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token).
31///
32/// This type is opinionated in the following ways:
33/// 1. Serialization tries to duplicate as little as possible between the required registered claims and the `vc` entry.
34/// 2. Only allows serializing/deserializing claims "exp, iss, nbf &/or iat, jti, sub and vc". Other custom properties
35///    must be set in the `vc` entry.
36#[derive(Serialize, Deserialize)]
37pub(crate) struct CredentialJwtClaims<'credential, T = Object>
38where
39  T: ToOwned + Serialize,
40  <T as ToOwned>::Owned: DeserializeOwned,
41{
42  /// Represents the expirationDate encoded as a UNIX timestamp.  
43  #[serde(skip_serializing_if = "Option::is_none")]
44  exp: Option<i64>,
45  /// Represents the issuer.
46  pub(crate) iss: Cow<'credential, Issuer>,
47
48  /// Represents the issuanceDate encoded as a UNIX timestamp.
49  #[serde(flatten)]
50  issuance_date: IssuanceDateClaims,
51
52  /// Represents the id property of the credential.
53  #[serde(skip_serializing_if = "Option::is_none")]
54  jti: Option<Cow<'credential, Url>>,
55
56  /// Represents the subject's id.
57  #[serde(skip_serializing_if = "Option::is_none")]
58  sub: Option<Cow<'credential, Url>>,
59
60  vc: InnerCredential<'credential, T>,
61
62  #[serde(flatten, skip_serializing_if = "Option::is_none")]
63  pub(crate) custom: Option<Object>,
64}
65
66impl<'credential, T> CredentialJwtClaims<'credential, T>
67where
68  T: ToOwned<Owned = T> + Serialize + DeserializeOwned,
69{
70  pub(crate) fn new(credential: &'credential Credential<T>, custom: Option<Object>) -> Result<Self> {
71    let Credential {
72      context,
73      id,
74      types,
75      credential_subject: OneOrMany::One(subject),
76      issuer,
77      issuance_date,
78      expiration_date,
79      credential_status,
80      credential_schema,
81      refresh_service,
82      terms_of_use,
83      evidence,
84      non_transferable,
85      properties,
86      proof,
87    } = credential
88    else {
89      return Err(Error::MoreThanOneSubjectInJwt);
90    };
91
92    Ok(Self {
93      exp: expiration_date.map(|value| Timestamp::to_unix(&value)),
94      iss: Cow::Borrowed(issuer),
95      issuance_date: IssuanceDateClaims::new(*issuance_date),
96      jti: id.as_ref().map(Cow::Borrowed),
97      sub: subject.id.as_ref().map(Cow::Borrowed),
98      vc: InnerCredential {
99        context: Cow::Borrowed(context),
100        id: None,
101        types: Cow::Borrowed(types),
102        credential_subject: InnerCredentialSubject::new(subject),
103        issuance_date: None,
104        expiration_date: None,
105        issuer: None,
106        credential_schema: Cow::Borrowed(credential_schema),
107        credential_status: credential_status.as_ref().map(Cow::Borrowed),
108        refresh_service: Cow::Borrowed(refresh_service),
109        terms_of_use: Cow::Borrowed(terms_of_use),
110        evidence: Cow::Borrowed(evidence),
111        non_transferable: *non_transferable,
112        properties: Cow::Borrowed(properties),
113        proof: proof.as_ref().map(Cow::Borrowed),
114      },
115      custom,
116    })
117  }
118}
119
120#[cfg(feature = "validator")]
121impl<T> CredentialJwtClaims<'_, T>
122where
123  T: ToOwned<Owned = T> + Serialize + DeserializeOwned,
124{
125  /// Checks whether the fields that are set in the `vc` object are consistent with the corresponding values
126  /// set for the registered claims.
127  fn check_consistency(&self) -> Result<()> {
128    // Check consistency of issuer.
129    let issuer_from_claims: &Issuer = self.iss.as_ref();
130    if !self
131      .vc
132      .issuer
133      .as_ref()
134      .map(|value| value == issuer_from_claims)
135      .unwrap_or(true)
136    {
137      return Err(Error::InconsistentCredentialJwtClaims("inconsistent issuer"));
138    };
139
140    // Check consistency of issuanceDate
141    let issuance_date_from_claims = self.issuance_date.to_issuance_date()?;
142    if !self
143      .vc
144      .issuance_date
145      .map(|value| value == issuance_date_from_claims)
146      .unwrap_or(true)
147    {
148      return Err(Error::InconsistentCredentialJwtClaims("inconsistent issuanceDate"));
149    };
150
151    // Check consistency of expirationDate
152    if !self
153      .vc
154      .expiration_date
155      .map(|value| self.exp.filter(|exp| *exp == value.to_unix()).is_some())
156      .unwrap_or(true)
157    {
158      return Err(Error::InconsistentCredentialJwtClaims(
159        "inconsistent credential expirationDate",
160      ));
161    };
162
163    // Check consistency of id
164    if !self
165      .vc
166      .id
167      .as_ref()
168      .map(|value| self.jti.as_ref().filter(|jti| jti.as_ref() == value).is_some())
169      .unwrap_or(true)
170    {
171      return Err(Error::InconsistentCredentialJwtClaims("inconsistent credential id"));
172    };
173
174    // Check consistency of credentialSubject
175    if let Some(ref inner_credential_subject_id) = self.vc.credential_subject.id {
176      let subject_claim = self.sub.as_ref().ok_or(Error::InconsistentCredentialJwtClaims(
177        "inconsistent credentialSubject: expected identifier in sub",
178      ))?;
179      if subject_claim.as_ref() != inner_credential_subject_id {
180        return Err(Error::InconsistentCredentialJwtClaims(
181          "inconsistent credentialSubject: identifiers do not match",
182        ));
183      }
184    };
185
186    Ok(())
187  }
188
189  /// Converts the JWT representation into a [`Credential`].
190  ///
191  /// # Errors
192  /// Errors if either timestamp conversion or [`Self::check_consistency`] fails.
193  pub(crate) fn try_into_credential(self) -> Result<Credential<T>> {
194    self.check_consistency()?;
195
196    let Self {
197      exp,
198      iss,
199      issuance_date,
200      jti,
201      sub,
202      vc,
203      custom: _,
204    } = self;
205
206    let InnerCredential {
207      context,
208      id: _,
209      types,
210      credential_subject,
211      credential_status,
212      credential_schema,
213      refresh_service,
214      terms_of_use,
215      evidence,
216      non_transferable,
217      properties,
218      proof,
219      issuance_date: _,
220      issuer: _,
221      expiration_date: _,
222    } = vc;
223
224    Ok(Credential {
225      context: context.into_owned(),
226      id: jti.map(Cow::into_owned),
227      types: types.into_owned(),
228      credential_subject: {
229        OneOrMany::One(Subject {
230          id: sub.map(Cow::into_owned),
231          properties: credential_subject.properties.into_owned(),
232        })
233      },
234      issuer: iss.into_owned(),
235      issuance_date: issuance_date.to_issuance_date()?,
236      expiration_date: exp
237        .map(Timestamp::from_unix)
238        .transpose()
239        .map_err(|_| Error::TimestampConversionError)?,
240      credential_status: credential_status.map(Cow::into_owned),
241      credential_schema: credential_schema.into_owned(),
242      refresh_service: refresh_service.into_owned(),
243      terms_of_use: terms_of_use.into_owned(),
244      evidence: evidence.into_owned(),
245      non_transferable,
246      properties: properties.into_owned(),
247      proof: proof.map(Cow::into_owned),
248    })
249  }
250}
251
252/// The [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token) states that issuanceDate
253/// corresponds to the registered `nbf` claim, but `iat` is also used in the ecosystem.
254/// This type aims to take care of this discrepancy on a best effort basis.
255#[derive(Serialize, Deserialize, Clone, Copy)]
256pub(crate) struct IssuanceDateClaims {
257  #[serde(skip_serializing_if = "Option::is_none")]
258  pub(crate) iat: Option<i64>,
259  #[serde(skip_serializing_if = "Option::is_none")]
260  pub(crate) nbf: Option<i64>,
261}
262
263impl IssuanceDateClaims {
264  pub(crate) fn new(issuance_date: Timestamp) -> Self {
265    Self {
266      iat: None,
267      nbf: Some(issuance_date.to_unix()),
268    }
269  }
270  /// Produces the `issuanceDate` value from `nbf` if it is set,
271  /// otherwise falls back to `iat`. If none of these values are set an error is returned.
272  #[cfg(feature = "validator")]
273  pub(crate) fn to_issuance_date(self) -> Result<Timestamp> {
274    if let Some(timestamp) = self
275      .nbf
276      .map(Timestamp::from_unix)
277      .transpose()
278      .map_err(|_| Error::TimestampConversionError)?
279    {
280      Ok(timestamp)
281    } else {
282      Timestamp::from_unix(self.iat.ok_or(Error::TimestampConversionError)?)
283        .map_err(|_| Error::TimestampConversionError)
284    }
285  }
286}
287
288#[derive(Serialize, Deserialize)]
289struct InnerCredentialSubject<'credential> {
290  // Do not serialize this to save space as the value must be included in the `sub` claim.
291  #[cfg(feature = "validator")]
292  #[serde(skip_serializing)]
293  id: Option<Url>,
294
295  #[serde(flatten)]
296  properties: Cow<'credential, Object>,
297}
298
299impl<'credential> InnerCredentialSubject<'credential> {
300  fn new(subject: &'credential Subject) -> Self {
301    Self {
302      #[cfg(feature = "validator")]
303      id: None,
304      properties: Cow::Borrowed(&subject.properties),
305    }
306  }
307}
308
309/// Mostly copied from [`VerifiableCredential`] with the entries corresponding
310/// to registered claims being the exception.
311#[derive(Serialize, Deserialize)]
312struct InnerCredential<'credential, T = Object>
313where
314  T: ToOwned + Serialize,
315  <T as ToOwned>::Owned: DeserializeOwned,
316{
317  /// The JSON-LD context(s) applicable to the `Credential`.
318  #[serde(rename = "@context")]
319  context: Cow<'credential, OneOrMany<Context>>,
320  /// A unique `URI` that may be used to identify the `Credential`.
321  #[serde(skip_serializing_if = "Option::is_none")]
322  id: Option<Url>,
323  /// One or more URIs defining the type of the `Credential`.
324  #[serde(rename = "type")]
325  types: Cow<'credential, OneOrMany<String>>,
326  /// The issuer of the `Credential`.
327  #[serde(skip_serializing_if = "Option::is_none")]
328  issuer: Option<Issuer>,
329  /// One or more `Object`s representing the `Credential` subject(s).
330  #[serde(rename = "credentialSubject")]
331  credential_subject: InnerCredentialSubject<'credential>,
332  /// A timestamp of when the `Credential` becomes valid.
333  #[serde(rename = "issuanceDate", skip_serializing_if = "Option::is_none")]
334  issuance_date: Option<Timestamp>,
335  /// A timestamp of when the `Credential` should no longer be considered valid.
336  #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
337  expiration_date: Option<Timestamp>,
338  /// Information used to determine the current status of the `Credential`.
339  #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
340  credential_status: Option<Cow<'credential, Status>>,
341  /// Information used to assist in the enforcement of a specific `Credential` structure.
342  #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
343  credential_schema: Cow<'credential, OneOrMany<Schema>>,
344  /// Service(s) used to refresh an expired `Credential`.
345  #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
346  refresh_service: Cow<'credential, OneOrMany<RefreshService>>,
347  /// Terms-of-use specified by the `Credential` issuer.
348  #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
349  terms_of_use: Cow<'credential, OneOrMany<Policy>>,
350  /// Human-readable evidence used to support the claims within the `Credential`.
351  #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
352  evidence: Cow<'credential, OneOrMany<Evidence>>,
353  /// Indicates that the `Credential` must only be contained within a
354  /// [`Presentation`][crate::presentation::Presentation] with a proof issued from the `Credential` subject.
355  #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
356  non_transferable: Option<bool>,
357  /// Miscellaneous properties.
358  #[serde(flatten)]
359  properties: Cow<'credential, T>,
360  /// Proof(s) used to verify a `Credential`
361  #[serde(skip_serializing_if = "Option::is_none")]
362  proof: Option<Cow<'credential, Proof>>,
363}
364
365#[cfg(feature = "jpt-bbs-plus")]
366impl<'credential, T> From<CredentialJwtClaims<'credential, T>> for JptClaims
367where
368  T: ToOwned + Serialize,
369  <T as ToOwned>::Owned: DeserializeOwned,
370{
371  fn from(item: CredentialJwtClaims<'credential, T>) -> Self {
372    let CredentialJwtClaims {
373      exp,
374      iss,
375      issuance_date,
376      jti,
377      sub,
378      vc,
379      custom,
380    } = item;
381
382    let mut claims = JptClaims::new();
383
384    if let Some(exp) = exp {
385      claims.set_exp(exp);
386    }
387
388    claims.set_iss(iss.url().to_string());
389
390    if let Some(iat) = issuance_date.iat {
391      claims.set_iat(iat);
392    }
393
394    if let Some(nbf) = issuance_date.nbf {
395      claims.set_nbf(nbf);
396    }
397
398    if let Some(jti) = jti {
399      claims.set_jti(jti.to_string());
400    }
401
402    if let Some(sub) = sub {
403      claims.set_sub(sub.to_string());
404    }
405
406    claims.set_claim(Some("vc"), vc, true);
407
408    if let Some(custom) = custom {
409      claims.set_claim(None, custom, true);
410    }
411
412    claims
413  }
414}
415
416#[cfg(test)]
417mod tests {
418  use identity_core::common::Object;
419  use identity_core::convert::FromJson;
420  use identity_core::convert::ToJson;
421
422  use crate::credential::Credential;
423  use crate::Error;
424
425  use super::CredentialJwtClaims;
426
427  #[test]
428  fn roundtrip() {
429    let credential_json: &str = r#"
430    {
431      "@context": [
432        "https://www.w3.org/2018/credentials/v1",
433        "https://www.w3.org/2018/credentials/examples/v1"
434      ],
435      "id": "http://example.edu/credentials/3732",
436      "type": ["VerifiableCredential", "UniversityDegreeCredential"],
437      "issuer": "https://example.edu/issuers/14",
438      "issuanceDate": "2010-01-01T19:23:24Z",
439      "credentialSubject": {
440        "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
441        "degree": {
442          "type": "BachelorDegree",
443          "name": "Bachelor of Science in Mechanical Engineering"
444        }
445      }
446    }"#;
447
448    let expected_serialization_json: &str = r#"
449    {
450      "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
451      "jti": "http://example.edu/credentials/3732",
452      "iss": "https://example.edu/issuers/14",
453      "nbf":  1262373804,
454      "vc": {
455        "@context": [
456        "https://www.w3.org/2018/credentials/v1",
457        "https://www.w3.org/2018/credentials/examples/v1"
458      ],
459      "type": ["VerifiableCredential", "UniversityDegreeCredential"],
460      "credentialSubject": {
461        "degree": {
462          "type": "BachelorDegree",
463          "name": "Bachelor of Science in Mechanical Engineering"
464          }
465        }
466      }
467    }"#;
468
469    let credential: Credential = Credential::from_json(credential_json).unwrap();
470    let jwt_credential_claims: CredentialJwtClaims<'_> = CredentialJwtClaims::new(&credential, None).unwrap();
471    let jwt_credential_claims_serialized: String = jwt_credential_claims.to_json().unwrap();
472    // Compare JSON representations
473    assert_eq!(
474      Object::from_json(expected_serialization_json).unwrap(),
475      Object::from_json(&jwt_credential_claims_serialized).unwrap()
476    );
477
478    // Retrieve the credential from the JWT serialization
479    let retrieved_credential: Credential = {
480      CredentialJwtClaims::<'static, Object>::from_json(&jwt_credential_claims_serialized)
481        .unwrap()
482        .try_into_credential()
483        .unwrap()
484    };
485
486    assert_eq!(credential, retrieved_credential);
487  }
488
489  #[test]
490  fn claims_duplication() {
491    let credential_json: &str = r#"
492    {
493      "@context": [
494        "https://www.w3.org/2018/credentials/v1",
495        "https://www.w3.org/2018/credentials/examples/v1"
496      ],
497      "id": "http://example.edu/credentials/3732",
498      "type": ["VerifiableCredential", "UniversityDegreeCredential"],
499      "issuer": "https://example.edu/issuers/14",
500      "issuanceDate": "2010-01-01T19:23:24Z",
501      "expirationDate": "2025-09-13T15:56:23Z",
502      "credentialSubject": {
503        "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
504        "degree": {
505          "type": "BachelorDegree",
506          "name": "Bachelor of Science in Mechanical Engineering"
507        }
508      }
509    }"#;
510
511    // `sub`, `exp`, `jti`, `iss`, `nbf` are duplicated in `vc`.
512    let claims_json: &str = r#"
513    {
514      "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
515      "jti": "http://example.edu/credentials/3732",
516      "iss": "https://example.edu/issuers/14",
517      "nbf":  1262373804,
518      "exp": 1757778983,
519      "vc": {
520        "@context": [
521          "https://www.w3.org/2018/credentials/v1",
522          "https://www.w3.org/2018/credentials/examples/v1"
523        ],
524        "id": "http://example.edu/credentials/3732",
525        "type": ["VerifiableCredential", "UniversityDegreeCredential"],
526        "issuer": "https://example.edu/issuers/14",
527        "issuanceDate": "2010-01-01T19:23:24Z",
528        "expirationDate": "2025-09-13T15:56:23Z",
529        "credentialSubject": {
530          "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
531          "degree": {
532            "type": "BachelorDegree",
533            "name": "Bachelor of Science in Mechanical Engineering"
534          }
535        }
536      }
537    }"#;
538
539    let credential: Credential = Credential::from_json(credential_json).unwrap();
540    let credential_from_claims: Credential = CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
541      .unwrap()
542      .try_into_credential()
543      .unwrap();
544
545    assert_eq!(credential, credential_from_claims);
546  }
547
548  #[test]
549  fn inconsistent_issuer() {
550    // issuer is inconsistent (15 instead of 14).
551    let claims_json: &str = r#"
552    {
553      "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
554      "jti": "http://example.edu/credentials/3732",
555      "iss": "https://example.edu/issuers/14",
556      "nbf":  1262373804,
557      "vc": {
558        "@context": [
559          "https://www.w3.org/2018/credentials/v1",
560          "https://www.w3.org/2018/credentials/examples/v1"
561        ],
562        "type": ["VerifiableCredential", "UniversityDegreeCredential"],
563        "issuer": "https://example.edu/issuers/15",
564        "credentialSubject": {
565          "degree": {
566            "type": "BachelorDegree",
567            "name": "Bachelor of Science in Mechanical Engineering"
568          }
569        }
570      }
571    }"#;
572
573    let credential_from_claims_result: Result<Credential, _> =
574      CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
575        .unwrap()
576        .try_into_credential();
577    assert!(matches!(
578      credential_from_claims_result.unwrap_err(),
579      Error::InconsistentCredentialJwtClaims("inconsistent issuer")
580    ));
581  }
582
583  #[test]
584  fn inconsistent_id() {
585    let claims_json: &str = r#"
586    {
587      "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
588      "jti": "http://example.edu/credentials/3732",
589      "iss": "https://example.edu/issuers/14",
590      "nbf":  1262373804,
591      "vc": {
592        "@context": [
593          "https://www.w3.org/2018/credentials/v1",
594          "https://www.w3.org/2018/credentials/examples/v1"
595        ],
596        "type": ["VerifiableCredential", "UniversityDegreeCredential"],
597        "id": "http://example.edu/credentials/1111",
598        "credentialSubject": {
599          "degree": {
600            "type": "BachelorDegree",
601            "name": "Bachelor of Science in Mechanical Engineering"
602          }
603        }
604      }
605    }"#;
606
607    let credential_from_claims_result: Result<Credential, _> =
608      CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
609        .unwrap()
610        .try_into_credential();
611    assert!(matches!(
612      credential_from_claims_result.unwrap_err(),
613      Error::InconsistentCredentialJwtClaims("inconsistent credential id")
614    ));
615  }
616
617  #[test]
618  fn inconsistent_subject() {
619    let claims_json: &str = r#"
620    {
621      "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
622      "jti": "http://example.edu/credentials/3732",
623      "iss": "https://example.edu/issuers/14",
624      "nbf":  1262373804,
625      "vc": {
626        "@context": [
627          "https://www.w3.org/2018/credentials/v1",
628          "https://www.w3.org/2018/credentials/examples/v1"
629        ],
630        "id": "http://example.edu/credentials/3732",
631        "type": ["VerifiableCredential", "UniversityDegreeCredential"],
632        "issuer": "https://example.edu/issuers/14",
633        "issuanceDate": "2010-01-01T19:23:24Z",
634        "credentialSubject": {
635          "id": "did:example:1111111111111111111111111",
636          "degree": {
637            "type": "BachelorDegree",
638            "name": "Bachelor of Science in Mechanical Engineering"
639          }
640        }
641      }
642    }"#;
643
644    let credential_from_claims_result: Result<Credential, _> =
645      CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
646        .unwrap()
647        .try_into_credential();
648    assert!(matches!(
649      credential_from_claims_result.unwrap_err(),
650      Error::InconsistentCredentialJwtClaims("inconsistent credentialSubject: identifiers do not match")
651    ));
652  }
653
654  #[test]
655  fn inconsistent_issuance_date() {
656    // issuer is inconsistent (15 instead of 14).
657    let claims_json: &str = r#"
658    {
659      "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
660      "jti": "http://example.edu/credentials/3732",
661      "iss": "https://example.edu/issuers/14",
662      "nbf":  1262373804,
663      "vc": {
664        "@context": [
665          "https://www.w3.org/2018/credentials/v1",
666          "https://www.w3.org/2018/credentials/examples/v1"
667        ],
668        "id": "http://example.edu/credentials/3732",
669        "type": ["VerifiableCredential", "UniversityDegreeCredential"],
670        "issuer": "https://example.edu/issuers/14",
671        "issuanceDate": "2020-01-01T19:23:24Z",
672        "credentialSubject": {
673          "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
674          "degree": {
675            "type": "BachelorDegree",
676            "name": "Bachelor of Science in Mechanical Engineering"
677          }
678        }
679      }
680    }"#;
681
682    let credential_from_claims_result: Result<Credential, _> =
683      CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
684        .unwrap()
685        .try_into_credential();
686    assert!(matches!(
687      credential_from_claims_result.unwrap_err(),
688      Error::InconsistentCredentialJwtClaims("inconsistent issuanceDate")
689    ));
690  }
691
692  #[test]
693  fn inconsistent_expiration_date() {
694    // issuer is inconsistent (15 instead of 14).
695    let claims_json: &str = r#"
696    {
697      "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
698      "jti": "http://example.edu/credentials/3732",
699      "iss": "https://example.edu/issuers/14",
700      "nbf":  1262373804,
701      "exp": 1757778983,
702      "vc": {
703        "@context": [
704          "https://www.w3.org/2018/credentials/v1",
705          "https://www.w3.org/2018/credentials/examples/v1"
706        ],
707        "id": "http://example.edu/credentials/3732",
708        "type": ["VerifiableCredential", "UniversityDegreeCredential"],
709        "issuer": "https://example.edu/issuers/14",
710        "issuanceDate": "2010-01-01T19:23:24Z",
711        "expirationDate": "2026-09-13T15:56:23Z",
712        "credentialSubject": {
713          "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
714          "degree": {
715            "type": "BachelorDegree",
716            "name": "Bachelor of Science in Mechanical Engineering"
717          }
718        }
719      }
720    }"#;
721
722    let credential_from_claims_result: Result<Credential, _> =
723      CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
724        .unwrap()
725        .try_into_credential();
726    assert!(matches!(
727      credential_from_claims_result.unwrap_err(),
728      Error::InconsistentCredentialJwtClaims("inconsistent credential expirationDate")
729    ));
730  }
731}