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 issuer(&self) -> &str {
109 self.credential.issuer()
110 }
111
112 pub fn subject(&self) -> &str {
114 self.credential.subject()
115 }
116
117 pub fn valid_from(&self) -> DateTime<Utc> {
119 self.credential.valid_from()
120 }
121
122 pub fn valid_until(&self) -> Option<DateTime<Utc>> {
124 self.credential.valid_until()
125 }
126
127 pub fn task_context(&self) -> Option<&str> {
132 self.credential.task_context()
133 }
134
135 pub fn digest_multibase(&self) -> Result<String, DTGCredentialError> {
150 let canonical = serde_json_canonicalizer::to_vec(&self.credential)
151 .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
152
153 let mut multihash = Vec::with_capacity(34);
155 multihash.extend_from_slice(&[0x12, 0x20]);
156 multihash.extend_from_slice(&Sha256::digest(&canonical));
157
158 Ok(multibase::encode(Base::Base58Btc, &multihash))
159 }
160
161 pub fn verify_digest(&self, witnessed: &DTGCredential) -> Result<bool, DTGCredentialError> {
166 let CredentialSubject::Witness(subject) = &self.credential.credential_subject else {
167 return Ok(false);
168 };
169
170 let Some(digest) = &subject.digest else {
171 return Ok(false);
172 };
173
174 Ok(*digest == witnessed.digest_multibase()?)
175 }
176
177 pub fn proof_value(&self) -> Option<&str> {
179 if let Some(proof) = &self.credential.proof {
180 proof.proof_value.as_deref()
181 } else {
182 None
183 }
184 }
185
186 #[cfg(feature = "affinidi-signing")]
187 pub async fn sign(
191 &mut self,
192 signing_secret: &Secret,
193 create_time: Option<DateTime<Utc>>,
194 ) -> Result<DataIntegrityProof, DTGCredentialError> {
195 let mut options = SignOptions::new();
196 if let Some(ts) = create_time {
197 options = options.with_created(ts);
198 }
199
200 let proof = DataIntegrityProof::sign(self, signing_secret, options).await?;
201
202 self.credential.proof = Some(proof.clone());
203 Ok(proof)
204 }
205
206 #[cfg(feature = "affinidi-signing")]
207 pub fn verify_proof_with_public_key(
211 &self,
212 public_key_bytes: &[u8],
213 ) -> Result<(), DTGCredentialError> {
214 let proof = if let Some(proof) = &self.credential.proof {
215 proof.clone()
216 } else {
217 use tracing::warn;
218
219 warn!("Trying to verify a DTG Credential that has no proof");
220 return Err(DTGCredentialError::NotSigned);
221 };
222
223 let unsigned = DTGCommon {
224 proof: None,
225 ..self.credential.clone()
226 };
227
228 proof.verify_with_public_key(&unsigned, public_key_bytes, VerifyOptions::new())?;
229 Ok(())
230 }
231
232 pub fn get_w3c_vc_version(&self) -> W3CVCVersion {
234 self.version
235 }
236
237 pub fn is_personhood_credential(&self) -> bool {
239 if let DTGCredentialType::Membership = self.type_ {
240 self.credential
241 .type_
242 .contains(&"PersonhoodCredential".to_string())
243 } else {
244 false
245 }
246 }
247}
248
249#[derive(Debug, Clone)]
251#[non_exhaustive]
252pub enum DTGCredentialType {
253 Membership,
254 Relationship,
255 Invitation,
256 Persona,
257 Endorsement,
258 Witness,
259
260 #[deprecated(
262 since = "0.2.0",
263 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
264 It was removed from the DTG Core Credentials specification in Working Draft 01 \
265 and will be defined by the planned DTG Verifiable Data Structures specification. \
266 This variant will be removed in a future release."
267 )]
268 RCard,
269}
270
271impl Display for DTGCredentialType {
272 #[allow(deprecated)]
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 match self {
275 DTGCredentialType::Membership => write!(f, "MembershipCredential"),
276 DTGCredentialType::Relationship => write!(f, "RelationshipCredential"),
277 DTGCredentialType::Invitation => write!(f, "InvitationCredential"),
278 DTGCredentialType::Persona => write!(f, "PersonaCredential"),
279 DTGCredentialType::Endorsement => write!(f, "EndorsementCredential"),
280 DTGCredentialType::Witness => write!(f, "WitnessCredential"),
281 DTGCredentialType::RCard => write!(f, "RCardCredential"),
282 }
283 }
284}
285
286const DTG_TYPES: [&str; 7] = [
288 "MembershipCredential",
289 "RelationshipCredential",
290 "InvitationCredential",
291 "PersonaCredential",
292 "EndorsementCredential",
293 "WitnessCredential",
294 "RCardCredential",
295];
296
297impl TryFrom<&[String]> for DTGCredentialType {
298 type Error = DTGCredentialError;
299
300 #[allow(deprecated)]
301 fn try_from(types: &[String]) -> Result<Self, Self::Error> {
302 if let Some(type_) = DTG_TYPES.iter().find(|t| types.contains(&t.to_string())) {
303 match *type_ {
304 "MembershipCredential" => Ok(DTGCredentialType::Membership),
305 "RelationshipCredential" => Ok(DTGCredentialType::Relationship),
306 "InvitationCredential" => Ok(DTGCredentialType::Invitation),
307 "PersonaCredential" => Ok(DTGCredentialType::Persona),
308 "EndorsementCredential" => Ok(DTGCredentialType::Endorsement),
309 "WitnessCredential" => Ok(DTGCredentialType::Witness),
310 "RCardCredential" => Ok(DTGCredentialType::RCard),
311 _ => Err(DTGCredentialError::UnknownCredential),
312 }
313 } else {
314 Err(DTGCredentialError::UnknownCredential)
315 }
316 }
317}
318
319#[derive(Serialize, Deserialize, Debug, Clone)]
321#[serde(rename_all = "camelCase")]
322pub struct DTGCommon {
323 #[serde(rename = "@context")]
328 pub context: Vec<String>,
329
330 #[serde(rename = "type")]
335 pub type_: Vec<String>,
336
337 pub issuer: String,
339
340 #[serde(serialize_with = "iso8601_format", alias = "issuanceDate")]
342 pub valid_from: DateTime<Utc>,
343
344 #[serde(serialize_with = "iso8601_format_option")]
346 #[serde(
347 skip_serializing_if = "Option::is_none",
348 alias = "expirationDate",
349 default
350 )]
351 pub valid_until: Option<DateTime<Utc>>,
352
353 #[serde(skip_serializing_if = "Option::is_none", default)]
363 pub task_context: Option<String>,
364
365 pub credential_subject: CredentialSubject,
367
368 #[serde(skip_serializing_if = "Option::is_none", default)]
370 pub proof: Option<DataIntegrityProof>,
371}
372
373impl DTGCommon {
374 pub fn signed(&self) -> bool {
378 self.proof.is_some()
379 }
380
381 pub fn issuer(&self) -> &str {
383 &self.issuer
384 }
385
386 #[allow(deprecated)]
388 pub fn subject(&self) -> &str {
389 match &self.credential_subject {
390 CredentialSubject::Basic(subject) => &subject.id,
391 CredentialSubject::Endorsement(subject) => &subject.id,
392 CredentialSubject::Witness(subject) => &subject.id,
393 CredentialSubject::RCard(subject) => &subject.id,
394 }
395 }
396
397 pub fn valid_from(&self) -> DateTime<Utc> {
399 self.valid_from
400 }
401
402 pub fn valid_until(&self) -> Option<DateTime<Utc>> {
404 self.valid_until
405 }
406
407 pub fn task_context(&self) -> Option<&str> {
409 self.task_context.as_deref()
410 }
411}
412
413impl Default for DTGCommon {
415 fn default() -> Self {
416 DTGCommon {
417 context: vec![
418 "https://www.w3.org/ns/credentials/v2".to_string(),
419 "https://firstperson.network/credentials/dtg/v1".to_string(),
420 ],
421 type_: vec![
422 "VerifiableCredential".to_string(),
423 "DTGCredential".to_string(),
424 ],
425 issuer: String::new(),
426 valid_from: Utc::now(),
427 valid_until: None,
428 task_context: None,
429 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic {
430 id: String::new(),
431 }),
432 proof: None,
433 }
434 }
435}
436
437impl TryFrom<DTGCommon> for DTGCredential {
439 type Error = DTGCredentialError;
440
441 #[allow(deprecated)]
442 fn try_from(value: DTGCommon) -> Result<Self, Self::Error> {
443 match &value.type_.as_slice().try_into()? {
444 DTGCredentialType::Membership => Ok(DTGCredential {
445 type_: DTGCredentialType::Membership,
446 version: value.context.as_slice().try_into()?,
447 credential: value,
448 }),
449 DTGCredentialType::Relationship => Ok(DTGCredential {
450 type_: DTGCredentialType::Relationship,
451 version: value.context.as_slice().try_into()?,
452 credential: value,
453 }),
454 DTGCredentialType::Invitation => Ok(DTGCredential {
455 type_: DTGCredentialType::Invitation,
456 version: value.context.as_slice().try_into()?,
457 credential: value,
458 }),
459 DTGCredentialType::Persona => Ok(DTGCredential {
460 type_: DTGCredentialType::Persona,
461 version: value.context.as_slice().try_into()?,
462 credential: value,
463 }),
464 DTGCredentialType::Endorsement => {
465 if let CredentialSubject::Endorsement { .. } = &value.credential_subject {
466 Ok(DTGCredential {
467 type_: DTGCredentialType::Endorsement,
468 version: value.context.as_slice().try_into()?,
469 credential: value,
470 })
471 } else {
472 Err(DTGCredentialError::UnknownCredential)
473 }
474 }
475 DTGCredentialType::Witness => {
476 if value.task_context.is_none() {
480 return Err(DTGCredentialError::MissingTaskContext);
481 }
482
483 match &value.credential_subject {
484 CredentialSubject::Witness(_) => Ok(DTGCredential {
485 type_: DTGCredentialType::Witness,
486 version: value.context.as_slice().try_into()?,
487 credential: value,
488 }),
489 CredentialSubject::Basic(subject) => {
490 Ok(DTGCredential {
492 type_: DTGCredentialType::Witness,
493 version: value.context.as_slice().try_into()?,
494 credential: DTGCommon {
495 credential_subject: CredentialSubject::Witness(
496 CredentialSubjectWitness {
497 id: subject.id.clone(),
498 digest: None,
499 witness_context: None,
500 },
501 ),
502 ..value
503 },
504 })
505 }
506 _ => Err(DTGCredentialError::UnknownCredential),
507 }
508 }
509 DTGCredentialType::RCard => match &value.credential_subject {
510 CredentialSubject::RCard { .. } => Ok(DTGCredential {
511 type_: DTGCredentialType::RCard,
512 version: value.context.as_slice().try_into()?,
513 credential: value,
514 }),
515 _ => Err(DTGCredentialError::UnknownCredential),
516 },
517 }
518 }
519}
520
521fn iso8601_format<S>(timestamp: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
524where
525 S: Serializer,
526{
527 s.serialize_str(
528 timestamp
529 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
530 .as_str(),
531 )
532}
533
534fn iso8601_format_option<S>(timestamp: &Option<DateTime<Utc>>, s: S) -> Result<S::Ok, S::Error>
535where
536 S: Serializer,
537{
538 if let Some(timestamp) = timestamp {
539 s.serialize_str(
540 timestamp
541 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
542 .as_str(),
543 )
544 } else {
545 s.serialize_none()
546 }
547}
548
549#[allow(deprecated)]
558#[derive(Serialize, Deserialize, Debug, Clone)]
559#[serde(untagged)]
560pub enum CredentialSubject {
561 Endorsement(CredentialSubjectEndorsement),
563
564 #[deprecated(
566 since = "0.2.0",
567 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
568 See DTGCredentialType::RCard. This variant will be removed in a future release."
569 )]
570 RCard(CredentialSubjectRCard),
571
572 Basic(CredentialSubjectBasic),
575
576 Witness(CredentialSubjectWitness),
578}
579
580#[derive(Serialize, Deserialize, Debug, Clone)]
582#[serde(deny_unknown_fields)]
583pub struct CredentialSubjectBasic {
584 pub id: String,
585}
586
587#[derive(Serialize, Deserialize, Debug, Clone)]
589#[serde(deny_unknown_fields)]
590pub struct CredentialSubjectEndorsement {
591 pub id: String,
592 pub endorsement: Value,
594}
595
596#[derive(Serialize, Deserialize, Debug, Clone)]
598#[serde(rename_all = "camelCase", deny_unknown_fields)]
599pub struct CredentialSubjectWitness {
600 pub id: String,
601
602 #[serde(skip_serializing_if = "Option::is_none")]
603 pub digest: Option<String>,
604
605 #[serde(skip_serializing_if = "Option::is_none")]
607 pub witness_context: Option<WitnessContext>,
608}
609
610#[derive(Serialize, Deserialize, Debug, Clone)]
612#[serde(rename_all = "camelCase", deny_unknown_fields)]
613pub struct WitnessContext {
614 pub event: Option<String>,
616
617 pub session_id: Option<String>,
619
620 pub method: Option<String>,
622}
623
624#[deprecated(
626 since = "0.2.0",
627 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
628 See DTGCredentialType::RCard. This struct will be removed in a future release."
629)]
630#[derive(Serialize, Deserialize, Debug, Clone)]
631#[serde(deny_unknown_fields)]
632pub struct CredentialSubjectRCard {
633 pub id: String,
634
635 pub card: Value,
637}
638
639#[cfg(test)]
640#[allow(deprecated)]
641mod tests {
642 use crate::{
643 CredentialSubject, CredentialSubjectRCard, DTGCommon, DTGCredential, DTGCredentialType,
644 W3CVCVersion,
645 };
646 use chrono::{DateTime, Utc};
647 use serde_json::Value;
648
649 #[test]
650 fn test_vmc_vc_1_deserialize() {
651 let vmc: DTGCredential = match serde_json::from_str(
653 r#"{
654"@context": [
655 "https://www.w3.org/2018/credentials/v1",
656 "https://firstperson.network/credentials/dtg/v1",
657 "https://w3id.org/security/suites/ed25519-2020/v1"
658 ],
659 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
660 "issuer": "did:web:chess-club.example",
661 "issuanceDate": "2026-01-06T10:00:00Z",
662 "expirationDate": "2027-01-06T10:00:00Z",
663 "credentialSubject": {
664 "id": "did:key:z6MkpTHR8VNs..."
665 }
666 }"#,
667 ) {
668 Ok(vmc) => vmc,
669 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
670 };
671
672 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
673 assert!(matches!(
674 vmc.credential().credential_subject,
675 CredentialSubject::Basic(_)
676 ));
677 assert!(matches!(vmc.version, W3CVCVersion::V1_1));
678 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V1_1));
679 }
680
681 #[test]
682 fn test_missing_w3c_context() {
683 assert!(
685 serde_json::from_str::<DTGCredential>(
686 r#"{
687"@context": [
688 "https://firstperson.network/credentials/dtg/v1",
689 "https://w3id.org/security/suites/ed25519-2020/v1"
690 ],
691 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
692 "issuer": "did:web:chess-club.example",
693 "issuanceDate": "2026-01-06T10:00:00Z",
694 "expirationDate": "2027-01-06T10:00:00Z",
695 "credentialSubject": {
696 "id": "did:key:z6MkpTHR8VNs..."
697 }
698 }"#,
699 )
700 .is_err()
701 );
702 }
703
704 #[test]
705 fn test_mutable_credential() {
706 let mut vmc = DTGCredential::new_vmc(
707 "did:example:issuer".to_string(),
708 "did:example:subject".to_string(),
709 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
710 .unwrap()
711 .with_timezone(&Utc),
712 None,
713 false,
714 );
715
716 let cred = vmc.credential_mut();
717 cred.type_.push("PersonhoodCredential".to_string());
718 assert!(vmc.is_personhood_credential());
719 }
720
721 #[test]
722 fn test_vmc_deserialize() {
723 let vmc: DTGCredential = match serde_json::from_str(
724 r#"{
725 "@context": ["https://www.w3.org/ns/credentials/v2"],
726 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
727 "issuer": "did:example:community",
728 "validFrom": "2024-06-18T10:00:00Z",
729 "credentialSubject": { "id": "did:example:rDid" }
730 }"#,
731 ) {
732 Ok(vmc) => vmc,
733 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
734 };
735
736 assert!(!vmc.is_personhood_credential());
737 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
738 assert!(matches!(
739 vmc.credential().credential_subject,
740 CredentialSubject::Basic(_)
741 ));
742 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V2_0));
743 }
744
745 #[test]
746 fn test_vmc_phc_deserialize() {
747 let vmc: DTGCredential = match serde_json::from_str(
748 r#"{
749 "@context": ["https://www.w3.org/ns/credentials/v2"],
750 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential", "PersonhoodCredential"],
751 "issuer": "did:example:community",
752 "validFrom": "2024-06-18T10:00:00Z",
753 "credentialSubject": { "id": "did:example:rDid" }
754 }"#,
755 ) {
756 Ok(vmc) => vmc,
757 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
758 };
759
760 assert!(vmc.is_personhood_credential());
761 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
762 assert!(matches!(
763 vmc.credential().credential_subject,
764 CredentialSubject::Basic(_)
765 ));
766 }
767
768 #[test]
769 fn test_vrc_deserialize() {
770 let vrc: DTGCredential = match serde_json::from_str(
771 r#"{
772 "@context": ["https://www.w3.org/ns/credentials/v2"],
773 "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"],
774 "issuer": "did:example:governmentAgencyDid",
775 "validFrom": "2024-06-18T10:00:00Z",
776 "credentialSubject": { "id": "did:example:citizenRDid" }
777 }"#,
778 ) {
779 Ok(vrc) => vrc,
780 Err(e) => panic!("Couldn't deserialize VRC: {}", e),
781 };
782
783 assert!(matches!(vrc.type_, DTGCredentialType::Relationship));
784 assert!(matches!(
785 vrc.credential().credential_subject,
786 CredentialSubject::Basic(_)
787 ));
788 }
789
790 #[test]
791 fn test_vic_deserialize() {
792 let vic: DTGCredential = match serde_json::from_str(
793 r#"{
794 "@context": ["https://www.w3.org/ns/credentials/v2"],
795 "type": ["VerifiableCredential", "DTGCredential", "InvitationCredential"],
796 "issuer": "did:example:governmentAgencyVicDid",
797 "validFrom": "2024-06-18T10:00:00Z",
798 "credentialSubject": { "id": "did:example:citizenRDid" }
799 }"#,
800 ) {
801 Ok(vic) => vic,
802 Err(e) => panic!("Couldn't deserialize VIC: {}", e),
803 };
804
805 assert!(!vic.is_personhood_credential());
806 assert!(matches!(vic.type_, DTGCredentialType::Invitation));
807 assert!(matches!(
808 vic.credential().credential_subject,
809 CredentialSubject::Basic(_)
810 ));
811 }
812
813 #[test]
814 fn test_vpc_deserialize() {
815 let vpc: DTGCredential = match serde_json::from_str(
816 r#"{
817 "@context": ["https://www.w3.org/ns/credentials/v2"],
818 "type": ["VerifiableCredential", "DTGCredential", "PersonaCredential"],
819 "issuer": "did:example:governmentAgencyDid",
820 "validFrom": "2024-06-18T10:00:00Z",
821 "credentialSubject": { "id": "did:example:citizenRDid" }
822 }"#,
823 ) {
824 Ok(vpc) => vpc,
825 Err(e) => panic!("Couldn't deserialize VPC: {}", e),
826 };
827
828 assert!(matches!(vpc.type_, DTGCredentialType::Persona));
829 assert!(matches!(
830 vpc.credential().credential_subject,
831 CredentialSubject::Basic(_)
832 ));
833 }
834
835 #[test]
836 fn test_vec_deserialize() {
837 let vec: DTGCredential = match serde_json::from_str(
838 r#"{
839 "@context": ["https://www.w3.org/ns/credentials/v2"],
840 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
841 "issuer": "did:example:governmentAgencyDid",
842 "validFrom": "2024-06-18T10:00:00Z",
843 "credentialSubject": { "id": "did:example:citizenRDid", "endorsement": {} }
844 }"#,
845 ) {
846 Ok(vec) => vec,
847 Err(e) => panic!("Couldn't deserialize VEC: {}", e),
848 };
849
850 assert!(matches!(vec.type_, DTGCredentialType::Endorsement));
851 assert!(matches!(vec.subject(), "did:example:citizenRDid"));
852 assert!(matches!(
853 vec.credential().credential_subject,
854 CredentialSubject::Endorsement(_)
855 ));
856 }
857
858 #[test]
859 fn test_vec_bad_deserialize() {
860 match serde_json::from_str::<DTGCredential>(
861 r#"{
862 "@context": ["https://www.w3.org/ns/credentials/v2"],
863 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
864 "issuer": "did:example:governmentAgencyDid",
865 "validFrom": "2024-06-18T10:00:00Z",
866 "credentialSubject": { "id": "did:example:citizenRDid", "other": [] }
867 }"#,
868 ) {
869 Ok(_) => panic!("Expected Unknown Credential type"),
870 Err(_) => {
871 }
873 };
874 }
875
876 #[test]
877 fn test_vwc_simple_deserialize() {
878 let vwc: DTGCredential = match serde_json::from_str(
879 r#"{
880 "@context": ["https://www.w3.org/ns/credentials/v2"],
881 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
882 "issuer": "did:example:governmentAgencyDid",
883 "validFrom": "2024-06-18T10:00:00Z",
884 "taskContext": "thread-abc-123",
885 "credentialSubject": { "id": "did:example:citizenRDid" }
886 }"#,
887 ) {
888 Ok(vwc) => vwc,
889 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
890 };
891
892 assert!(matches!(vwc.type_, DTGCredentialType::Witness));
893 assert!(matches!(vwc.subject(), "did:example:citizenRDid"));
894 assert_eq!(vwc.task_context(), Some("thread-abc-123"));
895 assert!(matches!(
896 vwc.credential().credential_subject,
897 CredentialSubject::Witness(_)
898 ));
899 }
900
901 #[test]
902 fn test_vwc_full_deserialize() {
903 let vwc: DTGCredential = match serde_json::from_str(
904 r#"{
905 "@context": ["https://www.w3.org/ns/credentials/v2"],
906 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
907 "issuer": "did:example:governmentAgencyDid",
908 "validFrom": "2024-06-18T10:00:00Z",
909 "taskContext": "thread-abc-123",
910 "credentialSubject": { "id": "did:example:citizenRDid", "digest": "abcdf", "witnessContext": {} }
911 }"#,
912 ) {
913 Ok(vwc) => vwc,
914 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
915 };
916
917 assert!(matches!(vwc.type_(), DTGCredentialType::Witness));
918 assert!(matches!(
919 vwc.credential().credential_subject,
920 CredentialSubject::Witness(_)
921 ));
922 }
923
924 #[test]
925 fn test_vwc_bad_deserialize() {
926 if serde_json::from_str::<DTGCredential>(
927 r#"{
928 "@context": ["https://www.w3.org/ns/credentials/v2"],
929 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
930 "issuer": "did:example:governmentAgencyDid",
931 "validFrom": "2024-06-18T10:00:00Z",
932 "taskContext": "thread-abc-123",
933 "credentialSubject": { "id": "did:example:citizenRDid", "digest": "abcdf", "wrongContext": {} }
934 }"#,
935 ).is_ok() {
936 panic!("Should have failed due to wrong CredentialSubject!");
937 }
938 }
939
940 #[test]
941 fn test_rcard_simple_deserialize() {
942 let rcard: DTGCredential = match serde_json::from_str(
943 r#"{
944 "@context": ["https://www.w3.org/ns/credentials/v2"],
945 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
946 "issuer": "did:example:governmentAgencyDid",
947 "validFrom": "2024-06-18T10:00:00Z",
948 "credentialSubject": { "id": "did:example:citizenRDid", "card": [] }
949 }"#,
950 ) {
951 Ok(rcard) => rcard,
952 Err(e) => panic!("Couldn't deserialize R-Card: {}", e),
953 };
954
955 assert!(matches!(rcard.type_(), DTGCredentialType::RCard));
956 assert!(matches!(rcard.subject(), "did:example:citizenRDid"));
957 assert!(matches!(
958 rcard.credential().credential_subject,
959 CredentialSubject::RCard(_)
960 ));
961 }
962
963 #[test]
964 fn test_rcard_bad_deserialize() {
965 if serde_json::from_str::<DTGCredential>(
966 r#"{
967 "@context": ["https://www.w3.org/ns/credentials/v2"],
968 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
969 "issuer": "did:example:governmentAgencyDid",
970 "validFrom": "2024-06-18T10:00:00Z",
971 "credentialSubject": { "id": "did:example:citizenRDid" }
972 }"#,
973 )
974 .is_ok()
975 {
976 panic!("Should have failed due to wrong CredentialSubject!");
977 }
978 }
979 #[test]
980 fn test_deserialize_unknown() {
981 match serde_json::from_str::<DTGCredential>(
982 r#"{
983 "@context": ["https://www.w3.org/ns/credentials/v2"],
984 "type": ["VerifiableCredential", "DTGCredential", "UnknownCredential"],
985 "issuer": "did:example:governmentAgencyDid",
986 "validFrom": "2024-06-18T10:00:00Z",
987 "credentialSubject": { "id": "did:example:citizenRDid" }
988 }"#,
989 ) {
990 Ok(_) => panic!("Expected Unknown Credential type"),
991 Err(e) => {
992 if e.to_string() == "Unknown credential type" {
993 } else {
995 panic!("Wrong error type returned");
996 }
997 }
998 };
999 }
1000
1001 #[test]
1002 fn test_deserialize_mismatched_credential_subject() {
1003 match serde_json::from_str::<DTGCredential>(
1004 r#"{
1005 "@context": ["https://www.w3.org/ns/credentials/v2"],
1006 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
1007 "issuer": "did:example:governmentAgencyDid",
1008 "validFrom": "2024-06-18T10:00:00Z",
1009 "credentialSubject": { "id": "did:example:citizenRDid" }
1010 }"#,
1011 ) {
1012 Ok(_) => panic!("Expected Unknown Credential type"),
1013 Err(e) => {
1014 if e.to_string() == "Unknown credential type" {
1015 } else {
1017 panic!("Wrong error type returned");
1018 }
1019 }
1020 };
1021 }
1022
1023 #[test]
1024 fn test_proof_signed() {
1025 let cred: DTGCredential = match serde_json::from_str(
1026 r#"{
1027 "@context": ["https://www.w3.org/ns/credentials/v2"],
1028 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1029 "issuer": "did:example:community",
1030 "validFrom": "2024-06-18T10:00:00Z",
1031 "credentialSubject": { "id": "did:example:rDid" },
1032 "proof": {
1033 "type": "DataIntegrityProof",
1034 "cryptosuite": "eddsa-jcs-2022",
1035 "created": "2025-12-04T00:00:00",
1036 "verificationMethod": "did:example:test#key-1",
1037 "proofPurpose": "assertionMethod",
1038 "proofValue": "abcd"
1039 }
1040 }"#,
1041 ) {
1042 Ok(vmc) => vmc,
1043 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1044 };
1045
1046 assert!(cred.signed());
1047 assert!(cred.proof_value().is_some());
1048 }
1049
1050 #[test]
1051 fn test_proof_not_signed() {
1052 let cred: DTGCredential = match serde_json::from_str(
1053 r#"{
1054 "@context": ["https://www.w3.org/ns/credentials/v2"],
1055 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1056 "issuer": "did:example:community",
1057 "validFrom": "2024-06-18T10:00:00Z",
1058 "credentialSubject": { "id": "did:example:rDid" }
1059 }"#,
1060 ) {
1061 Ok(vmc) => vmc,
1062 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1063 };
1064
1065 assert!(!cred.signed());
1066 assert!(cred.proof_value().is_none());
1067 }
1068
1069 #[test]
1070 fn test_helpers() {
1071 let cred: DTGCredential = match serde_json::from_str(
1072 r#"{
1073 "@context": ["https://www.w3.org/ns/credentials/v2"],
1074 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1075 "issuer": "did:example:issuer",
1076 "validFrom": "2024-06-18T00:00:00Z",
1077 "credentialSubject": { "id": "did:example:subject" }
1078 }"#,
1079 ) {
1080 Ok(vmc) => vmc,
1081 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1082 };
1083
1084 assert_eq!(cred.issuer(), "did:example:issuer");
1085 assert_eq!(cred.subject(), "did:example:subject");
1086 assert_eq!(
1087 cred.valid_from()
1088 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1089 "2024-06-18T00:00:00Z"
1090 );
1091 assert_eq!(cred.valid_until(), None);
1092 }
1093
1094 #[test]
1095 fn test_valid_until() {
1096 let cred: DTGCredential = match serde_json::from_str(
1097 r#"{
1098 "@context": ["https://www.w3.org/ns/credentials/v2"],
1099 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1100 "issuer": "did:example:issuer",
1101 "validFrom": "2024-06-18T00:00:00Z",
1102 "validUntil": "2030-01-01T00:00:00Z",
1103 "credentialSubject": { "id": "did:example:subject" }
1104 }"#,
1105 ) {
1106 Ok(vmc) => vmc,
1107 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1108 };
1109
1110 assert_eq!(
1111 cred.valid_until()
1112 .unwrap()
1113 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1114 "2030-01-01T00:00:00Z"
1115 );
1116 }
1117
1118 #[test]
1119 fn test_bad_type() {
1120 assert!(
1121 std::convert::TryInto::<DTGCredentialType>::try_into(
1122 vec!["bad_type".to_string()].as_slice(),
1123 )
1124 .is_err()
1125 );
1126 }
1127
1128 #[test]
1129 fn test_badly_constructed_vwc() {
1130 let mut cred = DTGCommon::default();
1131 cred.type_.push("WitnessCredential".to_string());
1132 cred.task_context = Some("thread-abc-123".to_string());
1135 cred.credential_subject = CredentialSubject::RCard(CredentialSubjectRCard {
1136 id: "did:example:bad".to_string(),
1137 card: Value::Null,
1138 });
1139
1140 assert!(std::convert::TryInto::<DTGCredential>::try_into(cred).is_err());
1141 }
1142
1143 #[test]
1144 fn test_vwc_missing_task_context() {
1145 match serde_json::from_str::<DTGCredential>(
1147 r#"{
1148 "@context": ["https://www.w3.org/ns/credentials/v2"],
1149 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1150 "issuer": "did:example:witness",
1151 "validFrom": "2024-06-18T10:00:00Z",
1152 "credentialSubject": { "id": "did:example:observed" }
1153 }"#,
1154 ) {
1155 Ok(_) => panic!("Expected a VWC without taskContext to be rejected"),
1156 Err(e) => assert_eq!(
1157 e.to_string(),
1158 "WitnessCredential is missing the required taskContext property"
1159 ),
1160 }
1161 }
1162
1163 #[test]
1164 fn test_task_context_round_trip() {
1165 let raw = r#"{
1168 "@context": ["https://www.w3.org/ns/credentials/v2"],
1169 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1170 "issuer": "did:example:witness",
1171 "validFrom": "2024-06-18T10:00:00Z",
1172 "taskContext": "thread-abc-123",
1173 "credentialSubject": { "id": "did:example:observed" }
1174 }"#;
1175
1176 let cred: DTGCredential = serde_json::from_str(raw).unwrap();
1177 let out = serde_json::to_string(&cred).unwrap();
1178
1179 assert!(out.contains(r#""taskContext":"thread-abc-123""#));
1180 }
1181
1182 #[test]
1183 fn test_task_context_optional_on_other_types() {
1184 let vrc: DTGCredential = serde_json::from_str(
1186 r#"{
1187 "@context": ["https://www.w3.org/ns/credentials/v2"],
1188 "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"],
1189 "issuer": "did:example:issuer",
1190 "validFrom": "2024-06-18T10:00:00Z",
1191 "credentialSubject": { "id": "did:example:subject" }
1192 }"#,
1193 )
1194 .unwrap();
1195
1196 assert_eq!(vrc.task_context(), None);
1197 assert!(!serde_json::to_string(&vrc).unwrap().contains("taskContext"));
1199 }
1200
1201 #[test]
1202 fn test_digest_multibase() {
1203 let vrc = DTGCredential::new_vrc(
1204 "did:example:issuer".to_string(),
1205 "did:example:subject".to_string(),
1206 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1207 .unwrap()
1208 .with_timezone(&Utc),
1209 None,
1210 );
1211
1212 let digest = vrc.digest_multibase().unwrap();
1213
1214 assert!(digest.starts_with('z'));
1216
1217 let (base, bytes) = multibase::decode(&digest).unwrap();
1219 assert_eq!(base, multibase::Base::Base58Btc);
1220 assert_eq!(bytes.len(), 34);
1221 assert_eq!(&bytes[..2], &[0x12, 0x20]);
1222
1223 assert_eq!(digest, vrc.digest_multibase().unwrap());
1225
1226 let other = DTGCredential::new_vrc(
1228 "did:example:issuer".to_string(),
1229 "did:example:someone-else".to_string(),
1230 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1231 .unwrap()
1232 .with_timezone(&Utc),
1233 None,
1234 );
1235 assert_ne!(digest, other.digest_multibase().unwrap());
1236 }
1237
1238 #[test]
1239 fn test_verify_digest() {
1240 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1241 .unwrap()
1242 .with_timezone(&Utc);
1243
1244 let vrc = DTGCredential::new_vrc(
1245 "did:example:issuer".to_string(),
1246 "did:example:subject".to_string(),
1247 valid_from,
1248 None,
1249 );
1250
1251 let vwc = DTGCredential::new_vwc(
1252 "did:example:witness".to_string(),
1253 "did:example:issuer".to_string(),
1255 valid_from,
1256 None,
1257 "thread-abc-123".to_string(),
1258 Some(vrc.digest_multibase().unwrap()),
1259 None,
1260 );
1261
1262 assert!(vwc.verify_digest(&vrc).unwrap());
1263
1264 let other = DTGCredential::new_vrc(
1266 "did:example:issuer".to_string(),
1267 "did:example:someone-else".to_string(),
1268 valid_from,
1269 None,
1270 );
1271 assert!(!vwc.verify_digest(&other).unwrap());
1272 }
1273
1274 #[test]
1275 fn test_verify_digest_without_digest() {
1276 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1277 .unwrap()
1278 .with_timezone(&Utc);
1279
1280 let vrc = DTGCredential::new_vrc(
1281 "did:example:issuer".to_string(),
1282 "did:example:subject".to_string(),
1283 valid_from,
1284 None,
1285 );
1286
1287 let vwc = DTGCredential::new_vwc(
1289 "did:example:witness".to_string(),
1290 "did:example:issuer".to_string(),
1291 valid_from,
1292 None,
1293 "thread-abc-123".to_string(),
1294 None,
1295 None,
1296 );
1297
1298 assert!(!vwc.verify_digest(&vrc).unwrap());
1299 }
1300
1301 #[test]
1302 fn test_iso8601_format_option() {
1303 let now: DateTime<Utc> = DateTime::parse_from_rfc3339(
1304 &Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1305 )
1306 .unwrap()
1307 .to_utc();
1308 let cred = DTGCommon {
1309 valid_until: Some(now),
1310 ..Default::default()
1311 };
1312
1313 let value = serde_json::to_value(&cred).unwrap();
1314 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
1315 assert_eq!(cred2.valid_until, Some(now));
1316
1317 let cred = DTGCommon::default();
1318 let value = serde_json::to_value(&cred).unwrap();
1319 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
1320 assert_eq!(cred2.valid_until, None);
1321 }
1322
1323 #[cfg(feature = "affinidi-signing")]
1324 #[tokio::test]
1325 async fn test_signing() {
1326 use affinidi_secrets_resolver::secrets::Secret;
1327
1328 let secret = Secret::generate_ed25519(None, None);
1329
1330 let mut cred = DTGCredential::new_vrc(
1331 "did:example:issuer".to_string(),
1332 "did:example:subject".to_string(),
1333 Utc::now(),
1334 None,
1335 );
1336
1337 assert!(cred.sign(&secret, Some(Utc::now())).await.is_ok());
1338
1339 assert!(
1340 cred.verify_proof_with_public_key(secret.get_public_bytes())
1341 .is_ok()
1342 );
1343
1344 let secret2 = Secret::generate_ed25519(None, None);
1345 assert!(
1346 cred.verify_proof_with_public_key(secret2.get_public_bytes())
1347 .is_err()
1348 );
1349 }
1350
1351 #[cfg(feature = "affinidi-signing")]
1352 #[tokio::test]
1353 async fn test_signing_error() {
1354 use affinidi_secrets_resolver::secrets::Secret;
1355
1356 let secret = Secret::generate_x25519(None, None).unwrap();
1357
1358 let mut cred = DTGCredential::new_vrc(
1359 "did:example:issuer".to_string(),
1360 "did:example:subject".to_string(),
1361 Utc::now(),
1362 None,
1363 );
1364
1365 assert!(cred.sign(&secret, Some(Utc::now())).await.is_err());
1366 }
1367
1368 #[cfg(feature = "affinidi-signing")]
1369 #[test]
1370 fn test_signing_no_proof() {
1371 use crate::DTGCredentialError;
1372 use affinidi_secrets_resolver::secrets::Secret;
1373
1374 let cred = DTGCredential::new_vrc(
1375 "did:example:issuer".to_string(),
1376 "did:example:subject".to_string(),
1377 Utc::now(),
1378 None,
1379 );
1380
1381 let secret = Secret::generate_ed25519(None, None);
1382 match cred.verify_proof_with_public_key(secret.get_public_bytes()) {
1383 Err(DTGCredentialError::NotSigned) => {
1384 }
1386 _ => panic!("Expected NotSigned error!"),
1387 }
1388 }
1389}