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