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 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 fn check_consistency(&self) -> Result<()> {
143 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 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 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 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 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 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#[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 #[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 #[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#[derive(Serialize, Deserialize)]
324struct InnerCredential<'credential, T = Object>
325where
326 T: ToOwned + Serialize,
327 <T as ToOwned>::Owned: DeserializeOwned,
328{
329 #[serde(rename = "@context")]
331 context: Cow<'credential, OneOrMany<Context>>,
332 #[serde(skip_serializing_if = "Option::is_none")]
334 id: Option<Url>,
335 #[serde(rename = "type")]
337 types: Cow<'credential, OneOrMany<String>>,
338 #[serde(skip_serializing_if = "Option::is_none")]
340 issuer: Option<Issuer>,
341 #[serde(rename = "credentialSubject")]
343 credential_subject: InnerCredentialSubject<'credential>,
344 #[serde(rename = "issuanceDate", skip_serializing_if = "Option::is_none")]
346 issuance_date: Option<Timestamp>,
347 #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
349 expiration_date: Option<Timestamp>,
350 #[serde(rename = "validFrom", skip_serializing_if = "Option::is_none")]
352 valid_from: Option<Timestamp>,
353 #[serde(rename = "validUntil", skip_serializing_if = "Option::is_none")]
355 valid_until: Option<Timestamp>,
356 #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
358 credential_status: Option<Cow<'credential, Status>>,
359 #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
361 credential_schema: Cow<'credential, OneOrMany<Schema>>,
362 #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
364 refresh_service: Cow<'credential, OneOrMany<RefreshService>>,
365 #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
367 terms_of_use: Cow<'credential, OneOrMany<Policy>>,
368 #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
370 evidence: Cow<'credential, OneOrMany<Evidence>>,
371 #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
374 non_transferable: Option<bool>,
375 #[serde(flatten)]
377 properties: Cow<'credential, T>,
378 #[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 assert_eq!(
492 Object::from_json(expected_serialization_json).unwrap(),
493 Object::from_json(&jwt_credential_claims_serialized).unwrap()
494 );
495
496 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 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 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 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 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}