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