1use 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#[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#[derive(Serialize, Deserialize)]
50pub(crate) struct CredentialJwtClaims<'credential, T = Object>
51where
52 T: ToOwned + Serialize,
53 <T as ToOwned>::Owned: DeserializeOwned,
54{
55 #[serde(skip_serializing_if = "Option::is_none")]
57 exp: Option<i64>,
58 pub(crate) iss: Cow<'credential, Issuer>,
60
61 #[serde(flatten)]
63 issuance_date: IssuanceDateClaims,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 jti: Option<Cow<'credential, Url>>,
68
69 #[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 fn check_consistency(&self) -> Result<()> {
141 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 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 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 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 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 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#[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 #[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 #[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#[derive(Serialize, Deserialize)]
325struct InnerCredential<'credential, T = Object>
326where
327 T: ToOwned + Serialize,
328 <T as ToOwned>::Owned: DeserializeOwned,
329{
330 #[serde(rename = "@context")]
332 context: Cow<'credential, OneOrMany<Context>>,
333 #[serde(skip_serializing_if = "Option::is_none")]
335 id: Option<Url>,
336 #[serde(rename = "type")]
338 types: Cow<'credential, OneOrMany<String>>,
339 #[serde(skip_serializing_if = "Option::is_none")]
341 issuer: Option<Issuer>,
342 #[serde(rename = "credentialSubject")]
344 credential_subject: InnerCredentialSubject<'credential>,
345 #[serde(rename = "issuanceDate", skip_serializing_if = "Option::is_none")]
347 issuance_date: Option<Timestamp>,
348 #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
350 expiration_date: Option<Timestamp>,
351 #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
353 credential_status: Option<Cow<'credential, Status>>,
354 #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
356 credential_schema: Cow<'credential, OneOrMany<Schema>>,
357 #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
359 refresh_service: Cow<'credential, OneOrMany<RefreshService>>,
360 #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
362 terms_of_use: Cow<'credential, OneOrMany<Policy>>,
363 #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
365 evidence: Cow<'credential, OneOrMany<Evidence>>,
366 #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
369 non_transferable: Option<bool>,
370 #[serde(flatten)]
372 properties: Cow<'credential, T>,
373 #[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 assert_eq!(
487 Object::from_json(expected_serialization_json).unwrap(),
488 Object::from_json(&jwt_credential_claims_serialized).unwrap()
489 );
490
491 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 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 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 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 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}