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)]
37pub(crate) struct CredentialJwtClaims<'credential, T = Object>
38where
39 T: ToOwned + Serialize,
40 <T as ToOwned>::Owned: DeserializeOwned,
41{
42 #[serde(skip_serializing_if = "Option::is_none")]
44 exp: Option<i64>,
45 pub(crate) iss: Cow<'credential, Issuer>,
47
48 #[serde(flatten)]
50 issuance_date: IssuanceDateClaims,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 jti: Option<Cow<'credential, Url>>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 sub: Option<Cow<'credential, Url>>,
59
60 vc: InnerCredential<'credential, T>,
61
62 #[serde(flatten, skip_serializing_if = "Option::is_none")]
63 pub(crate) custom: Option<Object>,
64}
65
66impl<'credential, T> CredentialJwtClaims<'credential, T>
67where
68 T: ToOwned<Owned = T> + Serialize + DeserializeOwned,
69{
70 pub(crate) fn new(credential: &'credential Credential<T>, custom: Option<Object>) -> Result<Self> {
71 let Credential {
72 context,
73 id,
74 types,
75 credential_subject: OneOrMany::One(subject),
76 issuer,
77 issuance_date,
78 expiration_date,
79 credential_status,
80 credential_schema,
81 refresh_service,
82 terms_of_use,
83 evidence,
84 non_transferable,
85 properties,
86 proof,
87 } = credential
88 else {
89 return Err(Error::MoreThanOneSubjectInJwt);
90 };
91
92 Ok(Self {
93 exp: expiration_date.map(|value| Timestamp::to_unix(&value)),
94 iss: Cow::Borrowed(issuer),
95 issuance_date: IssuanceDateClaims::new(*issuance_date),
96 jti: id.as_ref().map(Cow::Borrowed),
97 sub: subject.id.as_ref().map(Cow::Borrowed),
98 vc: InnerCredential {
99 context: Cow::Borrowed(context),
100 id: None,
101 types: Cow::Borrowed(types),
102 credential_subject: InnerCredentialSubject::new(subject),
103 issuance_date: None,
104 expiration_date: None,
105 issuer: None,
106 credential_schema: Cow::Borrowed(credential_schema),
107 credential_status: credential_status.as_ref().map(Cow::Borrowed),
108 refresh_service: Cow::Borrowed(refresh_service),
109 terms_of_use: Cow::Borrowed(terms_of_use),
110 evidence: Cow::Borrowed(evidence),
111 non_transferable: *non_transferable,
112 properties: Cow::Borrowed(properties),
113 proof: proof.as_ref().map(Cow::Borrowed),
114 },
115 custom,
116 })
117 }
118}
119
120#[cfg(feature = "validator")]
121impl<T> CredentialJwtClaims<'_, T>
122where
123 T: ToOwned<Owned = T> + Serialize + DeserializeOwned,
124{
125 fn check_consistency(&self) -> Result<()> {
128 let issuer_from_claims: &Issuer = self.iss.as_ref();
130 if !self
131 .vc
132 .issuer
133 .as_ref()
134 .map(|value| value == issuer_from_claims)
135 .unwrap_or(true)
136 {
137 return Err(Error::InconsistentCredentialJwtClaims("inconsistent issuer"));
138 };
139
140 let issuance_date_from_claims = self.issuance_date.to_issuance_date()?;
142 if !self
143 .vc
144 .issuance_date
145 .map(|value| value == issuance_date_from_claims)
146 .unwrap_or(true)
147 {
148 return Err(Error::InconsistentCredentialJwtClaims("inconsistent issuanceDate"));
149 };
150
151 if !self
153 .vc
154 .expiration_date
155 .map(|value| self.exp.filter(|exp| *exp == value.to_unix()).is_some())
156 .unwrap_or(true)
157 {
158 return Err(Error::InconsistentCredentialJwtClaims(
159 "inconsistent credential expirationDate",
160 ));
161 };
162
163 if !self
165 .vc
166 .id
167 .as_ref()
168 .map(|value| self.jti.as_ref().filter(|jti| jti.as_ref() == value).is_some())
169 .unwrap_or(true)
170 {
171 return Err(Error::InconsistentCredentialJwtClaims("inconsistent credential id"));
172 };
173
174 if let Some(ref inner_credential_subject_id) = self.vc.credential_subject.id {
176 let subject_claim = self.sub.as_ref().ok_or(Error::InconsistentCredentialJwtClaims(
177 "inconsistent credentialSubject: expected identifier in sub",
178 ))?;
179 if subject_claim.as_ref() != inner_credential_subject_id {
180 return Err(Error::InconsistentCredentialJwtClaims(
181 "inconsistent credentialSubject: identifiers do not match",
182 ));
183 }
184 };
185
186 Ok(())
187 }
188
189 pub(crate) fn try_into_credential(self) -> Result<Credential<T>> {
194 self.check_consistency()?;
195
196 let Self {
197 exp,
198 iss,
199 issuance_date,
200 jti,
201 sub,
202 vc,
203 custom: _,
204 } = self;
205
206 let InnerCredential {
207 context,
208 id: _,
209 types,
210 credential_subject,
211 credential_status,
212 credential_schema,
213 refresh_service,
214 terms_of_use,
215 evidence,
216 non_transferable,
217 properties,
218 proof,
219 issuance_date: _,
220 issuer: _,
221 expiration_date: _,
222 } = vc;
223
224 Ok(Credential {
225 context: context.into_owned(),
226 id: jti.map(Cow::into_owned),
227 types: types.into_owned(),
228 credential_subject: {
229 OneOrMany::One(Subject {
230 id: sub.map(Cow::into_owned),
231 properties: credential_subject.properties.into_owned(),
232 })
233 },
234 issuer: iss.into_owned(),
235 issuance_date: issuance_date.to_issuance_date()?,
236 expiration_date: exp
237 .map(Timestamp::from_unix)
238 .transpose()
239 .map_err(|_| Error::TimestampConversionError)?,
240 credential_status: credential_status.map(Cow::into_owned),
241 credential_schema: credential_schema.into_owned(),
242 refresh_service: refresh_service.into_owned(),
243 terms_of_use: terms_of_use.into_owned(),
244 evidence: evidence.into_owned(),
245 non_transferable,
246 properties: properties.into_owned(),
247 proof: proof.map(Cow::into_owned),
248 })
249 }
250}
251
252#[derive(Serialize, Deserialize, Clone, Copy)]
256pub(crate) struct IssuanceDateClaims {
257 #[serde(skip_serializing_if = "Option::is_none")]
258 pub(crate) iat: Option<i64>,
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub(crate) nbf: Option<i64>,
261}
262
263impl IssuanceDateClaims {
264 pub(crate) fn new(issuance_date: Timestamp) -> Self {
265 Self {
266 iat: None,
267 nbf: Some(issuance_date.to_unix()),
268 }
269 }
270 #[cfg(feature = "validator")]
273 pub(crate) fn to_issuance_date(self) -> Result<Timestamp> {
274 if let Some(timestamp) = self
275 .nbf
276 .map(Timestamp::from_unix)
277 .transpose()
278 .map_err(|_| Error::TimestampConversionError)?
279 {
280 Ok(timestamp)
281 } else {
282 Timestamp::from_unix(self.iat.ok_or(Error::TimestampConversionError)?)
283 .map_err(|_| Error::TimestampConversionError)
284 }
285 }
286}
287
288#[derive(Serialize, Deserialize)]
289struct InnerCredentialSubject<'credential> {
290 #[cfg(feature = "validator")]
292 #[serde(skip_serializing)]
293 id: Option<Url>,
294
295 #[serde(flatten)]
296 properties: Cow<'credential, Object>,
297}
298
299impl<'credential> InnerCredentialSubject<'credential> {
300 fn new(subject: &'credential Subject) -> Self {
301 Self {
302 #[cfg(feature = "validator")]
303 id: None,
304 properties: Cow::Borrowed(&subject.properties),
305 }
306 }
307}
308
309#[derive(Serialize, Deserialize)]
312struct InnerCredential<'credential, T = Object>
313where
314 T: ToOwned + Serialize,
315 <T as ToOwned>::Owned: DeserializeOwned,
316{
317 #[serde(rename = "@context")]
319 context: Cow<'credential, OneOrMany<Context>>,
320 #[serde(skip_serializing_if = "Option::is_none")]
322 id: Option<Url>,
323 #[serde(rename = "type")]
325 types: Cow<'credential, OneOrMany<String>>,
326 #[serde(skip_serializing_if = "Option::is_none")]
328 issuer: Option<Issuer>,
329 #[serde(rename = "credentialSubject")]
331 credential_subject: InnerCredentialSubject<'credential>,
332 #[serde(rename = "issuanceDate", skip_serializing_if = "Option::is_none")]
334 issuance_date: Option<Timestamp>,
335 #[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
337 expiration_date: Option<Timestamp>,
338 #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
340 credential_status: Option<Cow<'credential, Status>>,
341 #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
343 credential_schema: Cow<'credential, OneOrMany<Schema>>,
344 #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
346 refresh_service: Cow<'credential, OneOrMany<RefreshService>>,
347 #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
349 terms_of_use: Cow<'credential, OneOrMany<Policy>>,
350 #[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
352 evidence: Cow<'credential, OneOrMany<Evidence>>,
353 #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
356 non_transferable: Option<bool>,
357 #[serde(flatten)]
359 properties: Cow<'credential, T>,
360 #[serde(skip_serializing_if = "Option::is_none")]
362 proof: Option<Cow<'credential, Proof>>,
363}
364
365#[cfg(feature = "jpt-bbs-plus")]
366impl<'credential, T> From<CredentialJwtClaims<'credential, T>> for JptClaims
367where
368 T: ToOwned + Serialize,
369 <T as ToOwned>::Owned: DeserializeOwned,
370{
371 fn from(item: CredentialJwtClaims<'credential, T>) -> Self {
372 let CredentialJwtClaims {
373 exp,
374 iss,
375 issuance_date,
376 jti,
377 sub,
378 vc,
379 custom,
380 } = item;
381
382 let mut claims = JptClaims::new();
383
384 if let Some(exp) = exp {
385 claims.set_exp(exp);
386 }
387
388 claims.set_iss(iss.url().to_string());
389
390 if let Some(iat) = issuance_date.iat {
391 claims.set_iat(iat);
392 }
393
394 if let Some(nbf) = issuance_date.nbf {
395 claims.set_nbf(nbf);
396 }
397
398 if let Some(jti) = jti {
399 claims.set_jti(jti.to_string());
400 }
401
402 if let Some(sub) = sub {
403 claims.set_sub(sub.to_string());
404 }
405
406 claims.set_claim(Some("vc"), vc, true);
407
408 if let Some(custom) = custom {
409 claims.set_claim(None, custom, true);
410 }
411
412 claims
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use identity_core::common::Object;
419 use identity_core::convert::FromJson;
420 use identity_core::convert::ToJson;
421
422 use crate::credential::Credential;
423 use crate::Error;
424
425 use super::CredentialJwtClaims;
426
427 #[test]
428 fn roundtrip() {
429 let credential_json: &str = r#"
430 {
431 "@context": [
432 "https://www.w3.org/2018/credentials/v1",
433 "https://www.w3.org/2018/credentials/examples/v1"
434 ],
435 "id": "http://example.edu/credentials/3732",
436 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
437 "issuer": "https://example.edu/issuers/14",
438 "issuanceDate": "2010-01-01T19:23:24Z",
439 "credentialSubject": {
440 "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
441 "degree": {
442 "type": "BachelorDegree",
443 "name": "Bachelor of Science in Mechanical Engineering"
444 }
445 }
446 }"#;
447
448 let expected_serialization_json: &str = r#"
449 {
450 "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
451 "jti": "http://example.edu/credentials/3732",
452 "iss": "https://example.edu/issuers/14",
453 "nbf": 1262373804,
454 "vc": {
455 "@context": [
456 "https://www.w3.org/2018/credentials/v1",
457 "https://www.w3.org/2018/credentials/examples/v1"
458 ],
459 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
460 "credentialSubject": {
461 "degree": {
462 "type": "BachelorDegree",
463 "name": "Bachelor of Science in Mechanical Engineering"
464 }
465 }
466 }
467 }"#;
468
469 let credential: Credential = Credential::from_json(credential_json).unwrap();
470 let jwt_credential_claims: CredentialJwtClaims<'_> = CredentialJwtClaims::new(&credential, None).unwrap();
471 let jwt_credential_claims_serialized: String = jwt_credential_claims.to_json().unwrap();
472 assert_eq!(
474 Object::from_json(expected_serialization_json).unwrap(),
475 Object::from_json(&jwt_credential_claims_serialized).unwrap()
476 );
477
478 let retrieved_credential: Credential = {
480 CredentialJwtClaims::<'static, Object>::from_json(&jwt_credential_claims_serialized)
481 .unwrap()
482 .try_into_credential()
483 .unwrap()
484 };
485
486 assert_eq!(credential, retrieved_credential);
487 }
488
489 #[test]
490 fn claims_duplication() {
491 let credential_json: &str = r#"
492 {
493 "@context": [
494 "https://www.w3.org/2018/credentials/v1",
495 "https://www.w3.org/2018/credentials/examples/v1"
496 ],
497 "id": "http://example.edu/credentials/3732",
498 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
499 "issuer": "https://example.edu/issuers/14",
500 "issuanceDate": "2010-01-01T19:23:24Z",
501 "expirationDate": "2025-09-13T15:56:23Z",
502 "credentialSubject": {
503 "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
504 "degree": {
505 "type": "BachelorDegree",
506 "name": "Bachelor of Science in Mechanical Engineering"
507 }
508 }
509 }"#;
510
511 let claims_json: &str = r#"
513 {
514 "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
515 "jti": "http://example.edu/credentials/3732",
516 "iss": "https://example.edu/issuers/14",
517 "nbf": 1262373804,
518 "exp": 1757778983,
519 "vc": {
520 "@context": [
521 "https://www.w3.org/2018/credentials/v1",
522 "https://www.w3.org/2018/credentials/examples/v1"
523 ],
524 "id": "http://example.edu/credentials/3732",
525 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
526 "issuer": "https://example.edu/issuers/14",
527 "issuanceDate": "2010-01-01T19:23:24Z",
528 "expirationDate": "2025-09-13T15:56:23Z",
529 "credentialSubject": {
530 "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
531 "degree": {
532 "type": "BachelorDegree",
533 "name": "Bachelor of Science in Mechanical Engineering"
534 }
535 }
536 }
537 }"#;
538
539 let credential: Credential = Credential::from_json(credential_json).unwrap();
540 let credential_from_claims: Credential = CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
541 .unwrap()
542 .try_into_credential()
543 .unwrap();
544
545 assert_eq!(credential, credential_from_claims);
546 }
547
548 #[test]
549 fn inconsistent_issuer() {
550 let claims_json: &str = r#"
552 {
553 "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
554 "jti": "http://example.edu/credentials/3732",
555 "iss": "https://example.edu/issuers/14",
556 "nbf": 1262373804,
557 "vc": {
558 "@context": [
559 "https://www.w3.org/2018/credentials/v1",
560 "https://www.w3.org/2018/credentials/examples/v1"
561 ],
562 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
563 "issuer": "https://example.edu/issuers/15",
564 "credentialSubject": {
565 "degree": {
566 "type": "BachelorDegree",
567 "name": "Bachelor of Science in Mechanical Engineering"
568 }
569 }
570 }
571 }"#;
572
573 let credential_from_claims_result: Result<Credential, _> =
574 CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
575 .unwrap()
576 .try_into_credential();
577 assert!(matches!(
578 credential_from_claims_result.unwrap_err(),
579 Error::InconsistentCredentialJwtClaims("inconsistent issuer")
580 ));
581 }
582
583 #[test]
584 fn inconsistent_id() {
585 let claims_json: &str = r#"
586 {
587 "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
588 "jti": "http://example.edu/credentials/3732",
589 "iss": "https://example.edu/issuers/14",
590 "nbf": 1262373804,
591 "vc": {
592 "@context": [
593 "https://www.w3.org/2018/credentials/v1",
594 "https://www.w3.org/2018/credentials/examples/v1"
595 ],
596 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
597 "id": "http://example.edu/credentials/1111",
598 "credentialSubject": {
599 "degree": {
600 "type": "BachelorDegree",
601 "name": "Bachelor of Science in Mechanical Engineering"
602 }
603 }
604 }
605 }"#;
606
607 let credential_from_claims_result: Result<Credential, _> =
608 CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
609 .unwrap()
610 .try_into_credential();
611 assert!(matches!(
612 credential_from_claims_result.unwrap_err(),
613 Error::InconsistentCredentialJwtClaims("inconsistent credential id")
614 ));
615 }
616
617 #[test]
618 fn inconsistent_subject() {
619 let claims_json: &str = r#"
620 {
621 "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
622 "jti": "http://example.edu/credentials/3732",
623 "iss": "https://example.edu/issuers/14",
624 "nbf": 1262373804,
625 "vc": {
626 "@context": [
627 "https://www.w3.org/2018/credentials/v1",
628 "https://www.w3.org/2018/credentials/examples/v1"
629 ],
630 "id": "http://example.edu/credentials/3732",
631 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
632 "issuer": "https://example.edu/issuers/14",
633 "issuanceDate": "2010-01-01T19:23:24Z",
634 "credentialSubject": {
635 "id": "did:example:1111111111111111111111111",
636 "degree": {
637 "type": "BachelorDegree",
638 "name": "Bachelor of Science in Mechanical Engineering"
639 }
640 }
641 }
642 }"#;
643
644 let credential_from_claims_result: Result<Credential, _> =
645 CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
646 .unwrap()
647 .try_into_credential();
648 assert!(matches!(
649 credential_from_claims_result.unwrap_err(),
650 Error::InconsistentCredentialJwtClaims("inconsistent credentialSubject: identifiers do not match")
651 ));
652 }
653
654 #[test]
655 fn inconsistent_issuance_date() {
656 let claims_json: &str = r#"
658 {
659 "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
660 "jti": "http://example.edu/credentials/3732",
661 "iss": "https://example.edu/issuers/14",
662 "nbf": 1262373804,
663 "vc": {
664 "@context": [
665 "https://www.w3.org/2018/credentials/v1",
666 "https://www.w3.org/2018/credentials/examples/v1"
667 ],
668 "id": "http://example.edu/credentials/3732",
669 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
670 "issuer": "https://example.edu/issuers/14",
671 "issuanceDate": "2020-01-01T19:23:24Z",
672 "credentialSubject": {
673 "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
674 "degree": {
675 "type": "BachelorDegree",
676 "name": "Bachelor of Science in Mechanical Engineering"
677 }
678 }
679 }
680 }"#;
681
682 let credential_from_claims_result: Result<Credential, _> =
683 CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
684 .unwrap()
685 .try_into_credential();
686 assert!(matches!(
687 credential_from_claims_result.unwrap_err(),
688 Error::InconsistentCredentialJwtClaims("inconsistent issuanceDate")
689 ));
690 }
691
692 #[test]
693 fn inconsistent_expiration_date() {
694 let claims_json: &str = r#"
696 {
697 "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
698 "jti": "http://example.edu/credentials/3732",
699 "iss": "https://example.edu/issuers/14",
700 "nbf": 1262373804,
701 "exp": 1757778983,
702 "vc": {
703 "@context": [
704 "https://www.w3.org/2018/credentials/v1",
705 "https://www.w3.org/2018/credentials/examples/v1"
706 ],
707 "id": "http://example.edu/credentials/3732",
708 "type": ["VerifiableCredential", "UniversityDegreeCredential"],
709 "issuer": "https://example.edu/issuers/14",
710 "issuanceDate": "2010-01-01T19:23:24Z",
711 "expirationDate": "2026-09-13T15:56:23Z",
712 "credentialSubject": {
713 "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
714 "degree": {
715 "type": "BachelorDegree",
716 "name": "Bachelor of Science in Mechanical Engineering"
717 }
718 }
719 }
720 }"#;
721
722 let credential_from_claims_result: Result<Credential, _> =
723 CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
724 .unwrap()
725 .try_into_credential();
726 assert!(matches!(
727 credential_from_claims_result.unwrap_err(),
728 Error::InconsistentCredentialJwtClaims("inconsistent credential expirationDate")
729 ));
730 }
731}