Skip to main content

dtg_credentials/
lib.rs

1/*! Decentralized Trust Graph (DTG) Credentials
2*/
3
4use 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/// What W3C VC Format is the credential using?
18#[derive(Clone, Copy, Debug)]
19pub enum W3CVCVersion {
20    /// https://www.w3.org/2018/credentials/v1
21    V1_1,
22
23    /// https://www.w3.org/ns/credentials/v2
24    V2_0,
25}
26
27impl TryFrom<&[String]> for W3CVCVersion {
28    type Error = DTGCredentialError;
29
30    /// Will return the W3C Version from the context array
31    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/// Errors related to DTG Credentials
43#[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/// Defined DTG Credentials
60#[derive(Serialize, Deserialize, Debug, Clone)]
61#[serde(try_from = "DTGCommon")]
62pub struct DTGCredential {
63    /// The DTG Credential inner struct
64    #[serde(flatten)]
65    credential: DTGCommon,
66
67    /// Type of the credential
68    #[serde(skip)]
69    type_: DTGCredentialType,
70
71    /// W3C VC Version
72    #[serde(skip)]
73    version: W3CVCVersion,
74}
75
76impl DTGCredential {
77    /// get the raw credential
78    pub fn credential(&self) -> &DTGCommon {
79        &self.credential
80    }
81
82    /// Get the raw credential as mutable
83    pub fn credential_mut(&mut self) -> &mut DTGCommon {
84        &mut self.credential
85    }
86
87    /// Has this credential been signed?
88    pub fn signed(&self) -> bool {
89        self.credential.signed()
90    }
91
92    /// get the credential type
93    pub fn type_(&self) -> DTGCredentialType {
94        self.type_.clone()
95    }
96
97    /// Returns the Issuer DID
98    pub fn issuer(&self) -> &str {
99        self.credential.issuer()
100    }
101
102    /// Returns the Subject DID
103    pub fn subject(&self) -> &str {
104        self.credential.subject()
105    }
106
107    /// Returns the valid_from timestamp
108    pub fn valid_from(&self) -> DateTime<Utc> {
109        self.credential.valid_from()
110    }
111
112    /// Returns the valid until timestamp
113    pub fn valid_until(&self) -> Option<DateTime<Utc>> {
114        self.credential.valid_until()
115    }
116
117    /// Returns the proof value if signed else None
118    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    /// Sign the credential using W3C Data Integrity Proof with JCS EdDSA 2022
128    /// signing_secret: The secret key to use to sign the credential
129    /// create_time: Optional creation time for the proof, defaults to now if None
130    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    /// Verify the credential if you already know the public key bytes
148    /// otherwise use the affinidi_tdk:verify_data() method
149    /// public_key_bytes: The public key bytes to use to verify the credential
150    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    /// Is this credential a W3C VC Version 1.1 or 2.0 credential?
173    pub fn get_w3c_vc_version(&self) -> W3CVCVersion {
174        self.version
175    }
176
177    /// returns true if this credential a personhood credential (PHC)
178    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/// TDG VC Type Identifiers
190#[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
216/// This helps with matching the right credential type to the [DTGCredentialType]
217const 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/// All DTG Credentials follow a common structure.
249#[derive(Serialize, Deserialize, Debug, Clone)]
250#[serde(rename_all = "camelCase")]
251pub struct DTGCommon {
252    /// JSON-LD links to contexts
253    /// Must contain at least:
254    /// https://www.w3.org/ns/credentials/v2
255    /// https://firstperson.network/credentials/dtg/v1
256    #[serde(rename = "@context")]
257    pub context: Vec<String>,
258
259    /// Credential type identifiers
260    /// Must contain at least:
261    /// DTGCredential
262    /// VerifiableCredential
263    #[serde(rename = "type")]
264    pub type_: Vec<String>,
265
266    /// DID of the entity issuing this credential
267    pub issuer: String,
268
269    /// ISO 8601 format of when this credentials become valid from
270    #[serde(serialize_with = "iso8601_format", alias = "issuanceDate")]
271    pub valid_from: DateTime<Utc>,
272
273    /// ISO 8601 format of when these credentials are valid to
274    #[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    /// The assertion between the entities involved
283    pub credential_subject: CredentialSubject,
284
285    /// Cryptographic proof of credential authenticity
286    #[serde(skip_serializing_if = "Option::is_none", default)]
287    pub proof: Option<DataIntegrityProof>,
288}
289
290impl DTGCommon {
291    /// Has this credential been signed?
292    /// Returns true if a proof exists
293    /// NOTE: This does NOT validate the proof itself
294    pub fn signed(&self) -> bool {
295        self.proof.is_some()
296    }
297
298    /// Returns the issuer DID
299    pub fn issuer(&self) -> &str {
300        &self.issuer
301    }
302
303    /// Returns the subject DID
304    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    /// The credential is valid from this timestamp
314    pub fn valid_from(&self) -> DateTime<Utc> {
315        self.valid_from
316    }
317
318    /// The credential is valid until this timestamp, if set
319    pub fn valid_until(&self) -> Option<DateTime<Utc>> {
320        self.valid_until
321    }
322}
323
324/// Helps ensure default starting point is correct
325impl 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
347/// Post deserialize setup of a CredentialSubject and CredntialType
348impl 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                    // If Wtiness CredentialSubject only contains id, it is still valid
392                    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
421/// This correctly formats timestamps into the correct iso8601 specification for W3C Verifiable
422/// Credentials
423fn 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// ****************************************************************************
450// Credential Subject types
451// ****************************************************************************
452// NOTE: The DTG credential spec overloads the JSON attributes for different credential payloads.
453// The following enum will map the credential subject schema to correct Struct type
454
455/// This represents all possible credential subjects
456/// The order of the enum is important as it will match on first match
457#[derive(Serialize, Deserialize, Debug, Clone)]
458#[serde(untagged)]
459pub enum CredentialSubject {
460    /// Verifiable Endorsement Credential subject
461    Endorsement(CredentialSubjectEndorsement),
462
463    /// R-Card Credential subject
464    RCard(CredentialSubjectRCard),
465
466    /// Credential Subject of just `id`
467    /// Use by  VMC, VRC, VIC and VPC
468    Basic(CredentialSubjectBasic),
469
470    /// Verifiable Witness Credential subject
471    Witness(CredentialSubjectWitness),
472}
473
474/// id of the credential subject only
475#[derive(Serialize, Deserialize, Debug, Clone)]
476#[serde(deny_unknown_fields)]
477pub struct CredentialSubjectBasic {
478    pub id: String,
479}
480
481/// Endorsement Credential subject
482#[derive(Serialize, Deserialize, Debug, Clone)]
483#[serde(deny_unknown_fields)]
484pub struct CredentialSubjectEndorsement {
485    pub id: String,
486    /// There is no spec for the endorsement content, so we use a generic JSON value
487    pub endorsement: Value,
488}
489
490/// Witness Credential subject
491#[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    /// There is no spec for the witness context content, so we use a generic JSON value
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub witness_context: Option<WitnessContext>,
502}
503
504/// Witness Credential Context
505#[derive(Serialize, Deserialize, Debug, Clone)]
506#[serde(rename_all = "camelCase", deny_unknown_fields)]
507pub struct WitnessContext {
508    /// Human-readable event name
509    pub event: Option<String>,
510
511    /// Session or nonce identifier
512    pub session_id: Option<String>,
513
514    ///Verification method used
515    pub method: Option<String>,
516}
517
518/// R-Card Credential subject
519#[derive(Serialize, Deserialize, Debug, Clone)]
520#[serde(deny_unknown_fields)]
521pub struct CredentialSubjectRCard {
522    pub id: String,
523
524    /// JCard spec, generic JSON value
525    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        // tests deserialize a W3C VC Version 1.1 credential
540        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        // tests deserialize a W3C VC Version 1.1 credential
572        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                // Good
760            }
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                    // test passed
878                } 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                    // test passed
900                } 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                // Good
1108            }
1109            _ => panic!("Expected NotSigned error!"),
1110        }
1111    }
1112}