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