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 multibase::Base;
11use serde::{Deserialize, Serialize, Serializer};
12use serde_json::Value;
13use sha2::{Digest, Sha256};
14use std::fmt::Display;
15use thiserror::Error;
16
17pub mod create;
18
19#[derive(Clone, Copy, Debug)]
21pub enum W3CVCVersion {
22 V1_1,
24
25 V2_0,
27}
28
29impl TryFrom<&[String]> for W3CVCVersion {
30 type Error = DTGCredentialError;
31
32 fn try_from(types: &[String]) -> Result<Self, Self::Error> {
34 if types.contains(&"https://www.w3.org/2018/credentials/v1".to_string()) {
35 Ok(W3CVCVersion::V1_1)
36 } else if types.contains(&"https://www.w3.org/ns/credentials/v2".to_string()) {
37 Ok(W3CVCVersion::V2_0)
38 } else {
39 Err(DTGCredentialError::UnknownVCVersion)
40 }
41 }
42}
43
44#[derive(Error, Debug)]
46pub enum DTGCredentialError {
47 #[error("Unknown credential type")]
48 UnknownCredential,
49
50 #[cfg(feature = "affinidi-signing")]
51 #[error("Data Integrity Error: {0}")]
52 DataIntegrity(#[from] DataIntegrityError),
53
54 #[error("Credential is not signed")]
55 NotSigned,
56
57 #[error("Unknown W3C VC Version")]
58 UnknownVCVersion,
59
60 #[error("WitnessCredential is missing the required taskContext property")]
62 MissingTaskContext,
63
64 #[error("Could not canonicalize credential: {0}")]
66 Canonicalization(String),
67}
68
69#[derive(Serialize, Deserialize, Debug, Clone)]
71#[serde(try_from = "DTGCommon")]
72pub struct DTGCredential {
73 #[serde(flatten)]
75 credential: DTGCommon,
76
77 #[serde(skip)]
79 type_: DTGCredentialType,
80
81 #[serde(skip)]
83 version: W3CVCVersion,
84}
85
86impl DTGCredential {
87 pub fn credential(&self) -> &DTGCommon {
89 &self.credential
90 }
91
92 pub fn credential_mut(&mut self) -> &mut DTGCommon {
94 &mut self.credential
95 }
96
97 pub fn signed(&self) -> bool {
99 self.credential.signed()
100 }
101
102 pub fn type_(&self) -> DTGCredentialType {
104 self.type_.clone()
105 }
106
107 pub fn id(&self) -> Option<&str> {
113 self.credential.id()
114 }
115
116 pub fn issuer(&self) -> &str {
118 self.credential.issuer()
119 }
120
121 pub fn subject(&self) -> &str {
123 self.credential.subject()
124 }
125
126 pub fn valid_from(&self) -> DateTime<Utc> {
128 self.credential.valid_from()
129 }
130
131 pub fn valid_until(&self) -> Option<DateTime<Utc>> {
133 self.credential.valid_until()
134 }
135
136 pub fn task_context(&self) -> Option<&str> {
141 self.credential.task_context()
142 }
143
144 pub fn digest_multibase(&self) -> Result<String, DTGCredentialError> {
167 let canonical = serde_json_canonicalizer::to_vec(&self.credential)
168 .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
169
170 let mut multihash = Vec::with_capacity(34);
172 multihash.extend_from_slice(&[0x12, 0x20]);
173 multihash.extend_from_slice(&Sha256::digest(&canonical));
174
175 Ok(multibase::encode(Base::Base58Btc, &multihash))
176 }
177
178 pub fn verify_digest(&self, witnessed: &DTGCredential) -> Result<bool, DTGCredentialError> {
190 let CredentialSubject::Witness(subject) = &self.credential.credential_subject else {
191 return Ok(false);
192 };
193
194 let Some(digest) = &subject.digest else {
195 return Ok(false);
196 };
197
198 Ok(*digest == witnessed.digest_multibase()?)
199 }
200
201 pub fn proof_value(&self) -> Option<&str> {
203 if let Some(proof) = &self.credential.proof {
204 proof.proof_value.as_deref()
205 } else {
206 None
207 }
208 }
209
210 #[cfg(feature = "affinidi-signing")]
211 pub async fn sign(
215 &mut self,
216 signing_secret: &Secret,
217 create_time: Option<DateTime<Utc>>,
218 ) -> Result<DataIntegrityProof, DTGCredentialError> {
219 let mut options = SignOptions::new();
220 if let Some(ts) = create_time {
221 options = options.with_created(ts);
222 }
223
224 let proof = DataIntegrityProof::sign(self, signing_secret, options).await?;
225
226 self.credential.proof = Some(proof.clone());
227 Ok(proof)
228 }
229
230 #[cfg(feature = "affinidi-signing")]
231 pub fn verify_proof_with_public_key(
235 &self,
236 public_key_bytes: &[u8],
237 ) -> Result<(), DTGCredentialError> {
238 let proof = if let Some(proof) = &self.credential.proof {
239 proof.clone()
240 } else {
241 use tracing::warn;
242
243 warn!("Trying to verify a DTG Credential that has no proof");
244 return Err(DTGCredentialError::NotSigned);
245 };
246
247 let unsigned = DTGCommon {
248 proof: None,
249 ..self.credential.clone()
250 };
251
252 proof.verify_with_public_key(&unsigned, public_key_bytes, VerifyOptions::new())?;
253 Ok(())
254 }
255
256 pub fn get_w3c_vc_version(&self) -> W3CVCVersion {
258 self.version
259 }
260
261 pub fn is_personhood_credential(&self) -> bool {
263 if let DTGCredentialType::Membership = self.type_ {
264 self.credential
265 .type_
266 .contains(&"PersonhoodCredential".to_string())
267 } else {
268 false
269 }
270 }
271}
272
273#[derive(Debug, Clone)]
275#[non_exhaustive]
276pub enum DTGCredentialType {
277 Membership,
278 Relationship,
279 Invitation,
280 Persona,
281 Endorsement,
282 Witness,
283
284 #[deprecated(
286 since = "0.2.0",
287 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
288 It was removed from the DTG Core Credentials specification in Working Draft 01 \
289 and will be defined by the planned DTG Verifiable Data Structures specification. \
290 This variant will be removed in a future release."
291 )]
292 RCard,
293}
294
295impl Display for DTGCredentialType {
296 #[allow(deprecated)]
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 match self {
299 DTGCredentialType::Membership => write!(f, "MembershipCredential"),
300 DTGCredentialType::Relationship => write!(f, "RelationshipCredential"),
301 DTGCredentialType::Invitation => write!(f, "InvitationCredential"),
302 DTGCredentialType::Persona => write!(f, "PersonaCredential"),
303 DTGCredentialType::Endorsement => write!(f, "EndorsementCredential"),
304 DTGCredentialType::Witness => write!(f, "WitnessCredential"),
305 DTGCredentialType::RCard => write!(f, "RCardCredential"),
306 }
307 }
308}
309
310const DTG_TYPES: [&str; 7] = [
312 "MembershipCredential",
313 "RelationshipCredential",
314 "InvitationCredential",
315 "PersonaCredential",
316 "EndorsementCredential",
317 "WitnessCredential",
318 "RCardCredential",
319];
320
321impl TryFrom<&[String]> for DTGCredentialType {
322 type Error = DTGCredentialError;
323
324 #[allow(deprecated)]
325 fn try_from(types: &[String]) -> Result<Self, Self::Error> {
326 if let Some(type_) = DTG_TYPES.iter().find(|t| types.contains(&t.to_string())) {
327 match *type_ {
328 "MembershipCredential" => Ok(DTGCredentialType::Membership),
329 "RelationshipCredential" => Ok(DTGCredentialType::Relationship),
330 "InvitationCredential" => Ok(DTGCredentialType::Invitation),
331 "PersonaCredential" => Ok(DTGCredentialType::Persona),
332 "EndorsementCredential" => Ok(DTGCredentialType::Endorsement),
333 "WitnessCredential" => Ok(DTGCredentialType::Witness),
334 "RCardCredential" => Ok(DTGCredentialType::RCard),
335 _ => Err(DTGCredentialError::UnknownCredential),
336 }
337 } else {
338 Err(DTGCredentialError::UnknownCredential)
339 }
340 }
341}
342
343#[derive(Serialize, Deserialize, Debug, Clone)]
345#[serde(rename_all = "camelCase")]
346pub struct DTGCommon {
347 #[serde(rename = "@context")]
352 pub context: Vec<String>,
353
354 #[serde(rename = "type")]
359 pub type_: Vec<String>,
360
361 #[serde(skip_serializing_if = "Option::is_none", default)]
378 pub id: Option<String>,
379
380 pub issuer: String,
382
383 #[serde(serialize_with = "iso8601_format", alias = "issuanceDate")]
385 pub valid_from: DateTime<Utc>,
386
387 #[serde(serialize_with = "iso8601_format_option")]
389 #[serde(
390 skip_serializing_if = "Option::is_none",
391 alias = "expirationDate",
392 default
393 )]
394 pub valid_until: Option<DateTime<Utc>>,
395
396 #[serde(skip_serializing_if = "Option::is_none", default)]
406 pub task_context: Option<String>,
407
408 pub credential_subject: CredentialSubject,
410
411 #[serde(skip_serializing_if = "Option::is_none", default)]
413 pub proof: Option<DataIntegrityProof>,
414}
415
416impl DTGCommon {
417 pub fn signed(&self) -> bool {
421 self.proof.is_some()
422 }
423
424 pub fn id(&self) -> Option<&str> {
426 self.id.as_deref()
427 }
428
429 pub fn issuer(&self) -> &str {
431 &self.issuer
432 }
433
434 #[allow(deprecated)]
436 pub fn subject(&self) -> &str {
437 match &self.credential_subject {
438 CredentialSubject::Basic(subject) => &subject.id,
439 CredentialSubject::Endorsement(subject) => &subject.id,
440 CredentialSubject::Witness(subject) => &subject.id,
441 CredentialSubject::RCard(subject) => &subject.id,
442 }
443 }
444
445 pub fn valid_from(&self) -> DateTime<Utc> {
447 self.valid_from
448 }
449
450 pub fn valid_until(&self) -> Option<DateTime<Utc>> {
452 self.valid_until
453 }
454
455 pub fn task_context(&self) -> Option<&str> {
457 self.task_context.as_deref()
458 }
459}
460
461impl Default for DTGCommon {
463 fn default() -> Self {
464 DTGCommon {
465 context: vec![
466 "https://www.w3.org/ns/credentials/v2".to_string(),
467 "https://firstperson.network/credentials/dtg/v1".to_string(),
468 ],
469 type_: vec![
470 "VerifiableCredential".to_string(),
471 "DTGCredential".to_string(),
472 ],
473 id: None,
474 issuer: String::new(),
475 valid_from: Utc::now(),
476 valid_until: None,
477 task_context: None,
478 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic {
479 id: String::new(),
480 }),
481 proof: None,
482 }
483 }
484}
485
486impl TryFrom<DTGCommon> for DTGCredential {
488 type Error = DTGCredentialError;
489
490 #[allow(deprecated)]
491 fn try_from(value: DTGCommon) -> Result<Self, Self::Error> {
492 match &value.type_.as_slice().try_into()? {
493 DTGCredentialType::Membership => Ok(DTGCredential {
494 type_: DTGCredentialType::Membership,
495 version: value.context.as_slice().try_into()?,
496 credential: value,
497 }),
498 DTGCredentialType::Relationship => Ok(DTGCredential {
499 type_: DTGCredentialType::Relationship,
500 version: value.context.as_slice().try_into()?,
501 credential: value,
502 }),
503 DTGCredentialType::Invitation => Ok(DTGCredential {
504 type_: DTGCredentialType::Invitation,
505 version: value.context.as_slice().try_into()?,
506 credential: value,
507 }),
508 DTGCredentialType::Persona => Ok(DTGCredential {
509 type_: DTGCredentialType::Persona,
510 version: value.context.as_slice().try_into()?,
511 credential: value,
512 }),
513 DTGCredentialType::Endorsement => {
514 if let CredentialSubject::Endorsement { .. } = &value.credential_subject {
515 Ok(DTGCredential {
516 type_: DTGCredentialType::Endorsement,
517 version: value.context.as_slice().try_into()?,
518 credential: value,
519 })
520 } else {
521 Err(DTGCredentialError::UnknownCredential)
522 }
523 }
524 DTGCredentialType::Witness => {
525 if value.task_context.is_none() {
529 return Err(DTGCredentialError::MissingTaskContext);
530 }
531
532 match &value.credential_subject {
533 CredentialSubject::Witness(_) => Ok(DTGCredential {
534 type_: DTGCredentialType::Witness,
535 version: value.context.as_slice().try_into()?,
536 credential: value,
537 }),
538 CredentialSubject::Basic(subject) => {
539 Ok(DTGCredential {
541 type_: DTGCredentialType::Witness,
542 version: value.context.as_slice().try_into()?,
543 credential: DTGCommon {
544 credential_subject: CredentialSubject::Witness(
545 CredentialSubjectWitness {
546 id: subject.id.clone(),
547 digest: None,
548 witness_context: None,
549 },
550 ),
551 ..value
552 },
553 })
554 }
555 _ => Err(DTGCredentialError::UnknownCredential),
556 }
557 }
558 DTGCredentialType::RCard => match &value.credential_subject {
559 CredentialSubject::RCard { .. } => Ok(DTGCredential {
560 type_: DTGCredentialType::RCard,
561 version: value.context.as_slice().try_into()?,
562 credential: value,
563 }),
564 _ => Err(DTGCredentialError::UnknownCredential),
565 },
566 }
567 }
568}
569
570fn iso8601_format<S>(timestamp: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
573where
574 S: Serializer,
575{
576 s.serialize_str(
577 timestamp
578 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
579 .as_str(),
580 )
581}
582
583fn iso8601_format_option<S>(timestamp: &Option<DateTime<Utc>>, s: S) -> Result<S::Ok, S::Error>
584where
585 S: Serializer,
586{
587 if let Some(timestamp) = timestamp {
588 s.serialize_str(
589 timestamp
590 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
591 .as_str(),
592 )
593 } else {
594 s.serialize_none()
595 }
596}
597
598#[allow(deprecated)]
607#[derive(Serialize, Deserialize, Debug, Clone)]
608#[serde(untagged)]
609pub enum CredentialSubject {
610 Endorsement(CredentialSubjectEndorsement),
612
613 #[deprecated(
615 since = "0.2.0",
616 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
617 See DTGCredentialType::RCard. This variant will be removed in a future release."
618 )]
619 RCard(CredentialSubjectRCard),
620
621 Basic(CredentialSubjectBasic),
624
625 Witness(CredentialSubjectWitness),
627}
628
629#[derive(Serialize, Deserialize, Debug, Clone)]
631#[serde(deny_unknown_fields)]
632pub struct CredentialSubjectBasic {
633 pub id: String,
634}
635
636#[derive(Serialize, Deserialize, Debug, Clone)]
638#[serde(deny_unknown_fields)]
639pub struct CredentialSubjectEndorsement {
640 pub id: String,
641 pub endorsement: Value,
643}
644
645#[derive(Serialize, Deserialize, Debug, Clone)]
647#[serde(rename_all = "camelCase", deny_unknown_fields)]
648pub struct CredentialSubjectWitness {
649 pub id: String,
650
651 #[serde(skip_serializing_if = "Option::is_none")]
652 pub digest: Option<String>,
653
654 #[serde(skip_serializing_if = "Option::is_none")]
656 pub witness_context: Option<WitnessContext>,
657}
658
659#[derive(Serialize, Deserialize, Debug, Clone)]
661#[serde(rename_all = "camelCase", deny_unknown_fields)]
662pub struct WitnessContext {
663 pub event: Option<String>,
665
666 pub session_id: Option<String>,
668
669 pub method: Option<String>,
671}
672
673#[deprecated(
675 since = "0.2.0",
676 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
677 See DTGCredentialType::RCard. This struct will be removed in a future release."
678)]
679#[derive(Serialize, Deserialize, Debug, Clone)]
680#[serde(deny_unknown_fields)]
681pub struct CredentialSubjectRCard {
682 pub id: String,
683
684 pub card: Value,
686}
687
688#[cfg(test)]
689#[allow(deprecated)]
690mod tests {
691 use crate::{
692 CredentialSubject, CredentialSubjectRCard, DTGCommon, DTGCredential, DTGCredentialType,
693 W3CVCVersion,
694 };
695 use chrono::{DateTime, Utc};
696 use serde_json::Value;
697
698 #[test]
699 fn test_vmc_vc_1_deserialize() {
700 let vmc: DTGCredential = match serde_json::from_str(
702 r#"{
703"@context": [
704 "https://www.w3.org/2018/credentials/v1",
705 "https://firstperson.network/credentials/dtg/v1",
706 "https://w3id.org/security/suites/ed25519-2020/v1"
707 ],
708 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
709 "issuer": "did:web:chess-club.example",
710 "issuanceDate": "2026-01-06T10:00:00Z",
711 "expirationDate": "2027-01-06T10:00:00Z",
712 "credentialSubject": {
713 "id": "did:key:z6MkpTHR8VNs..."
714 }
715 }"#,
716 ) {
717 Ok(vmc) => vmc,
718 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
719 };
720
721 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
722 assert!(matches!(
723 vmc.credential().credential_subject,
724 CredentialSubject::Basic(_)
725 ));
726 assert!(matches!(vmc.version, W3CVCVersion::V1_1));
727 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V1_1));
728 }
729
730 #[test]
731 fn test_missing_w3c_context() {
732 assert!(
734 serde_json::from_str::<DTGCredential>(
735 r#"{
736"@context": [
737 "https://firstperson.network/credentials/dtg/v1",
738 "https://w3id.org/security/suites/ed25519-2020/v1"
739 ],
740 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
741 "issuer": "did:web:chess-club.example",
742 "issuanceDate": "2026-01-06T10:00:00Z",
743 "expirationDate": "2027-01-06T10:00:00Z",
744 "credentialSubject": {
745 "id": "did:key:z6MkpTHR8VNs..."
746 }
747 }"#,
748 )
749 .is_err()
750 );
751 }
752
753 #[test]
754 fn test_mutable_credential() {
755 let mut vmc = DTGCredential::new_vmc(
756 "did:example:issuer".to_string(),
757 "did:example:subject".to_string(),
758 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
759 .unwrap()
760 .with_timezone(&Utc),
761 None,
762 false,
763 );
764
765 let cred = vmc.credential_mut();
766 cred.type_.push("PersonhoodCredential".to_string());
767 assert!(vmc.is_personhood_credential());
768 }
769
770 #[test]
771 fn test_vmc_deserialize() {
772 let vmc: DTGCredential = match serde_json::from_str(
773 r#"{
774 "@context": ["https://www.w3.org/ns/credentials/v2"],
775 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
776 "issuer": "did:example:community",
777 "validFrom": "2024-06-18T10:00:00Z",
778 "credentialSubject": { "id": "did:example:rDid" }
779 }"#,
780 ) {
781 Ok(vmc) => vmc,
782 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
783 };
784
785 assert!(!vmc.is_personhood_credential());
786 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
787 assert!(matches!(
788 vmc.credential().credential_subject,
789 CredentialSubject::Basic(_)
790 ));
791 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V2_0));
792 }
793
794 #[test]
795 fn test_vmc_phc_deserialize() {
796 let vmc: DTGCredential = match serde_json::from_str(
797 r#"{
798 "@context": ["https://www.w3.org/ns/credentials/v2"],
799 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential", "PersonhoodCredential"],
800 "issuer": "did:example:community",
801 "validFrom": "2024-06-18T10:00:00Z",
802 "credentialSubject": { "id": "did:example:rDid" }
803 }"#,
804 ) {
805 Ok(vmc) => vmc,
806 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
807 };
808
809 assert!(vmc.is_personhood_credential());
810 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
811 assert!(matches!(
812 vmc.credential().credential_subject,
813 CredentialSubject::Basic(_)
814 ));
815 }
816
817 #[test]
818 fn test_vrc_deserialize() {
819 let vrc: DTGCredential = match serde_json::from_str(
820 r#"{
821 "@context": ["https://www.w3.org/ns/credentials/v2"],
822 "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"],
823 "issuer": "did:example:governmentAgencyDid",
824 "validFrom": "2024-06-18T10:00:00Z",
825 "credentialSubject": { "id": "did:example:citizenRDid" }
826 }"#,
827 ) {
828 Ok(vrc) => vrc,
829 Err(e) => panic!("Couldn't deserialize VRC: {}", e),
830 };
831
832 assert!(matches!(vrc.type_, DTGCredentialType::Relationship));
833 assert!(matches!(
834 vrc.credential().credential_subject,
835 CredentialSubject::Basic(_)
836 ));
837 }
838
839 #[test]
840 fn test_vic_deserialize() {
841 let vic: DTGCredential = match serde_json::from_str(
842 r#"{
843 "@context": ["https://www.w3.org/ns/credentials/v2"],
844 "type": ["VerifiableCredential", "DTGCredential", "InvitationCredential"],
845 "issuer": "did:example:governmentAgencyVicDid",
846 "validFrom": "2024-06-18T10:00:00Z",
847 "credentialSubject": { "id": "did:example:citizenRDid" }
848 }"#,
849 ) {
850 Ok(vic) => vic,
851 Err(e) => panic!("Couldn't deserialize VIC: {}", e),
852 };
853
854 assert!(!vic.is_personhood_credential());
855 assert!(matches!(vic.type_, DTGCredentialType::Invitation));
856 assert!(matches!(
857 vic.credential().credential_subject,
858 CredentialSubject::Basic(_)
859 ));
860 }
861
862 #[test]
863 fn test_vpc_deserialize() {
864 let vpc: DTGCredential = match serde_json::from_str(
865 r#"{
866 "@context": ["https://www.w3.org/ns/credentials/v2"],
867 "type": ["VerifiableCredential", "DTGCredential", "PersonaCredential"],
868 "issuer": "did:example:governmentAgencyDid",
869 "validFrom": "2024-06-18T10:00:00Z",
870 "credentialSubject": { "id": "did:example:citizenRDid" }
871 }"#,
872 ) {
873 Ok(vpc) => vpc,
874 Err(e) => panic!("Couldn't deserialize VPC: {}", e),
875 };
876
877 assert!(matches!(vpc.type_, DTGCredentialType::Persona));
878 assert!(matches!(
879 vpc.credential().credential_subject,
880 CredentialSubject::Basic(_)
881 ));
882 }
883
884 #[test]
885 fn test_vec_deserialize() {
886 let vec: DTGCredential = match serde_json::from_str(
887 r#"{
888 "@context": ["https://www.w3.org/ns/credentials/v2"],
889 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
890 "issuer": "did:example:governmentAgencyDid",
891 "validFrom": "2024-06-18T10:00:00Z",
892 "credentialSubject": { "id": "did:example:citizenRDid", "endorsement": {} }
893 }"#,
894 ) {
895 Ok(vec) => vec,
896 Err(e) => panic!("Couldn't deserialize VEC: {}", e),
897 };
898
899 assert!(matches!(vec.type_, DTGCredentialType::Endorsement));
900 assert!(matches!(vec.subject(), "did:example:citizenRDid"));
901 assert!(matches!(
902 vec.credential().credential_subject,
903 CredentialSubject::Endorsement(_)
904 ));
905 }
906
907 #[test]
908 fn test_vec_bad_deserialize() {
909 match serde_json::from_str::<DTGCredential>(
910 r#"{
911 "@context": ["https://www.w3.org/ns/credentials/v2"],
912 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
913 "issuer": "did:example:governmentAgencyDid",
914 "validFrom": "2024-06-18T10:00:00Z",
915 "credentialSubject": { "id": "did:example:citizenRDid", "other": [] }
916 }"#,
917 ) {
918 Ok(_) => panic!("Expected Unknown Credential type"),
919 Err(_) => {
920 }
922 };
923 }
924
925 #[test]
926 fn test_vwc_simple_deserialize() {
927 let vwc: DTGCredential = match serde_json::from_str(
928 r#"{
929 "@context": ["https://www.w3.org/ns/credentials/v2"],
930 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
931 "issuer": "did:example:governmentAgencyDid",
932 "validFrom": "2024-06-18T10:00:00Z",
933 "taskContext": "thread-abc-123",
934 "credentialSubject": { "id": "did:example:citizenRDid" }
935 }"#,
936 ) {
937 Ok(vwc) => vwc,
938 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
939 };
940
941 assert!(matches!(vwc.type_, DTGCredentialType::Witness));
942 assert!(matches!(vwc.subject(), "did:example:citizenRDid"));
943 assert_eq!(vwc.task_context(), Some("thread-abc-123"));
944 assert!(matches!(
945 vwc.credential().credential_subject,
946 CredentialSubject::Witness(_)
947 ));
948 }
949
950 #[test]
951 fn test_vwc_full_deserialize() {
952 let vwc: DTGCredential = match serde_json::from_str(
953 r#"{
954 "@context": ["https://www.w3.org/ns/credentials/v2"],
955 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
956 "issuer": "did:example:governmentAgencyDid",
957 "validFrom": "2024-06-18T10:00:00Z",
958 "taskContext": "thread-abc-123",
959 "credentialSubject": { "id": "did:example:citizenRDid", "digest": "abcdf", "witnessContext": {} }
960 }"#,
961 ) {
962 Ok(vwc) => vwc,
963 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
964 };
965
966 assert!(matches!(vwc.type_(), DTGCredentialType::Witness));
967 assert!(matches!(
968 vwc.credential().credential_subject,
969 CredentialSubject::Witness(_)
970 ));
971 }
972
973 #[test]
974 fn test_vwc_bad_deserialize() {
975 if serde_json::from_str::<DTGCredential>(
976 r#"{
977 "@context": ["https://www.w3.org/ns/credentials/v2"],
978 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
979 "issuer": "did:example:governmentAgencyDid",
980 "validFrom": "2024-06-18T10:00:00Z",
981 "taskContext": "thread-abc-123",
982 "credentialSubject": { "id": "did:example:citizenRDid", "digest": "abcdf", "wrongContext": {} }
983 }"#,
984 ).is_ok() {
985 panic!("Should have failed due to wrong CredentialSubject!");
986 }
987 }
988
989 #[test]
990 fn test_rcard_simple_deserialize() {
991 let rcard: DTGCredential = match serde_json::from_str(
992 r#"{
993 "@context": ["https://www.w3.org/ns/credentials/v2"],
994 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
995 "issuer": "did:example:governmentAgencyDid",
996 "validFrom": "2024-06-18T10:00:00Z",
997 "credentialSubject": { "id": "did:example:citizenRDid", "card": [] }
998 }"#,
999 ) {
1000 Ok(rcard) => rcard,
1001 Err(e) => panic!("Couldn't deserialize R-Card: {}", e),
1002 };
1003
1004 assert!(matches!(rcard.type_(), DTGCredentialType::RCard));
1005 assert!(matches!(rcard.subject(), "did:example:citizenRDid"));
1006 assert!(matches!(
1007 rcard.credential().credential_subject,
1008 CredentialSubject::RCard(_)
1009 ));
1010 }
1011
1012 #[test]
1013 fn test_rcard_bad_deserialize() {
1014 if serde_json::from_str::<DTGCredential>(
1015 r#"{
1016 "@context": ["https://www.w3.org/ns/credentials/v2"],
1017 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
1018 "issuer": "did:example:governmentAgencyDid",
1019 "validFrom": "2024-06-18T10:00:00Z",
1020 "credentialSubject": { "id": "did:example:citizenRDid" }
1021 }"#,
1022 )
1023 .is_ok()
1024 {
1025 panic!("Should have failed due to wrong CredentialSubject!");
1026 }
1027 }
1028 #[test]
1029 fn test_deserialize_unknown() {
1030 match serde_json::from_str::<DTGCredential>(
1031 r#"{
1032 "@context": ["https://www.w3.org/ns/credentials/v2"],
1033 "type": ["VerifiableCredential", "DTGCredential", "UnknownCredential"],
1034 "issuer": "did:example:governmentAgencyDid",
1035 "validFrom": "2024-06-18T10:00:00Z",
1036 "credentialSubject": { "id": "did:example:citizenRDid" }
1037 }"#,
1038 ) {
1039 Ok(_) => panic!("Expected Unknown Credential type"),
1040 Err(e) => {
1041 if e.to_string() == "Unknown credential type" {
1042 } else {
1044 panic!("Wrong error type returned");
1045 }
1046 }
1047 };
1048 }
1049
1050 #[test]
1051 fn test_deserialize_mismatched_credential_subject() {
1052 match serde_json::from_str::<DTGCredential>(
1053 r#"{
1054 "@context": ["https://www.w3.org/ns/credentials/v2"],
1055 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
1056 "issuer": "did:example:governmentAgencyDid",
1057 "validFrom": "2024-06-18T10:00:00Z",
1058 "credentialSubject": { "id": "did:example:citizenRDid" }
1059 }"#,
1060 ) {
1061 Ok(_) => panic!("Expected Unknown Credential type"),
1062 Err(e) => {
1063 if e.to_string() == "Unknown credential type" {
1064 } else {
1066 panic!("Wrong error type returned");
1067 }
1068 }
1069 };
1070 }
1071
1072 #[test]
1073 fn test_proof_signed() {
1074 let cred: DTGCredential = match serde_json::from_str(
1075 r#"{
1076 "@context": ["https://www.w3.org/ns/credentials/v2"],
1077 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1078 "issuer": "did:example:community",
1079 "validFrom": "2024-06-18T10:00:00Z",
1080 "credentialSubject": { "id": "did:example:rDid" },
1081 "proof": {
1082 "type": "DataIntegrityProof",
1083 "cryptosuite": "eddsa-jcs-2022",
1084 "created": "2025-12-04T00:00:00",
1085 "verificationMethod": "did:example:test#key-1",
1086 "proofPurpose": "assertionMethod",
1087 "proofValue": "abcd"
1088 }
1089 }"#,
1090 ) {
1091 Ok(vmc) => vmc,
1092 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1093 };
1094
1095 assert!(cred.signed());
1096 assert!(cred.proof_value().is_some());
1097 }
1098
1099 #[test]
1100 fn test_proof_not_signed() {
1101 let cred: DTGCredential = match serde_json::from_str(
1102 r#"{
1103 "@context": ["https://www.w3.org/ns/credentials/v2"],
1104 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1105 "issuer": "did:example:community",
1106 "validFrom": "2024-06-18T10:00:00Z",
1107 "credentialSubject": { "id": "did:example:rDid" }
1108 }"#,
1109 ) {
1110 Ok(vmc) => vmc,
1111 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1112 };
1113
1114 assert!(!cred.signed());
1115 assert!(cred.proof_value().is_none());
1116 }
1117
1118 #[test]
1119 fn test_helpers() {
1120 let cred: DTGCredential = match serde_json::from_str(
1121 r#"{
1122 "@context": ["https://www.w3.org/ns/credentials/v2"],
1123 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1124 "issuer": "did:example:issuer",
1125 "validFrom": "2024-06-18T00:00:00Z",
1126 "credentialSubject": { "id": "did:example:subject" }
1127 }"#,
1128 ) {
1129 Ok(vmc) => vmc,
1130 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1131 };
1132
1133 assert_eq!(cred.issuer(), "did:example:issuer");
1134 assert_eq!(cred.subject(), "did:example:subject");
1135 assert_eq!(
1136 cred.valid_from()
1137 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1138 "2024-06-18T00:00:00Z"
1139 );
1140 assert_eq!(cred.valid_until(), None);
1141 }
1142
1143 #[test]
1144 fn test_valid_until() {
1145 let cred: DTGCredential = match serde_json::from_str(
1146 r#"{
1147 "@context": ["https://www.w3.org/ns/credentials/v2"],
1148 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1149 "issuer": "did:example:issuer",
1150 "validFrom": "2024-06-18T00:00:00Z",
1151 "validUntil": "2030-01-01T00:00:00Z",
1152 "credentialSubject": { "id": "did:example:subject" }
1153 }"#,
1154 ) {
1155 Ok(vmc) => vmc,
1156 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1157 };
1158
1159 assert_eq!(
1160 cred.valid_until()
1161 .unwrap()
1162 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1163 "2030-01-01T00:00:00Z"
1164 );
1165 }
1166
1167 #[test]
1168 fn test_bad_type() {
1169 assert!(
1170 std::convert::TryInto::<DTGCredentialType>::try_into(
1171 vec!["bad_type".to_string()].as_slice(),
1172 )
1173 .is_err()
1174 );
1175 }
1176
1177 #[test]
1178 fn test_badly_constructed_vwc() {
1179 let mut cred = DTGCommon::default();
1180 cred.type_.push("WitnessCredential".to_string());
1181 cred.task_context = Some("thread-abc-123".to_string());
1184 cred.credential_subject = CredentialSubject::RCard(CredentialSubjectRCard {
1185 id: "did:example:bad".to_string(),
1186 card: Value::Null,
1187 });
1188
1189 assert!(std::convert::TryInto::<DTGCredential>::try_into(cred).is_err());
1190 }
1191
1192 #[test]
1193 fn test_vwc_missing_task_context() {
1194 match serde_json::from_str::<DTGCredential>(
1196 r#"{
1197 "@context": ["https://www.w3.org/ns/credentials/v2"],
1198 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1199 "issuer": "did:example:witness",
1200 "validFrom": "2024-06-18T10:00:00Z",
1201 "credentialSubject": { "id": "did:example:observed" }
1202 }"#,
1203 ) {
1204 Ok(_) => panic!("Expected a VWC without taskContext to be rejected"),
1205 Err(e) => assert_eq!(
1206 e.to_string(),
1207 "WitnessCredential is missing the required taskContext property"
1208 ),
1209 }
1210 }
1211
1212 #[test]
1213 fn test_task_context_round_trip() {
1214 let raw = r#"{
1217 "@context": ["https://www.w3.org/ns/credentials/v2"],
1218 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1219 "issuer": "did:example:witness",
1220 "validFrom": "2024-06-18T10:00:00Z",
1221 "taskContext": "thread-abc-123",
1222 "credentialSubject": { "id": "did:example:observed" }
1223 }"#;
1224
1225 let cred: DTGCredential = serde_json::from_str(raw).unwrap();
1226 let out = serde_json::to_string(&cred).unwrap();
1227
1228 assert!(out.contains(r#""taskContext":"thread-abc-123""#));
1229 }
1230
1231 #[test]
1232 fn test_task_context_optional_on_other_types() {
1233 let vrc: DTGCredential = serde_json::from_str(
1235 r#"{
1236 "@context": ["https://www.w3.org/ns/credentials/v2"],
1237 "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"],
1238 "issuer": "did:example:issuer",
1239 "validFrom": "2024-06-18T10:00:00Z",
1240 "credentialSubject": { "id": "did:example:subject" }
1241 }"#,
1242 )
1243 .unwrap();
1244
1245 assert_eq!(vrc.task_context(), None);
1246 assert!(!serde_json::to_string(&vrc).unwrap().contains("taskContext"));
1248 }
1249
1250 #[test]
1251 fn test_digest_multibase() {
1252 let vrc = DTGCredential::new_vrc(
1253 "did:example:issuer".to_string(),
1254 "did:example:subject".to_string(),
1255 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1256 .unwrap()
1257 .with_timezone(&Utc),
1258 None,
1259 );
1260
1261 let digest = vrc.digest_multibase().unwrap();
1262
1263 assert!(digest.starts_with('z'));
1265
1266 let (base, bytes) = multibase::decode(&digest).unwrap();
1268 assert_eq!(base, multibase::Base::Base58Btc);
1269 assert_eq!(bytes.len(), 34);
1270 assert_eq!(&bytes[..2], &[0x12, 0x20]);
1271
1272 assert_eq!(digest, vrc.digest_multibase().unwrap());
1274
1275 let other = DTGCredential::new_vrc(
1277 "did:example:issuer".to_string(),
1278 "did:example:someone-else".to_string(),
1279 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1280 .unwrap()
1281 .with_timezone(&Utc),
1282 None,
1283 );
1284 assert_ne!(digest, other.digest_multibase().unwrap());
1285 }
1286
1287 #[test]
1288 fn test_verify_digest() {
1289 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1290 .unwrap()
1291 .with_timezone(&Utc);
1292
1293 let vrc = DTGCredential::new_vrc(
1294 "did:example:issuer".to_string(),
1295 "did:example:subject".to_string(),
1296 valid_from,
1297 None,
1298 );
1299
1300 let vwc = DTGCredential::new_vwc(
1301 "did:example:witness".to_string(),
1302 "did:example:issuer".to_string(),
1304 valid_from,
1305 None,
1306 "thread-abc-123".to_string(),
1307 Some(vrc.digest_multibase().unwrap()),
1308 None,
1309 );
1310
1311 assert!(vwc.verify_digest(&vrc).unwrap());
1312
1313 let other = DTGCredential::new_vrc(
1315 "did:example:issuer".to_string(),
1316 "did:example:someone-else".to_string(),
1317 valid_from,
1318 None,
1319 );
1320 assert!(!vwc.verify_digest(&other).unwrap());
1321 }
1322
1323 #[test]
1324 fn test_verify_digest_without_digest() {
1325 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1326 .unwrap()
1327 .with_timezone(&Utc);
1328
1329 let vrc = DTGCredential::new_vrc(
1330 "did:example:issuer".to_string(),
1331 "did:example:subject".to_string(),
1332 valid_from,
1333 None,
1334 );
1335
1336 let vwc = DTGCredential::new_vwc(
1338 "did:example:witness".to_string(),
1339 "did:example:issuer".to_string(),
1340 valid_from,
1341 None,
1342 "thread-abc-123".to_string(),
1343 None,
1344 None,
1345 );
1346
1347 assert!(!vwc.verify_digest(&vrc).unwrap());
1348 }
1349
1350 #[test]
1351 fn test_iso8601_format_option() {
1352 let now: DateTime<Utc> = DateTime::parse_from_rfc3339(
1353 &Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1354 )
1355 .unwrap()
1356 .to_utc();
1357 let cred = DTGCommon {
1358 valid_until: Some(now),
1359 ..Default::default()
1360 };
1361
1362 let value = serde_json::to_value(&cred).unwrap();
1363 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
1364 assert_eq!(cred2.valid_until, Some(now));
1365
1366 let cred = DTGCommon::default();
1367 let value = serde_json::to_value(&cred).unwrap();
1368 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
1369 assert_eq!(cred2.valid_until, None);
1370 }
1371
1372 #[cfg(feature = "affinidi-signing")]
1373 #[tokio::test]
1374 async fn test_signing() {
1375 use affinidi_secrets_resolver::secrets::Secret;
1376
1377 let secret = Secret::generate_ed25519(None, None);
1378
1379 let mut cred = DTGCredential::new_vrc(
1380 "did:example:issuer".to_string(),
1381 "did:example:subject".to_string(),
1382 Utc::now(),
1383 None,
1384 );
1385
1386 assert!(cred.sign(&secret, Some(Utc::now())).await.is_ok());
1387
1388 assert!(
1389 cred.verify_proof_with_public_key(secret.get_public_bytes())
1390 .is_ok()
1391 );
1392
1393 let secret2 = Secret::generate_ed25519(None, None);
1394 assert!(
1395 cred.verify_proof_with_public_key(secret2.get_public_bytes())
1396 .is_err()
1397 );
1398 }
1399
1400 #[cfg(feature = "affinidi-signing")]
1408 #[tokio::test]
1409 async fn test_id_is_covered_by_the_proof() {
1410 use affinidi_secrets_resolver::secrets::Secret;
1411
1412 let secret = Secret::generate_ed25519(None, None);
1413
1414 let mut cred = DTGCredential::new_vrc(
1415 "did:example:issuer".to_string(),
1416 "did:example:subject".to_string(),
1417 Utc::now(),
1418 None,
1419 )
1420 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
1421
1422 cred.sign(&secret, Some(Utc::now()))
1423 .await
1424 .expect("signing a credential that carries an id");
1425 assert!(
1426 cred.verify_proof_with_public_key(secret.get_public_bytes())
1427 .is_ok(),
1428 "an id set before signing verifies"
1429 );
1430
1431 cred.set_id("urn:uuid:00000000-0000-0000-0000-000000000000");
1434 assert!(
1435 cred.verify_proof_with_public_key(secret.get_public_bytes())
1436 .is_err(),
1437 "an id changed after signing must break the proof"
1438 );
1439 }
1440
1441 #[cfg(feature = "affinidi-signing")]
1442 #[tokio::test]
1443 async fn test_signing_error() {
1444 use affinidi_secrets_resolver::secrets::Secret;
1445
1446 let secret = Secret::generate_x25519(None, None).unwrap();
1447
1448 let mut cred = DTGCredential::new_vrc(
1449 "did:example:issuer".to_string(),
1450 "did:example:subject".to_string(),
1451 Utc::now(),
1452 None,
1453 );
1454
1455 assert!(cred.sign(&secret, Some(Utc::now())).await.is_err());
1456 }
1457
1458 #[cfg(feature = "affinidi-signing")]
1459 #[test]
1460 fn test_signing_no_proof() {
1461 use crate::DTGCredentialError;
1462 use affinidi_secrets_resolver::secrets::Secret;
1463
1464 let cred = DTGCredential::new_vrc(
1465 "did:example:issuer".to_string(),
1466 "did:example:subject".to_string(),
1467 Utc::now(),
1468 None,
1469 );
1470
1471 let secret = Secret::generate_ed25519(None, None);
1472 match cred.verify_proof_with_public_key(secret.get_public_bytes()) {
1473 Err(DTGCredentialError::NotSigned) => {
1474 }
1476 _ => panic!("Expected NotSigned error!"),
1477 }
1478 }
1479}