1use affinidi_data_integrity::DataIntegrityProof;
5#[cfg(feature = "affinidi-signing")]
6use affinidi_data_integrity::{DataIntegrityError, verification_proof::VerificationProof};
7#[cfg(feature = "affinidi-signing")]
8use affinidi_secrets_resolver::secrets::Secret;
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize, Serializer};
11use serde_json::Value;
12use std::fmt::Display;
13use thiserror::Error;
14
15pub mod create;
16
17#[derive(Clone, Copy, Debug)]
19pub enum W3CVCVersion {
20 V1_1,
22
23 V2_0,
25}
26
27impl TryFrom<&[String]> for W3CVCVersion {
28 type Error = DTGCredentialError;
29
30 fn try_from(types: &[String]) -> Result<Self, Self::Error> {
32 if types.contains(&"https://www.w3.org/2018/credentials/v1".to_string()) {
33 Ok(W3CVCVersion::V1_1)
34 } else if types.contains(&"https://www.w3.org/ns/credentials/v2".to_string()) {
35 Ok(W3CVCVersion::V2_0)
36 } else {
37 Err(DTGCredentialError::UnknownVCVersion)
38 }
39 }
40}
41
42#[derive(Error, Debug)]
44pub enum DTGCredentialError {
45 #[error("Unknown credential type")]
46 UnknownCredential,
47
48 #[cfg(feature = "affinidi-signing")]
49 #[error("Data Integrity Error: {0}")]
50 DataIntegrity(#[from] DataIntegrityError),
51
52 #[error("Credential is not signed")]
53 NotSigned,
54
55 #[error("Unknown W3C VC Version")]
56 UnknownVCVersion,
57}
58
59#[derive(Serialize, Deserialize, Debug, Clone)]
61#[serde(try_from = "DTGCommon")]
62pub struct DTGCredential {
63 #[serde(flatten)]
65 credential: DTGCommon,
66
67 #[serde(skip)]
69 type_: DTGCredentialType,
70
71 #[serde(skip)]
73 version: W3CVCVersion,
74}
75
76impl DTGCredential {
77 pub fn credential(&self) -> &DTGCommon {
79 &self.credential
80 }
81
82 pub fn credential_mut(&mut self) -> &mut DTGCommon {
84 &mut self.credential
85 }
86
87 pub fn signed(&self) -> bool {
89 self.credential.signed()
90 }
91
92 pub fn type_(&self) -> DTGCredentialType {
94 self.type_.clone()
95 }
96
97 pub fn issuer(&self) -> &str {
99 self.credential.issuer()
100 }
101
102 pub fn subject(&self) -> &str {
104 self.credential.subject()
105 }
106
107 pub fn valid_from(&self) -> DateTime<Utc> {
109 self.credential.valid_from()
110 }
111
112 pub fn valid_until(&self) -> Option<DateTime<Utc>> {
114 self.credential.valid_until()
115 }
116
117 pub fn proof_value(&self) -> Option<&str> {
119 if let Some(proof) = &self.credential.proof {
120 proof.proof_value.as_deref()
121 } else {
122 None
123 }
124 }
125
126 #[cfg(feature = "affinidi-signing")]
127 pub async fn sign(
131 &mut self,
132 signing_secret: &Secret,
133 create_time: Option<DateTime<Utc>>,
134 ) -> Result<DataIntegrityProof, DTGCredentialError> {
135 let proof = DataIntegrityProof::sign_jcs_data(
136 self,
137 None,
138 signing_secret,
139 create_time.map(|ts| ts.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
140 )
141 .await?;
142
143 self.credential.proof = Some(proof.clone());
144 Ok(proof)
145 }
146
147 #[cfg(feature = "affinidi-signing")]
148 pub fn verify_proof_with_public_key(
152 &self,
153 public_key_bytes: &[u8],
154 ) -> Result<VerificationProof, DTGCredentialError> {
155 let proof = if let Some(proof) = &self.credential.proof {
156 proof.clone()
157 } else {
158 use tracing::warn;
159
160 warn!("Trying to verify a DTG Credential that has no proof");
161 return Err(DTGCredentialError::NotSigned);
162 };
163
164 let unsigned = DTGCommon {
165 proof: None,
166 ..self.credential.clone()
167 };
168
169 Ok(
170 affinidi_data_integrity::verification_proof::verify_data_with_public_key(
171 &unsigned,
172 None,
173 &proof,
174 public_key_bytes,
175 )?,
176 )
177 }
178
179 pub fn get_w3c_vc_version(&self) -> W3CVCVersion {
181 self.version
182 }
183
184 pub fn is_personhood_credential(&self) -> bool {
186 if let DTGCredentialType::Membership = self.type_ {
187 self.credential
188 .type_
189 .contains(&"PersonhoodCredential".to_string())
190 } else {
191 false
192 }
193 }
194}
195
196#[derive(Debug, Clone)]
198#[non_exhaustive]
199pub enum DTGCredentialType {
200 Membership,
201 Relationship,
202 Invitation,
203 Persona,
204 Endorsement,
205 Witness,
206 RCard,
207}
208
209impl Display for DTGCredentialType {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 match self {
212 DTGCredentialType::Membership => write!(f, "MembershipCredential"),
213 DTGCredentialType::Relationship => write!(f, "RelationshipCredential"),
214 DTGCredentialType::Invitation => write!(f, "InvitationCredential"),
215 DTGCredentialType::Persona => write!(f, "PersonaCredential"),
216 DTGCredentialType::Endorsement => write!(f, "EndorsementCredential"),
217 DTGCredentialType::Witness => write!(f, "WitnessCredential"),
218 DTGCredentialType::RCard => write!(f, "RCardCredential"),
219 }
220 }
221}
222
223const DTG_TYPES: [&str; 7] = [
225 "MembershipCredential",
226 "RelationshipCredential",
227 "InvitationCredential",
228 "PersonaCredential",
229 "EndorsementCredential",
230 "WitnessCredential",
231 "RCardCredential",
232];
233
234impl TryFrom<&[String]> for DTGCredentialType {
235 type Error = DTGCredentialError;
236
237 fn try_from(types: &[String]) -> Result<Self, Self::Error> {
238 if let Some(type_) = DTG_TYPES.iter().find(|t| types.contains(&t.to_string())) {
239 match *type_ {
240 "MembershipCredential" => Ok(DTGCredentialType::Membership),
241 "RelationshipCredential" => Ok(DTGCredentialType::Relationship),
242 "InvitationCredential" => Ok(DTGCredentialType::Invitation),
243 "PersonaCredential" => Ok(DTGCredentialType::Persona),
244 "EndorsementCredential" => Ok(DTGCredentialType::Endorsement),
245 "WitnessCredential" => Ok(DTGCredentialType::Witness),
246 "RCardCredential" => Ok(DTGCredentialType::RCard),
247 _ => Err(DTGCredentialError::UnknownCredential),
248 }
249 } else {
250 Err(DTGCredentialError::UnknownCredential)
251 }
252 }
253}
254
255#[derive(Serialize, Deserialize, Debug, Clone)]
257#[serde(rename_all = "camelCase")]
258pub struct DTGCommon {
259 #[serde(rename = "@context")]
264 pub context: Vec<String>,
265
266 #[serde(rename = "type")]
271 pub type_: Vec<String>,
272
273 pub issuer: String,
275
276 #[serde(serialize_with = "iso8601_format", alias = "issuanceDate")]
278 pub valid_from: DateTime<Utc>,
279
280 #[serde(serialize_with = "iso8601_format_option")]
282 #[serde(
283 skip_serializing_if = "Option::is_none",
284 alias = "expirationDate",
285 default
286 )]
287 pub valid_until: Option<DateTime<Utc>>,
288
289 pub credential_subject: CredentialSubject,
291
292 #[serde(skip_serializing_if = "Option::is_none", default)]
294 pub proof: Option<DataIntegrityProof>,
295}
296
297impl DTGCommon {
298 pub fn signed(&self) -> bool {
302 self.proof.is_some()
303 }
304
305 pub fn issuer(&self) -> &str {
307 &self.issuer
308 }
309
310 pub fn subject(&self) -> &str {
312 match &self.credential_subject {
313 CredentialSubject::Basic(subject) => &subject.id,
314 CredentialSubject::Endorsement(subject) => &subject.id,
315 CredentialSubject::Witness(subject) => &subject.id,
316 CredentialSubject::RCard(subject) => &subject.id,
317 }
318 }
319
320 pub fn valid_from(&self) -> DateTime<Utc> {
322 self.valid_from
323 }
324
325 pub fn valid_until(&self) -> Option<DateTime<Utc>> {
327 self.valid_until
328 }
329}
330
331impl Default for DTGCommon {
333 fn default() -> Self {
334 DTGCommon {
335 context: vec![
336 "https://www.w3.org/ns/credentials/v2".to_string(),
337 "https://firstperson.network/credentials/dtg/v1".to_string(),
338 ],
339 type_: vec![
340 "VerifiableCredential".to_string(),
341 "DTGCredential".to_string(),
342 ],
343 issuer: String::new(),
344 valid_from: Utc::now(),
345 valid_until: None,
346 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic {
347 id: String::new(),
348 }),
349 proof: None,
350 }
351 }
352}
353
354impl TryFrom<DTGCommon> for DTGCredential {
356 type Error = DTGCredentialError;
357
358 fn try_from(value: DTGCommon) -> Result<Self, Self::Error> {
359 match &value.type_.as_slice().try_into()? {
360 DTGCredentialType::Membership => Ok(DTGCredential {
361 type_: DTGCredentialType::Membership,
362 version: value.context.as_slice().try_into()?,
363 credential: value,
364 }),
365 DTGCredentialType::Relationship => Ok(DTGCredential {
366 type_: DTGCredentialType::Relationship,
367 version: value.context.as_slice().try_into()?,
368 credential: value,
369 }),
370 DTGCredentialType::Invitation => Ok(DTGCredential {
371 type_: DTGCredentialType::Invitation,
372 version: value.context.as_slice().try_into()?,
373 credential: value,
374 }),
375 DTGCredentialType::Persona => Ok(DTGCredential {
376 type_: DTGCredentialType::Persona,
377 version: value.context.as_slice().try_into()?,
378 credential: value,
379 }),
380 DTGCredentialType::Endorsement => {
381 if let CredentialSubject::Endorsement { .. } = &value.credential_subject {
382 Ok(DTGCredential {
383 type_: DTGCredentialType::Endorsement,
384 version: value.context.as_slice().try_into()?,
385 credential: value,
386 })
387 } else {
388 Err(DTGCredentialError::UnknownCredential)
389 }
390 }
391 DTGCredentialType::Witness => match &value.credential_subject {
392 CredentialSubject::Witness(_) => Ok(DTGCredential {
393 type_: DTGCredentialType::Witness,
394 version: value.context.as_slice().try_into()?,
395 credential: value,
396 }),
397 CredentialSubject::Basic(subject) => {
398 Ok(DTGCredential {
400 type_: DTGCredentialType::Witness,
401 version: value.context.as_slice().try_into()?,
402 credential: DTGCommon {
403 credential_subject: CredentialSubject::Witness(
404 CredentialSubjectWitness {
405 id: subject.id.clone(),
406 digest: None,
407 witness_context: None,
408 },
409 ),
410 ..value
411 },
412 })
413 }
414 _ => Err(DTGCredentialError::UnknownCredential),
415 },
416 DTGCredentialType::RCard => match &value.credential_subject {
417 CredentialSubject::RCard { .. } => Ok(DTGCredential {
418 type_: DTGCredentialType::RCard,
419 version: value.context.as_slice().try_into()?,
420 credential: value,
421 }),
422 _ => Err(DTGCredentialError::UnknownCredential),
423 },
424 }
425 }
426}
427
428fn iso8601_format<S>(timestamp: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
431where
432 S: Serializer,
433{
434 s.serialize_str(
435 timestamp
436 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
437 .as_str(),
438 )
439}
440
441fn iso8601_format_option<S>(timestamp: &Option<DateTime<Utc>>, s: S) -> Result<S::Ok, S::Error>
442where
443 S: Serializer,
444{
445 if let Some(timestamp) = timestamp {
446 s.serialize_str(
447 timestamp
448 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
449 .as_str(),
450 )
451 } else {
452 s.serialize_none()
453 }
454}
455
456#[derive(Serialize, Deserialize, Debug, Clone)]
465#[serde(untagged)]
466pub enum CredentialSubject {
467 Endorsement(CredentialSubjectEndorsement),
469
470 RCard(CredentialSubjectRCard),
472
473 Basic(CredentialSubjectBasic),
476
477 Witness(CredentialSubjectWitness),
479}
480
481#[derive(Serialize, Deserialize, Debug, Clone)]
483#[serde(deny_unknown_fields)]
484pub struct CredentialSubjectBasic {
485 pub id: String,
486}
487
488#[derive(Serialize, Deserialize, Debug, Clone)]
490#[serde(deny_unknown_fields)]
491pub struct CredentialSubjectEndorsement {
492 pub id: String,
493 pub endorsement: Value,
495}
496
497#[derive(Serialize, Deserialize, Debug, Clone)]
499#[serde(rename_all = "camelCase", deny_unknown_fields)]
500pub struct CredentialSubjectWitness {
501 pub id: String,
502
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub digest: Option<String>,
505
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub witness_context: Option<WitnessContext>,
509}
510
511#[derive(Serialize, Deserialize, Debug, Clone)]
513#[serde(rename_all = "camelCase", deny_unknown_fields)]
514pub struct WitnessContext {
515 pub event: Option<String>,
517
518 pub session_id: Option<String>,
520
521 pub method: Option<String>,
523}
524
525#[derive(Serialize, Deserialize, Debug, Clone)]
527#[serde(deny_unknown_fields)]
528pub struct CredentialSubjectRCard {
529 pub id: String,
530
531 pub card: Value,
533}
534
535#[cfg(test)]
536mod tests {
537 use crate::{
538 CredentialSubject, CredentialSubjectRCard, DTGCommon, DTGCredential, DTGCredentialType,
539 W3CVCVersion,
540 };
541 use chrono::{DateTime, Utc};
542 use serde_json::Value;
543
544 #[test]
545 fn test_vmc_vc_1_deserialize() {
546 let vmc: DTGCredential = match serde_json::from_str(
548 r#"{
549"@context": [
550 "https://www.w3.org/2018/credentials/v1",
551 "https://firstperson.network/credentials/dtg/v1",
552 "https://w3id.org/security/suites/ed25519-2020/v1"
553 ],
554 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
555 "issuer": "did:web:chess-club.example",
556 "issuanceDate": "2026-01-06T10:00:00Z",
557 "expirationDate": "2027-01-06T10:00:00Z",
558 "credentialSubject": {
559 "id": "did:key:z6MkpTHR8VNs..."
560 }
561 }"#,
562 ) {
563 Ok(vmc) => vmc,
564 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
565 };
566
567 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
568 assert!(matches!(
569 vmc.credential().credential_subject,
570 CredentialSubject::Basic(_)
571 ));
572 assert!(matches!(vmc.version, W3CVCVersion::V1_1));
573 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V1_1));
574 }
575
576 #[test]
577 fn test_missing_w3c_context() {
578 assert!(
580 serde_json::from_str::<DTGCredential>(
581 r#"{
582"@context": [
583 "https://firstperson.network/credentials/dtg/v1",
584 "https://w3id.org/security/suites/ed25519-2020/v1"
585 ],
586 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
587 "issuer": "did:web:chess-club.example",
588 "issuanceDate": "2026-01-06T10:00:00Z",
589 "expirationDate": "2027-01-06T10:00:00Z",
590 "credentialSubject": {
591 "id": "did:key:z6MkpTHR8VNs..."
592 }
593 }"#,
594 )
595 .is_err()
596 );
597 }
598
599 #[test]
600 fn test_mutable_credential() {
601 let mut vmc = DTGCredential::new_vmc(
602 "did:example:issuer".to_string(),
603 "did:example:subject".to_string(),
604 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
605 .unwrap()
606 .with_timezone(&Utc),
607 None,
608 false,
609 );
610
611 let cred = vmc.credential_mut();
612 cred.type_.push("PersonhoodCredential".to_string());
613 assert!(vmc.is_personhood_credential());
614 }
615
616 #[test]
617 fn test_vmc_deserialize() {
618 let vmc: DTGCredential = match serde_json::from_str(
619 r#"{
620 "@context": ["https://www.w3.org/ns/credentials/v2"],
621 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
622 "issuer": "did:example:community",
623 "validFrom": "2024-06-18T10:00:00Z",
624 "credentialSubject": { "id": "did:example:rDid" }
625 }"#,
626 ) {
627 Ok(vmc) => vmc,
628 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
629 };
630
631 assert!(!vmc.is_personhood_credential());
632 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
633 assert!(matches!(
634 vmc.credential().credential_subject,
635 CredentialSubject::Basic(_)
636 ));
637 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V2_0));
638 }
639
640 #[test]
641 fn test_vmc_phc_deserialize() {
642 let vmc: DTGCredential = match serde_json::from_str(
643 r#"{
644 "@context": ["https://www.w3.org/ns/credentials/v2"],
645 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential", "PersonhoodCredential"],
646 "issuer": "did:example:community",
647 "validFrom": "2024-06-18T10:00:00Z",
648 "credentialSubject": { "id": "did:example:rDid" }
649 }"#,
650 ) {
651 Ok(vmc) => vmc,
652 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
653 };
654
655 assert!(vmc.is_personhood_credential());
656 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
657 assert!(matches!(
658 vmc.credential().credential_subject,
659 CredentialSubject::Basic(_)
660 ));
661 }
662
663 #[test]
664 fn test_vrc_deserialize() {
665 let vrc: DTGCredential = match serde_json::from_str(
666 r#"{
667 "@context": ["https://www.w3.org/ns/credentials/v2"],
668 "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"],
669 "issuer": "did:example:governmentAgencyDid",
670 "validFrom": "2024-06-18T10:00:00Z",
671 "credentialSubject": { "id": "did:example:citizenRDid" }
672 }"#,
673 ) {
674 Ok(vrc) => vrc,
675 Err(e) => panic!("Couldn't deserialize VRC: {}", e),
676 };
677
678 assert!(matches!(vrc.type_, DTGCredentialType::Relationship));
679 assert!(matches!(
680 vrc.credential().credential_subject,
681 CredentialSubject::Basic(_)
682 ));
683 }
684
685 #[test]
686 fn test_vic_deserialize() {
687 let vic: DTGCredential = match serde_json::from_str(
688 r#"{
689 "@context": ["https://www.w3.org/ns/credentials/v2"],
690 "type": ["VerifiableCredential", "DTGCredential", "InvitationCredential"],
691 "issuer": "did:example:governmentAgencyVicDid",
692 "validFrom": "2024-06-18T10:00:00Z",
693 "credentialSubject": { "id": "did:example:citizenRDid" }
694 }"#,
695 ) {
696 Ok(vic) => vic,
697 Err(e) => panic!("Couldn't deserialize VIC: {}", e),
698 };
699
700 assert!(!vic.is_personhood_credential());
701 assert!(matches!(vic.type_, DTGCredentialType::Invitation));
702 assert!(matches!(
703 vic.credential().credential_subject,
704 CredentialSubject::Basic(_)
705 ));
706 }
707
708 #[test]
709 fn test_vpc_deserialize() {
710 let vpc: DTGCredential = match serde_json::from_str(
711 r#"{
712 "@context": ["https://www.w3.org/ns/credentials/v2"],
713 "type": ["VerifiableCredential", "DTGCredential", "PersonaCredential"],
714 "issuer": "did:example:governmentAgencyDid",
715 "validFrom": "2024-06-18T10:00:00Z",
716 "credentialSubject": { "id": "did:example:citizenRDid" }
717 }"#,
718 ) {
719 Ok(vpc) => vpc,
720 Err(e) => panic!("Couldn't deserialize VPC: {}", e),
721 };
722
723 assert!(matches!(vpc.type_, DTGCredentialType::Persona));
724 assert!(matches!(
725 vpc.credential().credential_subject,
726 CredentialSubject::Basic(_)
727 ));
728 }
729
730 #[test]
731 fn test_vec_deserialize() {
732 let vec: DTGCredential = match serde_json::from_str(
733 r#"{
734 "@context": ["https://www.w3.org/ns/credentials/v2"],
735 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
736 "issuer": "did:example:governmentAgencyDid",
737 "validFrom": "2024-06-18T10:00:00Z",
738 "credentialSubject": { "id": "did:example:citizenRDid", "endorsement": {} }
739 }"#,
740 ) {
741 Ok(vec) => vec,
742 Err(e) => panic!("Couldn't deserialize VEC: {}", e),
743 };
744
745 assert!(matches!(vec.type_, DTGCredentialType::Endorsement));
746 assert!(matches!(vec.subject(), "did:example:citizenRDid"));
747 assert!(matches!(
748 vec.credential().credential_subject,
749 CredentialSubject::Endorsement(_)
750 ));
751 }
752
753 #[test]
754 fn test_vec_bad_deserialize() {
755 match serde_json::from_str::<DTGCredential>(
756 r#"{
757 "@context": ["https://www.w3.org/ns/credentials/v2"],
758 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
759 "issuer": "did:example:governmentAgencyDid",
760 "validFrom": "2024-06-18T10:00:00Z",
761 "credentialSubject": { "id": "did:example:citizenRDid", "other": [] }
762 }"#,
763 ) {
764 Ok(_) => panic!("Expected Unknown Credential type"),
765 Err(_) => {
766 }
768 };
769 }
770
771 #[test]
772 fn test_vwc_simple_deserialize() {
773 let vwc: DTGCredential = match serde_json::from_str(
774 r#"{
775 "@context": ["https://www.w3.org/ns/credentials/v2"],
776 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
777 "issuer": "did:example:governmentAgencyDid",
778 "validFrom": "2024-06-18T10:00:00Z",
779 "credentialSubject": { "id": "did:example:citizenRDid" }
780 }"#,
781 ) {
782 Ok(vwc) => vwc,
783 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
784 };
785
786 assert!(matches!(vwc.type_, DTGCredentialType::Witness));
787 assert!(matches!(vwc.subject(), "did:example:citizenRDid"));
788 assert!(matches!(
789 vwc.credential().credential_subject,
790 CredentialSubject::Witness(_)
791 ));
792 }
793
794 #[test]
795 fn test_vwc_full_deserialize() {
796 let vwc: DTGCredential = match serde_json::from_str(
797 r#"{
798 "@context": ["https://www.w3.org/ns/credentials/v2"],
799 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
800 "issuer": "did:example:governmentAgencyDid",
801 "validFrom": "2024-06-18T10:00:00Z",
802 "credentialSubject": { "id": "did:example:citizenRDid", "digest": "abcdf", "witnessContext": {} }
803 }"#,
804 ) {
805 Ok(vwc) => vwc,
806 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
807 };
808
809 assert!(matches!(vwc.type_(), DTGCredentialType::Witness));
810 assert!(matches!(
811 vwc.credential().credential_subject,
812 CredentialSubject::Witness(_)
813 ));
814 }
815
816 #[test]
817 fn test_vwc_bad_deserialize() {
818 if serde_json::from_str::<DTGCredential>(
819 r#"{
820 "@context": ["https://www.w3.org/ns/credentials/v2"],
821 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
822 "issuer": "did:example:governmentAgencyDid",
823 "validFrom": "2024-06-18T10:00:00Z",
824 "credentialSubject": { "id": "did:example:citizenRDid", "digest": "abcdf", "wrongContext": {} }
825 }"#,
826 ).is_ok() {
827 panic!("Should have failed due to wrong CredentialSubject!");
828 }
829 }
830
831 #[test]
832 fn test_rcard_simple_deserialize() {
833 let rcard: DTGCredential = match serde_json::from_str(
834 r#"{
835 "@context": ["https://www.w3.org/ns/credentials/v2"],
836 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
837 "issuer": "did:example:governmentAgencyDid",
838 "validFrom": "2024-06-18T10:00:00Z",
839 "credentialSubject": { "id": "did:example:citizenRDid", "card": [] }
840 }"#,
841 ) {
842 Ok(rcard) => rcard,
843 Err(e) => panic!("Couldn't deserialize R-Card: {}", e),
844 };
845
846 assert!(matches!(rcard.type_(), DTGCredentialType::RCard));
847 assert!(matches!(rcard.subject(), "did:example:citizenRDid"));
848 assert!(matches!(
849 rcard.credential().credential_subject,
850 CredentialSubject::RCard(_)
851 ));
852 }
853
854 #[test]
855 fn test_rcard_bad_deserialize() {
856 if serde_json::from_str::<DTGCredential>(
857 r#"{
858 "@context": ["https://www.w3.org/ns/credentials/v2"],
859 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
860 "issuer": "did:example:governmentAgencyDid",
861 "validFrom": "2024-06-18T10:00:00Z",
862 "credentialSubject": { "id": "did:example:citizenRDid" }
863 }"#,
864 )
865 .is_ok()
866 {
867 panic!("Should have failed due to wrong CredentialSubject!");
868 }
869 }
870 #[test]
871 fn test_deserialize_unknown() {
872 match serde_json::from_str::<DTGCredential>(
873 r#"{
874 "@context": ["https://www.w3.org/ns/credentials/v2"],
875 "type": ["VerifiableCredential", "DTGCredential", "UnknownCredential"],
876 "issuer": "did:example:governmentAgencyDid",
877 "validFrom": "2024-06-18T10:00:00Z",
878 "credentialSubject": { "id": "did:example:citizenRDid" }
879 }"#,
880 ) {
881 Ok(_) => panic!("Expected Unknown Credential type"),
882 Err(e) => {
883 if e.to_string() == "Unknown credential type" {
884 } else {
886 panic!("Wrong error type returned");
887 }
888 }
889 };
890 }
891
892 #[test]
893 fn test_deserialize_mismatched_credential_subject() {
894 match serde_json::from_str::<DTGCredential>(
895 r#"{
896 "@context": ["https://www.w3.org/ns/credentials/v2"],
897 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
898 "issuer": "did:example:governmentAgencyDid",
899 "validFrom": "2024-06-18T10:00:00Z",
900 "credentialSubject": { "id": "did:example:citizenRDid" }
901 }"#,
902 ) {
903 Ok(_) => panic!("Expected Unknown Credential type"),
904 Err(e) => {
905 if e.to_string() == "Unknown credential type" {
906 } else {
908 panic!("Wrong error type returned");
909 }
910 }
911 };
912 }
913
914 #[test]
915 fn test_proof_signed() {
916 let cred: DTGCredential = match serde_json::from_str(
917 r#"{
918 "@context": ["https://www.w3.org/ns/credentials/v2"],
919 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
920 "issuer": "did:example:community",
921 "validFrom": "2024-06-18T10:00:00Z",
922 "credentialSubject": { "id": "did:example:rDid" },
923 "proof": {
924 "type": "DataIntegrityProof",
925 "cryptosuite": "eddsa-jcs-2022",
926 "created": "2025-12-04T00:00:00",
927 "verificationMethod": "did:example:test#key-1",
928 "proofPurpose": "assertionMethod",
929 "proofValue": "abcd"
930 }
931 }"#,
932 ) {
933 Ok(vmc) => vmc,
934 Err(e) => panic!("Couldn't deserialize credential: {}", e),
935 };
936
937 assert!(cred.signed());
938 assert!(cred.proof_value().is_some());
939 }
940
941 #[test]
942 fn test_proof_not_signed() {
943 let cred: DTGCredential = match serde_json::from_str(
944 r#"{
945 "@context": ["https://www.w3.org/ns/credentials/v2"],
946 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
947 "issuer": "did:example:community",
948 "validFrom": "2024-06-18T10:00:00Z",
949 "credentialSubject": { "id": "did:example:rDid" }
950 }"#,
951 ) {
952 Ok(vmc) => vmc,
953 Err(e) => panic!("Couldn't deserialize credential: {}", e),
954 };
955
956 assert!(!cred.signed());
957 assert!(cred.proof_value().is_none());
958 }
959
960 #[test]
961 fn test_helpers() {
962 let cred: DTGCredential = match serde_json::from_str(
963 r#"{
964 "@context": ["https://www.w3.org/ns/credentials/v2"],
965 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
966 "issuer": "did:example:issuer",
967 "validFrom": "2024-06-18T00:00:00Z",
968 "credentialSubject": { "id": "did:example:subject" }
969 }"#,
970 ) {
971 Ok(vmc) => vmc,
972 Err(e) => panic!("Couldn't deserialize credential: {}", e),
973 };
974
975 assert_eq!(cred.issuer(), "did:example:issuer");
976 assert_eq!(cred.subject(), "did:example:subject");
977 assert_eq!(
978 cred.valid_from()
979 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
980 "2024-06-18T00:00:00Z"
981 );
982 assert_eq!(cred.valid_until(), None);
983 }
984
985 #[test]
986 fn test_valid_until() {
987 let cred: DTGCredential = match serde_json::from_str(
988 r#"{
989 "@context": ["https://www.w3.org/ns/credentials/v2"],
990 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
991 "issuer": "did:example:issuer",
992 "validFrom": "2024-06-18T00:00:00Z",
993 "validUntil": "2030-01-01T00:00:00Z",
994 "credentialSubject": { "id": "did:example:subject" }
995 }"#,
996 ) {
997 Ok(vmc) => vmc,
998 Err(e) => panic!("Couldn't deserialize credential: {}", e),
999 };
1000
1001 assert_eq!(
1002 cred.valid_until()
1003 .unwrap()
1004 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1005 "2030-01-01T00:00:00Z"
1006 );
1007 }
1008
1009 #[test]
1010 fn test_bad_type() {
1011 assert!(
1012 std::convert::TryInto::<DTGCredentialType>::try_into(
1013 vec!["bad_type".to_string()].as_slice(),
1014 )
1015 .is_err()
1016 );
1017 }
1018
1019 #[test]
1020 fn test_badly_constructed_vwc() {
1021 let mut cred = DTGCommon::default();
1022 cred.type_.push("WitnessCredential".to_string());
1023 cred.credential_subject = CredentialSubject::RCard(CredentialSubjectRCard {
1024 id: "did:example:bad".to_string(),
1025 card: Value::Null,
1026 });
1027
1028 assert!(std::convert::TryInto::<DTGCredential>::try_into(cred).is_err());
1029 }
1030
1031 #[test]
1032 fn test_iso8601_format_option() {
1033 let now: DateTime<Utc> = DateTime::parse_from_rfc3339(
1034 &Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1035 )
1036 .unwrap()
1037 .to_utc();
1038 let cred = DTGCommon {
1039 valid_until: Some(now),
1040 ..Default::default()
1041 };
1042
1043 let value = serde_json::to_value(&cred).unwrap();
1044 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
1045 assert_eq!(cred2.valid_until, Some(now));
1046
1047 let cred = DTGCommon::default();
1048 let value = serde_json::to_value(&cred).unwrap();
1049 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
1050 assert_eq!(cred2.valid_until, None);
1051 }
1052
1053 #[cfg(feature = "affinidi-signing")]
1054 #[tokio::test]
1055 async fn test_signing() {
1056 use affinidi_secrets_resolver::secrets::Secret;
1057
1058 let secret = Secret::generate_ed25519(None, None);
1059
1060 let mut cred = DTGCredential::new_vrc(
1061 "did:example:issuer".to_string(),
1062 "did:example:subject".to_string(),
1063 Utc::now(),
1064 None,
1065 );
1066
1067 assert!(cred.sign(&secret, Some(Utc::now())).await.is_ok());
1068
1069 assert!(
1070 cred.verify_proof_with_public_key(secret.get_public_bytes())
1071 .is_ok()
1072 );
1073
1074 let secret2 = Secret::generate_ed25519(None, None);
1075 assert!(
1076 cred.verify_proof_with_public_key(secret2.get_public_bytes())
1077 .is_err()
1078 );
1079 }
1080
1081 #[cfg(feature = "affinidi-signing")]
1082 #[tokio::test]
1083 async fn test_signing_error() {
1084 use affinidi_secrets_resolver::secrets::Secret;
1085
1086 let secret = Secret::generate_x25519(None, None).unwrap();
1087
1088 let mut cred = DTGCredential::new_vrc(
1089 "did:example:issuer".to_string(),
1090 "did:example:subject".to_string(),
1091 Utc::now(),
1092 None,
1093 );
1094
1095 assert!(cred.sign(&secret, Some(Utc::now())).await.is_err());
1096 }
1097
1098 #[cfg(feature = "affinidi-signing")]
1099 #[test]
1100 fn test_signing_no_proof() {
1101 use crate::DTGCredentialError;
1102 use affinidi_secrets_resolver::secrets::Secret;
1103
1104 let cred = DTGCredential::new_vrc(
1105 "did:example:issuer".to_string(),
1106 "did:example:subject".to_string(),
1107 Utc::now(),
1108 None,
1109 );
1110
1111 let secret = Secret::generate_ed25519(None, None);
1112 match cred.verify_proof_with_public_key(secret.get_public_bytes()) {
1113 Err(DTGCredentialError::NotSigned) => {
1114 }
1116 _ => panic!("Expected NotSigned error!"),
1117 }
1118 }
1119}