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