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 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/// What W3C VC Format is the credential using?
20#[derive(Clone, Copy, Debug)]
21pub enum W3CVCVersion {
22    /// https://www.w3.org/2018/credentials/v1
23    V1_1,
24
25    /// https://www.w3.org/ns/credentials/v2
26    V2_0,
27}
28
29impl TryFrom<&[String]> for W3CVCVersion {
30    type Error = DTGCredentialError;
31
32    /// Will return the W3C Version from the context array
33    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/// Errors related to DTG Credentials
45#[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    /// A WitnessCredential (VWC) was missing the REQUIRED `taskContext` property
61    #[error("WitnessCredential is missing the required taskContext property")]
62    MissingTaskContext,
63
64    /// The credential could not be canonicalized (JCS, RFC 8785) for digesting
65    #[error("Could not canonicalize credential: {0}")]
66    Canonicalization(String),
67}
68
69/// Defined DTG Credentials
70#[derive(Serialize, Deserialize, Debug, Clone)]
71#[serde(try_from = "DTGCommon")]
72pub struct DTGCredential {
73    /// The DTG Credential inner struct
74    #[serde(flatten)]
75    credential: DTGCommon,
76
77    /// Type of the credential
78    #[serde(skip)]
79    type_: DTGCredentialType,
80
81    /// W3C VC Version
82    #[serde(skip)]
83    version: W3CVCVersion,
84}
85
86impl DTGCredential {
87    /// get the raw credential
88    pub fn credential(&self) -> &DTGCommon {
89        &self.credential
90    }
91
92    /// Get the raw credential as mutable
93    pub fn credential_mut(&mut self) -> &mut DTGCommon {
94        &mut self.credential
95    }
96
97    /// Has this credential been signed?
98    pub fn signed(&self) -> bool {
99        self.credential.signed()
100    }
101
102    /// get the credential type
103    pub fn type_(&self) -> DTGCredentialType {
104        self.type_.clone()
105    }
106
107    /// Returns the Issuer DID
108    pub fn issuer(&self) -> &str {
109        self.credential.issuer()
110    }
111
112    /// Returns the Subject DID
113    pub fn subject(&self) -> &str {
114        self.credential.subject()
115    }
116
117    /// Returns the valid_from timestamp
118    pub fn valid_from(&self) -> DateTime<Utc> {
119        self.credential.valid_from()
120    }
121
122    /// Returns the valid until timestamp
123    pub fn valid_until(&self) -> Option<DateTime<Utc>> {
124        self.credential.valid_until()
125    }
126
127    /// The `threadId` of the trust task exchange this credential was issued in, if set
128    ///
129    /// This is always `Some` for [DTGCredentialType::Witness] credentials, where the spec
130    /// makes `taskContext` REQUIRED.
131    pub fn task_context(&self) -> Option<&str> {
132        self.credential.task_context()
133    }
134
135    /// Computes the digest of this credential, for use as the `digest` property of a
136    /// Witness Credential (VWC) attesting it.
137    ///
138    /// The digest is the SHA-256 hash of this credential canonicalized with the JSON
139    /// Canonicalization Scheme ([JCS, RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785)),
140    /// wrapped as a multihash and encoded as a base58btc multibase string (`z...`), matching
141    /// the W3C `digestMultibase` convention.
142    ///
143    /// NOTE: This intentionally differs from the DTG Core Credentials Working Draft 01, which
144    /// specifies a `sha256:<lowercase-hex>` encoding. The underlying hash is the same; only the
145    /// encoding differs. See CHANGELOG.md.
146    ///
147    /// The digest covers the credential exactly as it stands, including its `proof` if it has
148    /// been signed. A witness should therefore digest the VRC in the form it was witnessed in.
149    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        // multihash prefix: 0x12 = sha2-256, 0x20 = 32 byte digest length
154        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    /// Checks that this credential's `digest` matches the credential it claims to witness.
162    ///
163    /// Returns `Ok(false)` if the digests do not match, or if this credential carries no
164    /// `digest` (it is OPTIONAL), in which case there is nothing to rely on.
165    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    /// Returns the proof value if signed else None
178    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    /// Sign the credential using W3C Data Integrity Proof with JCS EdDSA 2022
188    /// signing_secret: The secret key to use to sign the credential
189    /// create_time: Optional creation time for the proof, defaults to now if None
190    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    /// Verify the credential if you already know the public key bytes
208    /// otherwise use the affinidi_tdk:verify_data() method
209    /// public_key_bytes: The public key bytes to use to verify the credential
210    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    /// Is this credential a W3C VC Version 1.1 or 2.0 credential?
233    pub fn get_w3c_vc_version(&self) -> W3CVCVersion {
234        self.version
235    }
236
237    /// returns true if this credential a personhood credential (PHC)
238    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/// TDG VC Type Identifiers
250#[derive(Debug, Clone)]
251#[non_exhaustive]
252pub enum DTGCredentialType {
253    Membership,
254    Relationship,
255    Invitation,
256    Persona,
257    Endorsement,
258    Witness,
259
260    /// R-Card is no longer a DTG credential type.
261    #[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
286/// This helps with matching the right credential type to the [DTGCredentialType]
287const 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/// All DTG Credentials follow a common structure.
320#[derive(Serialize, Deserialize, Debug, Clone)]
321#[serde(rename_all = "camelCase")]
322pub struct DTGCommon {
323    /// JSON-LD links to contexts
324    /// Must contain at least:
325    /// https://www.w3.org/ns/credentials/v2
326    /// https://firstperson.network/credentials/dtg/v1
327    #[serde(rename = "@context")]
328    pub context: Vec<String>,
329
330    /// Credential type identifiers
331    /// Must contain at least:
332    /// DTGCredential
333    /// VerifiableCredential
334    #[serde(rename = "type")]
335    pub type_: Vec<String>,
336
337    /// DID of the entity issuing this credential
338    pub issuer: String,
339
340    /// ISO 8601 format of when this credentials become valid from
341    #[serde(serialize_with = "iso8601_format", alias = "issuanceDate")]
342    pub valid_from: DateTime<Utc>,
343
344    /// ISO 8601 format of when these credentials are valid to
345    #[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    /// Identifier (`threadId`) of the trust task exchange in which this credential was issued.
354    ///
355    /// REQUIRED for [DTGCredentialType::Witness] credentials, OPTIONAL for all other DTG
356    /// credential types. A DTG credential without a `taskContext` MUST be interpretable
357    /// standing alone, independent of any exchange.
358    ///
359    /// NOTE: A verifier MUST NOT interpret a `taskContext`-bearing credential as proof that
360    /// the associated trust task completed unless the matching trust task outcome evidence is
361    /// also present and verified.
362    #[serde(skip_serializing_if = "Option::is_none", default)]
363    pub task_context: Option<String>,
364
365    /// The assertion between the entities involved
366    pub credential_subject: CredentialSubject,
367
368    /// Cryptographic proof of credential authenticity
369    #[serde(skip_serializing_if = "Option::is_none", default)]
370    pub proof: Option<DataIntegrityProof>,
371}
372
373impl DTGCommon {
374    /// Has this credential been signed?
375    /// Returns true if a proof exists
376    /// NOTE: This does NOT validate the proof itself
377    pub fn signed(&self) -> bool {
378        self.proof.is_some()
379    }
380
381    /// Returns the issuer DID
382    pub fn issuer(&self) -> &str {
383        &self.issuer
384    }
385
386    /// Returns the subject DID
387    #[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    /// The credential is valid from this timestamp
398    pub fn valid_from(&self) -> DateTime<Utc> {
399        self.valid_from
400    }
401
402    /// The credential is valid until this timestamp, if set
403    pub fn valid_until(&self) -> Option<DateTime<Utc>> {
404        self.valid_until
405    }
406
407    /// The `threadId` of the trust task exchange this credential was issued in, if set
408    pub fn task_context(&self) -> Option<&str> {
409        self.task_context.as_deref()
410    }
411}
412
413/// Helps ensure default starting point is correct
414impl 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
437/// Post deserialize setup of a CredentialSubject and CredntialType
438impl 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                // taskContext is REQUIRED on a VWC: the meaning of a witness attestation
477                // depends on the conditions it was made under, which live in the trust task
478                // exchange it is bound to.
479                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                        // If Witness CredentialSubject only contains id, it is still valid
491                        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
521/// This correctly formats timestamps into the correct iso8601 specification for W3C Verifiable
522/// Credentials
523fn 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// ****************************************************************************
550// Credential Subject types
551// ****************************************************************************
552// NOTE: The DTG credential spec overloads the JSON attributes for different credential payloads.
553// The following enum will map the credential subject schema to correct Struct type
554
555/// This represents all possible credential subjects
556/// The order of the enum is important as it will match on first match
557#[allow(deprecated)]
558#[derive(Serialize, Deserialize, Debug, Clone)]
559#[serde(untagged)]
560pub enum CredentialSubject {
561    /// Verifiable Endorsement Credential subject
562    Endorsement(CredentialSubjectEndorsement),
563
564    /// R-Card Credential subject
565    #[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    /// Credential Subject of just `id`
573    /// Use by  VMC, VRC, VIC and VPC
574    Basic(CredentialSubjectBasic),
575
576    /// Verifiable Witness Credential subject
577    Witness(CredentialSubjectWitness),
578}
579
580/// id of the credential subject only
581#[derive(Serialize, Deserialize, Debug, Clone)]
582#[serde(deny_unknown_fields)]
583pub struct CredentialSubjectBasic {
584    pub id: String,
585}
586
587/// Endorsement Credential subject
588#[derive(Serialize, Deserialize, Debug, Clone)]
589#[serde(deny_unknown_fields)]
590pub struct CredentialSubjectEndorsement {
591    pub id: String,
592    /// There is no spec for the endorsement content, so we use a generic JSON value
593    pub endorsement: Value,
594}
595
596/// Witness Credential subject
597#[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    /// There is no spec for the witness context content, so we use a generic JSON value
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub witness_context: Option<WitnessContext>,
608}
609
610/// Witness Credential Context
611#[derive(Serialize, Deserialize, Debug, Clone)]
612#[serde(rename_all = "camelCase", deny_unknown_fields)]
613pub struct WitnessContext {
614    /// Human-readable event name
615    pub event: Option<String>,
616
617    /// Session or nonce identifier
618    pub session_id: Option<String>,
619
620    ///Verification method used
621    pub method: Option<String>,
622}
623
624/// R-Card Credential subject
625#[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    /// JCard spec, generic JSON value
636    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        // tests deserialize a W3C VC Version 1.1 credential
652        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        // tests deserialize a W3C VC Version 1.1 credential
684        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                // Good
872            }
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                    // test passed
994                } 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                    // test passed
1016                } 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        // taskContext is set so this exercises the credentialSubject mismatch, not the
1133        // missing-taskContext path covered by test_vwc_missing_task_context()
1134        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        // taskContext is REQUIRED on a VWC
1146        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        // taskContext must survive deserialize -> serialize, otherwise a credential signed
1166        // elsewhere would fail verification here (and vice versa)
1167        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        // taskContext is OPTIONAL everywhere except the VWC
1185        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        // and it is omitted from the serialization entirely when absent
1198        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        // base58btc multibase prefix
1215        assert!(digest.starts_with('z'));
1216
1217        // decodes to a sha2-256 multihash: 0x12 0x20 followed by 32 digest bytes
1218        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        // stable across calls
1224        assert_eq!(digest, vrc.digest_multibase().unwrap());
1225
1226        // and distinct for a different credential
1227        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            // the DID of the issuer of the VRC being attested
1254            "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        // a different VRC must not match
1265        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        // digest is OPTIONAL - with none present there is nothing to rely on
1288        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                // Good
1385            }
1386            _ => panic!("Expected NotSigned error!"),
1387        }
1388    }
1389}