1use affinidi_data_integrity::DataIntegrityProof;
5#[cfg(feature = "affinidi-signing")]
6use affinidi_data_integrity::{DataIntegrityError, SignOptions, VerifyOptions};
7#[cfg(feature = "affinidi-signing")]
8use affinidi_secrets_resolver::secrets::Secret;
9use chrono::{DateTime, Utc};
10use multibase::Base;
11use serde::{Deserialize, Serialize, Serializer};
12use serde_json::Value;
13use sha2::{Digest, Sha256};
14use std::fmt::Display;
15use thiserror::Error;
16
17pub mod authority;
18pub mod create;
19pub mod delegation;
20
21#[derive(Clone, Copy, Debug)]
23pub enum W3CVCVersion {
24 V1_1,
26
27 V2_0,
29}
30
31impl TryFrom<&[String]> for W3CVCVersion {
32 type Error = DTGCredentialError;
33
34 fn try_from(types: &[String]) -> Result<Self, Self::Error> {
36 if types.contains(&"https://www.w3.org/2018/credentials/v1".to_string()) {
37 Ok(W3CVCVersion::V1_1)
38 } else if types.contains(&"https://www.w3.org/ns/credentials/v2".to_string()) {
39 Ok(W3CVCVersion::V2_0)
40 } else {
41 Err(DTGCredentialError::UnknownVCVersion)
42 }
43 }
44}
45
46#[derive(Error, Debug)]
48pub enum DTGCredentialError {
49 #[error("Unknown credential type")]
50 UnknownCredential,
51
52 #[cfg(feature = "affinidi-signing")]
53 #[error("Data Integrity Error: {0}")]
54 DataIntegrity(#[from] DataIntegrityError),
55
56 #[error("Credential is not signed")]
57 NotSigned,
58
59 #[error("Unknown W3C VC Version")]
60 UnknownVCVersion,
61
62 #[error("AuthorityCredential carries an empty actions list, which confers nothing")]
67 EmptyAuthorityActions,
68
69 #[error("not an AuthorityCredential, so there is no authority to attenuate")]
71 NotAnAuthorityCredential,
72
73 #[deprecated(
79 since = "0.7.0",
80 note = "Never returned. `authority.parent` is a digest as of Working Draft 02, so a \
81 parent VAC no longer needs an `id` to be attenuated. This variant will be \
82 removed in a future release."
83 )]
84 #[error("cannot attenuate a credential with no id — the derived VAC could not name it")]
85 AttenuationParentHasNoId,
86
87 #[error("not a well-formed digestMultibase value: {0}")]
93 InvalidDigest(String),
94
95 #[error("digest uses multihash algorithm 0x{0:x}, which this library does not accept")]
101 UnsupportedDigestAlgorithm(u64),
102
103 #[error("malformed DelegationCredential: {0}")]
105 MalformedDelegation(String),
106
107 #[error("Not a delegation grant: {0}")]
110 NotADelegationGrant(String),
111
112 #[error("attenuation would widen the parent grant: {0}")]
114 AttenuationWidens(String),
115
116 #[error("WitnessCredential is missing the required taskContext property")]
118 MissingTaskContext,
119
120 #[error("Could not canonicalize credential: {0}")]
122 Canonicalization(String),
123
124 #[error("Expected a {expected}, got a {got}")]
126 WrongCredentialType { expected: String, got: String },
127
128 #[error("Not a community-issued membership grant: {0}")]
131 NotAMembershipGrant(String),
132}
133
134#[derive(Serialize, Deserialize, Debug, Clone)]
136#[serde(try_from = "DTGCommon")]
137pub struct DTGCredential {
138 #[serde(flatten)]
140 credential: DTGCommon,
141
142 #[serde(skip)]
144 type_: DTGCredentialType,
145
146 #[serde(skip)]
148 version: W3CVCVersion,
149}
150
151impl DTGCredential {
152 pub fn credential(&self) -> &DTGCommon {
154 &self.credential
155 }
156
157 pub fn credential_mut(&mut self) -> &mut DTGCommon {
159 &mut self.credential
160 }
161
162 pub fn signed(&self) -> bool {
164 self.credential.signed()
165 }
166
167 pub fn type_(&self) -> DTGCredentialType {
169 self.type_.clone()
170 }
171
172 pub fn id(&self) -> Option<&str> {
178 self.credential.id()
179 }
180
181 pub fn issuer(&self) -> &str {
183 self.credential.issuer()
184 }
185
186 pub fn subject(&self) -> &str {
188 self.credential.subject()
189 }
190
191 pub fn valid_from(&self) -> DateTime<Utc> {
193 self.credential.valid_from()
194 }
195
196 pub fn valid_until(&self) -> Option<DateTime<Utc>> {
198 self.credential.valid_until()
199 }
200
201 pub fn task_context(&self) -> Option<&str> {
206 self.credential.task_context()
207 }
208
209 pub fn digest_multibase(&self) -> Result<String, DTGCredentialError> {
237 let unsigned = DTGCommon {
238 proof: None,
239 ..self.credential.clone()
240 };
241 let value = serde_json::to_value(&unsigned)
242 .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
243 digest_multibase_json(&value)
244 }
245
246 #[deprecated(
248 since = "0.7.0",
249 note = "Working Draft 02 replaced the `sha256:<hex>` digest with a base58btc \
250 multibase multihash under the property name `digestMultibase`. Use \
251 DTGCredential::digest_multibase. This method will be removed in a future \
252 release."
253 )]
254 pub fn digest(&self) -> Result<String, DTGCredentialError> {
255 let unsigned = DTGCommon {
256 proof: None,
257 ..self.credential.clone()
258 };
259 let value = serde_json::to_value(&unsigned)
260 .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
261 #[allow(deprecated)]
262 digest_json(&value)
263 }
264
265 pub fn subject_digest(&self) -> Option<&str> {
273 match &self.credential.credential_subject {
274 CredentialSubject::Membership(subject) => subject.digest_multibase.as_deref(),
275 CredentialSubject::Witness(subject) => subject.digest_multibase.as_deref(),
276 CredentialSubject::Authority(subject) => subject.authority.parent.as_deref(),
277 CredentialSubject::Delegation(subject) => subject
278 .delegation
279 .accepts
280 .as_deref()
281 .or(subject.delegation.parent.as_deref()),
282 _ => None,
283 }
284 }
285
286 pub fn verify_digest(&self, referenced: &DTGCredential) -> Result<bool, DTGCredentialError> {
312 let Some(carried) = self.subject_digest() else {
313 return Ok(false);
314 };
315
316 digests_match(carried, &referenced.digest_multibase()?)
317 }
318
319 pub fn acknowledges(&self, grant: &DTGCredential) -> Result<bool, DTGCredentialError> {
343 if !matches!(self.type_, DTGCredentialType::Membership)
344 || !matches!(grant.type_, DTGCredentialType::Membership)
345 {
346 return Ok(false);
347 }
348
349 if grant.subject_digest().is_some() {
352 return Ok(false);
353 }
354
355 if self.issuer() != grant.subject() || self.subject() != grant.issuer() {
356 return Ok(false);
357 }
358
359 self.verify_digest(grant)
360 }
361
362 pub fn accepts(&self, grant: &DTGCredential) -> Result<bool, DTGCredentialError> {
385 if !matches!(self.type_, DTGCredentialType::Delegation)
386 || !matches!(grant.type_, DTGCredentialType::Delegation)
387 {
388 return Ok(false);
389 }
390
391 let (Some(acceptance), Some(appointment)) =
392 (self.credential.delegation(), grant.credential.delegation())
393 else {
394 return Ok(false);
395 };
396
397 if appointment.accepts.is_some() || appointment.scope.is_none() {
400 return Ok(false);
401 }
402 let Some(carried) = &acceptance.accepts else {
403 return Ok(false);
404 };
405
406 if self.issuer() != grant.subject() || self.subject() != grant.issuer() {
407 return Ok(false);
408 }
409
410 digests_match(carried, &grant.digest_multibase()?)
411 }
412
413 pub fn proof_value(&self) -> Option<&str> {
415 if let Some(proof) = &self.credential.proof {
416 proof.proof_value.as_deref()
417 } else {
418 None
419 }
420 }
421
422 #[cfg(feature = "affinidi-signing")]
423 pub async fn sign(
427 &mut self,
428 signing_secret: &Secret,
429 create_time: Option<DateTime<Utc>>,
430 ) -> Result<DataIntegrityProof, DTGCredentialError> {
431 let mut options = SignOptions::new();
432 if let Some(ts) = create_time {
433 options = options.with_created(ts);
434 }
435
436 let proof = DataIntegrityProof::sign(self, signing_secret, options).await?;
437
438 self.credential.proof = Some(proof.clone());
439 Ok(proof)
440 }
441
442 #[cfg(feature = "affinidi-signing")]
443 pub fn verify_proof_with_public_key(
447 &self,
448 public_key_bytes: &[u8],
449 ) -> Result<(), DTGCredentialError> {
450 let proof = if let Some(proof) = &self.credential.proof {
451 proof.clone()
452 } else {
453 use tracing::warn;
454
455 warn!("Trying to verify a DTG Credential that has no proof");
456 return Err(DTGCredentialError::NotSigned);
457 };
458
459 let unsigned = DTGCommon {
460 proof: None,
461 ..self.credential.clone()
462 };
463
464 proof.verify_with_public_key(&unsigned, public_key_bytes, VerifyOptions::new())?;
465 Ok(())
466 }
467
468 pub fn get_w3c_vc_version(&self) -> W3CVCVersion {
470 self.version
471 }
472
473 pub fn is_personhood_credential(&self) -> bool {
475 if let DTGCredentialType::Membership = self.type_ {
476 self.credential
477 .type_
478 .contains(&"PersonhoodCredential".to_string())
479 } else {
480 false
481 }
482 }
483}
484
485const MULTIHASH_SHA2_256: u64 = 0x12;
489
490fn proofless(doc: &Value) -> Value {
492 match doc {
493 Value::Object(members) => {
494 let mut members = members.clone();
495 members.remove("proof");
496 Value::Object(members)
497 }
498 other => other.clone(),
501 }
502}
503
504pub fn digest_multibase_json(doc: &Value) -> Result<String, DTGCredentialError> {
535 let canonical = serde_json_canonicalizer::to_vec(&proofless(doc))
536 .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
537
538 let digest = Sha256::digest(&canonical);
539
540 let mut multihash = Vec::with_capacity(2 + digest.len());
543 multihash.push(MULTIHASH_SHA2_256 as u8);
544 multihash.push(digest.len() as u8);
545 multihash.extend_from_slice(&digest);
546
547 Ok(multibase::encode(Base::Base58Btc, &multihash))
548}
549
550pub fn decode_digest_multibase(digest: &str) -> Result<(u64, Vec<u8>), DTGCredentialError> {
564 let (_, bytes) = multibase::decode(digest)
565 .map_err(|e| DTGCredentialError::InvalidDigest(format!("multibase: {e}")))?;
566
567 let (&code, rest) = bytes
571 .split_first()
572 .ok_or_else(|| DTGCredentialError::InvalidDigest("empty multihash".into()))?;
573 if code & 0x80 != 0 {
574 return Err(DTGCredentialError::InvalidDigest(
575 "multi-byte multihash code, which names no algorithm this library accepts".into(),
576 ));
577 }
578 let (&length, raw) = rest
579 .split_first()
580 .ok_or_else(|| DTGCredentialError::InvalidDigest("multihash has no length".into()))?;
581
582 if code as u64 != MULTIHASH_SHA2_256 {
583 return Err(DTGCredentialError::UnsupportedDigestAlgorithm(code as u64));
584 }
585 if length as usize != raw.len() {
586 return Err(DTGCredentialError::InvalidDigest(format!(
587 "multihash declares {length} bytes but carries {}",
588 raw.len()
589 )));
590 }
591
592 Ok((code as u64, raw.to_vec()))
593}
594
595pub fn digests_match(left: &str, right: &str) -> Result<bool, DTGCredentialError> {
600 Ok(decode_digest_multibase(left)? == decode_digest_multibase(right)?)
601}
602
603#[deprecated(
605 since = "0.7.0",
606 note = "Working Draft 02 replaced the `sha256:<hex>` digest with a base58btc multibase \
607 multihash under the property name `digestMultibase`. Use \
608 digest_multibase_json. This function will be removed in a future release."
609)]
610pub fn digest_json(doc: &Value) -> Result<String, DTGCredentialError> {
611 let canonical = serde_json_canonicalizer::to_vec(&proofless(doc))
612 .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
613
614 const HEX: &[u8; 16] = b"0123456789abcdef";
615 let mut out = String::with_capacity("sha256:".len() + 64);
616 out.push_str("sha256:");
617 for byte in Sha256::digest(&canonical) {
618 out.push(HEX[(byte >> 4) as usize] as char);
619 out.push(HEX[(byte & 0x0f) as usize] as char);
620 }
621 Ok(out)
622}
623
624#[derive(Debug, Clone)]
626#[non_exhaustive]
627pub enum DTGCredentialType {
628 Membership,
629 Relationship,
630 Invitation,
631 Persona,
632 Endorsement,
633 Witness,
634
635 Authority,
643
644 Delegation,
650
651 #[deprecated(
653 since = "0.2.0",
654 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
655 It was removed from the DTG Core Credentials specification in Working Draft 01 \
656 and will be defined by the planned DTG Verifiable Data Structures specification. \
657 This variant will be removed in a future release."
658 )]
659 RCard,
660}
661
662impl Display for DTGCredentialType {
663 #[allow(deprecated)]
664 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
665 match self {
666 DTGCredentialType::Membership => write!(f, "MembershipCredential"),
667 DTGCredentialType::Relationship => write!(f, "RelationshipCredential"),
668 DTGCredentialType::Invitation => write!(f, "InvitationCredential"),
669 DTGCredentialType::Persona => write!(f, "PersonaCredential"),
670 DTGCredentialType::Endorsement => write!(f, "EndorsementCredential"),
671 DTGCredentialType::Witness => write!(f, "WitnessCredential"),
672 DTGCredentialType::Authority => write!(f, "AuthorityCredential"),
673 DTGCredentialType::Delegation => write!(f, "DelegationCredential"),
674 DTGCredentialType::RCard => write!(f, "RCardCredential"),
675 }
676 }
677}
678
679const DTG_TYPES: [&str; 9] = [
681 "MembershipCredential",
682 "RelationshipCredential",
683 "InvitationCredential",
684 "PersonaCredential",
685 "EndorsementCredential",
686 "WitnessCredential",
687 "AuthorityCredential",
688 "DelegationCredential",
689 "RCardCredential",
690];
691
692impl TryFrom<&[String]> for DTGCredentialType {
693 type Error = DTGCredentialError;
694
695 #[allow(deprecated)]
696 fn try_from(types: &[String]) -> Result<Self, Self::Error> {
697 if let Some(type_) = DTG_TYPES.iter().find(|t| types.contains(&t.to_string())) {
698 match *type_ {
699 "MembershipCredential" => Ok(DTGCredentialType::Membership),
700 "RelationshipCredential" => Ok(DTGCredentialType::Relationship),
701 "InvitationCredential" => Ok(DTGCredentialType::Invitation),
702 "PersonaCredential" => Ok(DTGCredentialType::Persona),
703 "EndorsementCredential" => Ok(DTGCredentialType::Endorsement),
704 "WitnessCredential" => Ok(DTGCredentialType::Witness),
705 "AuthorityCredential" => Ok(DTGCredentialType::Authority),
706 "DelegationCredential" => Ok(DTGCredentialType::Delegation),
707 "RCardCredential" => Ok(DTGCredentialType::RCard),
708 _ => Err(DTGCredentialError::UnknownCredential),
709 }
710 } else {
711 Err(DTGCredentialError::UnknownCredential)
712 }
713 }
714}
715
716#[derive(Serialize, Deserialize, Debug, Clone)]
718#[serde(rename_all = "camelCase")]
719pub struct DTGCommon {
720 #[serde(rename = "@context")]
725 pub context: Vec<String>,
726
727 #[serde(rename = "type")]
732 pub type_: Vec<String>,
733
734 #[serde(skip_serializing_if = "Option::is_none", default)]
751 pub id: Option<String>,
752
753 pub issuer: String,
755
756 #[serde(serialize_with = "iso8601_format", alias = "issuanceDate")]
758 pub valid_from: DateTime<Utc>,
759
760 #[serde(serialize_with = "iso8601_format_option")]
762 #[serde(
763 skip_serializing_if = "Option::is_none",
764 alias = "expirationDate",
765 default
766 )]
767 pub valid_until: Option<DateTime<Utc>>,
768
769 #[serde(skip_serializing_if = "Option::is_none", default)]
779 pub task_context: Option<String>,
780
781 pub credential_subject: CredentialSubject,
783
784 #[serde(skip_serializing_if = "Option::is_none", default)]
804 pub credential_status: Option<Value>,
805
806 #[serde(skip_serializing_if = "Option::is_none", default)]
808 pub proof: Option<DataIntegrityProof>,
809
810 #[serde(flatten)]
823 pub extra: serde_json::Map<String, Value>,
824}
825
826impl DTGCommon {
827 pub fn signed(&self) -> bool {
831 self.proof.is_some()
832 }
833
834 pub fn id(&self) -> Option<&str> {
836 self.id.as_deref()
837 }
838
839 pub fn issuer(&self) -> &str {
841 &self.issuer
842 }
843
844 #[allow(deprecated)]
846 pub fn subject(&self) -> &str {
847 match &self.credential_subject {
848 CredentialSubject::Basic(subject) => &subject.id,
849 CredentialSubject::Endorsement(subject) => &subject.id,
850 CredentialSubject::Witness(subject) => &subject.id,
851 CredentialSubject::Membership(subject) => &subject.id,
852 CredentialSubject::Authority(subject) => &subject.id,
853 CredentialSubject::Delegation(subject) => &subject.id,
854 CredentialSubject::RCard(subject) => &subject.id,
855 }
856 }
857
858 pub fn authority(&self) -> Option<&AuthorityGrant> {
864 match &self.credential_subject {
865 CredentialSubject::Authority(subject) => Some(&subject.authority),
866 _ => None,
867 }
868 }
869
870 pub fn authority_mut(&mut self) -> Option<&mut AuthorityGrant> {
876 match &mut self.credential_subject {
877 CredentialSubject::Authority(subject) => Some(&mut subject.authority),
878 _ => None,
879 }
880 }
881
882 pub fn delegation(&self) -> Option<&DelegationGrant> {
888 match &self.credential_subject {
889 CredentialSubject::Delegation(subject) => Some(&subject.delegation),
890 _ => None,
891 }
892 }
893
894 pub fn delegation_mut(&mut self) -> Option<&mut DelegationGrant> {
900 match &mut self.credential_subject {
901 CredentialSubject::Delegation(subject) => Some(&mut subject.delegation),
902 _ => None,
903 }
904 }
905
906 pub fn valid_from(&self) -> DateTime<Utc> {
908 self.valid_from
909 }
910
911 pub fn valid_until(&self) -> Option<DateTime<Utc>> {
913 self.valid_until
914 }
915
916 pub fn task_context(&self) -> Option<&str> {
918 self.task_context.as_deref()
919 }
920}
921
922impl Default for DTGCommon {
924 fn default() -> Self {
925 DTGCommon {
926 context: vec![
927 "https://www.w3.org/ns/credentials/v2".to_string(),
928 "https://firstperson.network/credentials/dtg/v1".to_string(),
929 ],
930 type_: vec![
931 "VerifiableCredential".to_string(),
932 "DTGCredential".to_string(),
933 ],
934 id: None,
935 issuer: String::new(),
936 valid_from: Utc::now(),
937 valid_until: None,
938 task_context: None,
939 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic {
940 id: String::new(),
941 }),
942 credential_status: None,
943 proof: None,
944 extra: serde_json::Map::new(),
945 }
946 }
947}
948
949impl TryFrom<DTGCommon> for DTGCredential {
951 type Error = DTGCredentialError;
952
953 #[allow(deprecated)]
954 fn try_from(value: DTGCommon) -> Result<Self, Self::Error> {
955 match &value.type_.as_slice().try_into()? {
956 DTGCredentialType::Membership => {
957 let subject = match &value.credential_subject {
962 CredentialSubject::Membership(subject) => subject.clone(),
965
966 CredentialSubject::Basic(subject) => CredentialSubjectMembership {
968 id: subject.id.clone(),
969 digest_multibase: None,
970 },
971
972 CredentialSubject::Witness(subject) if subject.witness_context.is_none() => {
978 CredentialSubjectMembership {
979 id: subject.id.clone(),
980 digest_multibase: subject.digest_multibase.clone(),
981 }
982 }
983
984 _ => return Err(DTGCredentialError::UnknownCredential),
985 };
986
987 Ok(DTGCredential {
988 type_: DTGCredentialType::Membership,
989 version: value.context.as_slice().try_into()?,
990 credential: DTGCommon {
991 credential_subject: CredentialSubject::Membership(subject),
992 ..value
993 },
994 })
995 }
996 DTGCredentialType::Relationship => Ok(DTGCredential {
997 type_: DTGCredentialType::Relationship,
998 version: value.context.as_slice().try_into()?,
999 credential: value,
1000 }),
1001 DTGCredentialType::Invitation => Ok(DTGCredential {
1002 type_: DTGCredentialType::Invitation,
1003 version: value.context.as_slice().try_into()?,
1004 credential: value,
1005 }),
1006 DTGCredentialType::Persona => Ok(DTGCredential {
1007 type_: DTGCredentialType::Persona,
1008 version: value.context.as_slice().try_into()?,
1009 credential: value,
1010 }),
1011 DTGCredentialType::Endorsement => {
1012 if let CredentialSubject::Endorsement { .. } = &value.credential_subject {
1013 Ok(DTGCredential {
1014 type_: DTGCredentialType::Endorsement,
1015 version: value.context.as_slice().try_into()?,
1016 credential: value,
1017 })
1018 } else {
1019 Err(DTGCredentialError::UnknownCredential)
1020 }
1021 }
1022 DTGCredentialType::Witness => {
1023 if value.task_context.is_none() {
1027 return Err(DTGCredentialError::MissingTaskContext);
1028 }
1029
1030 match &value.credential_subject {
1031 CredentialSubject::Witness(_) => Ok(DTGCredential {
1032 type_: DTGCredentialType::Witness,
1033 version: value.context.as_slice().try_into()?,
1034 credential: value,
1035 }),
1036 CredentialSubject::Basic(subject) => {
1037 Ok(DTGCredential {
1039 type_: DTGCredentialType::Witness,
1040 version: value.context.as_slice().try_into()?,
1041 credential: DTGCommon {
1042 credential_subject: CredentialSubject::Witness(
1043 CredentialSubjectWitness {
1044 id: subject.id.clone(),
1045 digest_multibase: None,
1046 witness_context: None,
1047 },
1048 ),
1049 ..value
1050 },
1051 })
1052 }
1053 _ => Err(DTGCredentialError::UnknownCredential),
1054 }
1055 }
1056 DTGCredentialType::Authority => {
1057 match &value.credential_subject {
1063 CredentialSubject::Authority(subject) => {
1064 if subject.authority.actions.is_empty() {
1065 return Err(DTGCredentialError::EmptyAuthorityActions);
1068 }
1069 Ok(DTGCredential {
1070 type_: DTGCredentialType::Authority,
1071 version: value.context.as_slice().try_into()?,
1072 credential: value,
1073 })
1074 }
1075 _ => Err(DTGCredentialError::UnknownCredential),
1076 }
1077 }
1078 DTGCredentialType::Delegation => {
1079 match &value.credential_subject {
1084 CredentialSubject::Delegation(subject) => {
1085 let d = &subject.delegation;
1086
1087 match (&d.accepts, &d.scope) {
1091 (Some(_), Some(_)) => {
1092 return Err(DTGCredentialError::MalformedDelegation(
1093 "carries both `accepts` and `scope`: an acceptance \
1094 consents to the scope of the grant it names rather \
1095 than restating it"
1096 .into(),
1097 ));
1098 }
1099 (Some(_), None) => {
1100 if d.parent.is_some() || d.max_depth.is_some() {
1101 return Err(DTGCredentialError::MalformedDelegation(
1102 "an acceptance carries `accepts` and nothing else".into(),
1103 ));
1104 }
1105 }
1106 (None, Some(scope)) => {
1107 if scope.is_empty() {
1108 return Err(DTGCredentialError::MalformedDelegation(
1109 "a grant's `scope` MUST contain at least one \
1110 entry — emptying it is not how an unbounded \
1111 appointment is expressed, because there is no \
1112 way to express one"
1113 .into(),
1114 ));
1115 }
1116 }
1117 (None, None) => {
1118 return Err(DTGCredentialError::MalformedDelegation(
1119 "carries neither `scope` nor `accepts`, so it is \
1120 neither a grant nor an acceptance"
1121 .into(),
1122 ));
1123 }
1124 }
1125
1126 Ok(DTGCredential {
1127 type_: DTGCredentialType::Delegation,
1128 version: value.context.as_slice().try_into()?,
1129 credential: value,
1130 })
1131 }
1132 _ => Err(DTGCredentialError::UnknownCredential),
1133 }
1134 }
1135 DTGCredentialType::RCard => match &value.credential_subject {
1136 CredentialSubject::RCard { .. } => Ok(DTGCredential {
1137 type_: DTGCredentialType::RCard,
1138 version: value.context.as_slice().try_into()?,
1139 credential: value,
1140 }),
1141 _ => Err(DTGCredentialError::UnknownCredential),
1142 },
1143 }
1144 }
1145}
1146
1147fn iso8601_format<S>(timestamp: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
1150where
1151 S: Serializer,
1152{
1153 s.serialize_str(
1154 timestamp
1155 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
1156 .as_str(),
1157 )
1158}
1159
1160fn iso8601_format_option<S>(timestamp: &Option<DateTime<Utc>>, s: S) -> Result<S::Ok, S::Error>
1161where
1162 S: Serializer,
1163{
1164 if let Some(timestamp) = timestamp {
1165 s.serialize_str(
1166 timestamp
1167 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
1168 .as_str(),
1169 )
1170 } else {
1171 s.serialize_none()
1172 }
1173}
1174
1175#[allow(deprecated)]
1184#[derive(Serialize, Deserialize, Debug, Clone)]
1185#[serde(untagged)]
1186pub enum CredentialSubject {
1187 Endorsement(CredentialSubjectEndorsement),
1189
1190 #[deprecated(
1192 since = "0.2.0",
1193 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1194 See DTGCredentialType::RCard. This variant will be removed in a future release."
1195 )]
1196 RCard(CredentialSubjectRCard),
1197
1198 Basic(CredentialSubjectBasic),
1201
1202 Witness(CredentialSubjectWitness),
1204
1205 Authority(CredentialSubjectAuthority),
1211
1212 Delegation(CredentialSubjectDelegation),
1217
1218 Membership(CredentialSubjectMembership),
1236}
1237
1238#[derive(Serialize, Deserialize, Debug, Clone)]
1240#[serde(deny_unknown_fields)]
1241pub struct CredentialSubjectBasic {
1242 pub id: String,
1243}
1244
1245#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1257#[serde(rename_all = "camelCase", deny_unknown_fields)]
1258pub struct AuthorityGrant {
1259 pub scope: String,
1264
1265 pub actions: Vec<String>,
1272
1273 #[serde(skip_serializing_if = "Option::is_none")]
1290 pub parent: Option<String>,
1291
1292 #[serde(skip_serializing_if = "Option::is_none")]
1305 pub audience: Option<String>,
1306}
1307
1308#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
1325#[serde(rename_all = "camelCase", deny_unknown_fields)]
1326pub struct DelegationGrant {
1327 #[serde(skip_serializing_if = "Option::is_none", default)]
1338 pub scope: Option<Vec<String>>,
1339
1340 #[serde(skip_serializing_if = "Option::is_none", default)]
1343 pub parent: Option<String>,
1344
1345 #[serde(skip_serializing_if = "Option::is_none", default)]
1353 pub max_depth: Option<u32>,
1354
1355 #[serde(skip_serializing_if = "Option::is_none", default)]
1360 pub accepts: Option<String>,
1361}
1362
1363#[derive(Serialize, Deserialize, Debug, Clone)]
1365#[serde(rename_all = "camelCase", deny_unknown_fields)]
1366pub struct CredentialSubjectDelegation {
1367 pub id: String,
1369
1370 pub delegation: DelegationGrant,
1372}
1373
1374#[derive(Serialize, Deserialize, Debug, Clone)]
1376#[serde(rename_all = "camelCase", deny_unknown_fields)]
1377pub struct CredentialSubjectAuthority {
1378 pub id: String,
1380
1381 pub authority: AuthorityGrant,
1383}
1384
1385#[derive(Serialize, Deserialize, Debug, Clone)]
1393#[serde(rename_all = "camelCase", deny_unknown_fields)]
1394pub struct CredentialSubjectMembership {
1395 pub id: String,
1396
1397 #[serde(
1409 rename = "digestMultibase",
1410 alias = "digest",
1411 skip_serializing_if = "Option::is_none",
1412 default
1413 )]
1414 pub digest_multibase: Option<String>,
1415}
1416
1417#[derive(Serialize, Deserialize, Debug, Clone)]
1419#[serde(deny_unknown_fields)]
1420pub struct CredentialSubjectEndorsement {
1421 pub id: String,
1422 pub endorsement: Value,
1424}
1425
1426#[derive(Serialize, Deserialize, Debug, Clone)]
1428#[serde(rename_all = "camelCase", deny_unknown_fields)]
1429pub struct CredentialSubjectWitness {
1430 pub id: String,
1431
1432 #[serde(
1439 rename = "digestMultibase",
1440 alias = "digest",
1441 skip_serializing_if = "Option::is_none",
1442 default
1443 )]
1444 pub digest_multibase: Option<String>,
1445
1446 #[serde(skip_serializing_if = "Option::is_none")]
1448 pub witness_context: Option<WitnessContext>,
1449}
1450
1451#[derive(Serialize, Deserialize, Debug, Clone)]
1453#[serde(rename_all = "camelCase", deny_unknown_fields)]
1454pub struct WitnessContext {
1455 pub event: Option<String>,
1457
1458 pub session_id: Option<String>,
1460
1461 pub method: Option<String>,
1463}
1464
1465#[deprecated(
1467 since = "0.2.0",
1468 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1469 See DTGCredentialType::RCard. This struct will be removed in a future release."
1470)]
1471#[derive(Serialize, Deserialize, Debug, Clone)]
1472#[serde(deny_unknown_fields)]
1473pub struct CredentialSubjectRCard {
1474 pub id: String,
1475
1476 pub card: Value,
1478}
1479
1480#[cfg(test)]
1481#[allow(deprecated)]
1482mod tests {
1483 use crate::{
1484 CredentialSubject, CredentialSubjectRCard, DTGCommon, DTGCredential, DTGCredentialError,
1485 DTGCredentialType, W3CVCVersion, decode_digest_multibase, digest_multibase_json,
1486 digests_match,
1487 };
1488 use chrono::{DateTime, Utc};
1489 use multibase::Base;
1490 use serde_json::Value;
1491 use sha2::{Digest, Sha256};
1492
1493 #[test]
1494 fn test_vmc_vc_1_deserialize() {
1495 let vmc: DTGCredential = match serde_json::from_str(
1497 r#"{
1498"@context": [
1499 "https://www.w3.org/2018/credentials/v1",
1500 "https://firstperson.network/credentials/dtg/v1",
1501 "https://w3id.org/security/suites/ed25519-2020/v1"
1502 ],
1503 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1504 "issuer": "did:web:chess-club.example",
1505 "issuanceDate": "2026-01-06T10:00:00Z",
1506 "expirationDate": "2027-01-06T10:00:00Z",
1507 "credentialSubject": {
1508 "id": "did:key:z6MkpTHR8VNs..."
1509 }
1510 }"#,
1511 ) {
1512 Ok(vmc) => vmc,
1513 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
1514 };
1515
1516 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
1517 assert!(matches!(
1518 vmc.credential().credential_subject,
1519 CredentialSubject::Membership(_)
1520 ));
1521 assert!(matches!(vmc.version, W3CVCVersion::V1_1));
1522 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V1_1));
1523 }
1524
1525 #[test]
1526 fn test_missing_w3c_context() {
1527 assert!(
1529 serde_json::from_str::<DTGCredential>(
1530 r#"{
1531"@context": [
1532 "https://firstperson.network/credentials/dtg/v1",
1533 "https://w3id.org/security/suites/ed25519-2020/v1"
1534 ],
1535 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1536 "issuer": "did:web:chess-club.example",
1537 "issuanceDate": "2026-01-06T10:00:00Z",
1538 "expirationDate": "2027-01-06T10:00:00Z",
1539 "credentialSubject": {
1540 "id": "did:key:z6MkpTHR8VNs..."
1541 }
1542 }"#,
1543 )
1544 .is_err()
1545 );
1546 }
1547
1548 #[test]
1549 fn test_mutable_credential() {
1550 let mut vmc = DTGCredential::new_vmc(
1551 "did:example:issuer".to_string(),
1552 "did:example:subject".to_string(),
1553 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1554 .unwrap()
1555 .with_timezone(&Utc),
1556 None,
1557 false,
1558 );
1559
1560 let cred = vmc.credential_mut();
1561 cred.type_.push("PersonhoodCredential".to_string());
1562 assert!(vmc.is_personhood_credential());
1563 }
1564
1565 #[test]
1566 fn test_vmc_deserialize() {
1567 let vmc: DTGCredential = match serde_json::from_str(
1568 r#"{
1569 "@context": ["https://www.w3.org/ns/credentials/v2"],
1570 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1571 "issuer": "did:example:community",
1572 "validFrom": "2024-06-18T10:00:00Z",
1573 "credentialSubject": { "id": "did:example:rDid" }
1574 }"#,
1575 ) {
1576 Ok(vmc) => vmc,
1577 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
1578 };
1579
1580 assert!(!vmc.is_personhood_credential());
1581 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
1582 assert!(matches!(
1583 vmc.credential().credential_subject,
1584 CredentialSubject::Membership(_)
1585 ));
1586 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V2_0));
1587 }
1588
1589 #[test]
1590 fn test_vmc_phc_deserialize() {
1591 let vmc: DTGCredential = match serde_json::from_str(
1592 r#"{
1593 "@context": ["https://www.w3.org/ns/credentials/v2"],
1594 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential", "PersonhoodCredential"],
1595 "issuer": "did:example:community",
1596 "validFrom": "2024-06-18T10:00:00Z",
1597 "credentialSubject": { "id": "did:example:rDid" }
1598 }"#,
1599 ) {
1600 Ok(vmc) => vmc,
1601 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
1602 };
1603
1604 assert!(vmc.is_personhood_credential());
1605 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
1606 assert!(matches!(
1607 vmc.credential().credential_subject,
1608 CredentialSubject::Membership(_)
1609 ));
1610 }
1611
1612 #[test]
1613 fn test_vrc_deserialize() {
1614 let vrc: DTGCredential = match serde_json::from_str(
1615 r#"{
1616 "@context": ["https://www.w3.org/ns/credentials/v2"],
1617 "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"],
1618 "issuer": "did:example:governmentAgencyDid",
1619 "validFrom": "2024-06-18T10:00:00Z",
1620 "credentialSubject": { "id": "did:example:citizenRDid" }
1621 }"#,
1622 ) {
1623 Ok(vrc) => vrc,
1624 Err(e) => panic!("Couldn't deserialize VRC: {}", e),
1625 };
1626
1627 assert!(matches!(vrc.type_, DTGCredentialType::Relationship));
1628 assert!(matches!(
1629 vrc.credential().credential_subject,
1630 CredentialSubject::Basic(_)
1631 ));
1632 }
1633
1634 #[test]
1635 fn test_vic_deserialize() {
1636 let vic: DTGCredential = match serde_json::from_str(
1637 r#"{
1638 "@context": ["https://www.w3.org/ns/credentials/v2"],
1639 "type": ["VerifiableCredential", "DTGCredential", "InvitationCredential"],
1640 "issuer": "did:example:governmentAgencyVicDid",
1641 "validFrom": "2024-06-18T10:00:00Z",
1642 "credentialSubject": { "id": "did:example:citizenRDid" }
1643 }"#,
1644 ) {
1645 Ok(vic) => vic,
1646 Err(e) => panic!("Couldn't deserialize VIC: {}", e),
1647 };
1648
1649 assert!(!vic.is_personhood_credential());
1650 assert!(matches!(vic.type_, DTGCredentialType::Invitation));
1651 assert!(matches!(
1652 vic.credential().credential_subject,
1653 CredentialSubject::Basic(_)
1654 ));
1655 }
1656
1657 #[test]
1658 fn test_vpc_deserialize() {
1659 let vpc: DTGCredential = match serde_json::from_str(
1660 r#"{
1661 "@context": ["https://www.w3.org/ns/credentials/v2"],
1662 "type": ["VerifiableCredential", "DTGCredential", "PersonaCredential"],
1663 "issuer": "did:example:governmentAgencyDid",
1664 "validFrom": "2024-06-18T10:00:00Z",
1665 "credentialSubject": { "id": "did:example:citizenRDid" }
1666 }"#,
1667 ) {
1668 Ok(vpc) => vpc,
1669 Err(e) => panic!("Couldn't deserialize VPC: {}", e),
1670 };
1671
1672 assert!(matches!(vpc.type_, DTGCredentialType::Persona));
1673 assert!(matches!(
1674 vpc.credential().credential_subject,
1675 CredentialSubject::Basic(_)
1676 ));
1677 }
1678
1679 #[test]
1680 fn test_vec_deserialize() {
1681 let vec: DTGCredential = match serde_json::from_str(
1682 r#"{
1683 "@context": ["https://www.w3.org/ns/credentials/v2"],
1684 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
1685 "issuer": "did:example:governmentAgencyDid",
1686 "validFrom": "2024-06-18T10:00:00Z",
1687 "credentialSubject": { "id": "did:example:citizenRDid", "endorsement": {} }
1688 }"#,
1689 ) {
1690 Ok(vec) => vec,
1691 Err(e) => panic!("Couldn't deserialize VEC: {}", e),
1692 };
1693
1694 assert!(matches!(vec.type_, DTGCredentialType::Endorsement));
1695 assert!(matches!(vec.subject(), "did:example:citizenRDid"));
1696 assert!(matches!(
1697 vec.credential().credential_subject,
1698 CredentialSubject::Endorsement(_)
1699 ));
1700 }
1701
1702 #[test]
1703 fn test_vec_bad_deserialize() {
1704 match serde_json::from_str::<DTGCredential>(
1705 r#"{
1706 "@context": ["https://www.w3.org/ns/credentials/v2"],
1707 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
1708 "issuer": "did:example:governmentAgencyDid",
1709 "validFrom": "2024-06-18T10:00:00Z",
1710 "credentialSubject": { "id": "did:example:citizenRDid", "other": [] }
1711 }"#,
1712 ) {
1713 Ok(_) => panic!("Expected Unknown Credential type"),
1714 Err(_) => {
1715 }
1717 };
1718 }
1719
1720 #[test]
1721 fn test_vwc_simple_deserialize() {
1722 let vwc: DTGCredential = match serde_json::from_str(
1723 r#"{
1724 "@context": ["https://www.w3.org/ns/credentials/v2"],
1725 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1726 "issuer": "did:example:governmentAgencyDid",
1727 "validFrom": "2024-06-18T10:00:00Z",
1728 "taskContext": "thread-abc-123",
1729 "credentialSubject": { "id": "did:example:citizenRDid" }
1730 }"#,
1731 ) {
1732 Ok(vwc) => vwc,
1733 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
1734 };
1735
1736 assert!(matches!(vwc.type_, DTGCredentialType::Witness));
1737 assert!(matches!(vwc.subject(), "did:example:citizenRDid"));
1738 assert_eq!(vwc.task_context(), Some("thread-abc-123"));
1739 assert!(matches!(
1740 vwc.credential().credential_subject,
1741 CredentialSubject::Witness(_)
1742 ));
1743 }
1744
1745 #[test]
1746 fn test_vwc_full_deserialize() {
1747 let vwc: DTGCredential = match serde_json::from_str(
1748 r#"{
1749 "@context": ["https://www.w3.org/ns/credentials/v2"],
1750 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1751 "issuer": "did:example:governmentAgencyDid",
1752 "validFrom": "2024-06-18T10:00:00Z",
1753 "taskContext": "thread-abc-123",
1754 "credentialSubject": { "id": "did:example:citizenRDid", "digestMultibase": "abcdf", "witnessContext": {} }
1755 }"#,
1756 ) {
1757 Ok(vwc) => vwc,
1758 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
1759 };
1760
1761 assert!(matches!(vwc.type_(), DTGCredentialType::Witness));
1762 assert!(matches!(
1763 vwc.credential().credential_subject,
1764 CredentialSubject::Witness(_)
1765 ));
1766 }
1767
1768 #[test]
1769 fn test_vwc_bad_deserialize() {
1770 if serde_json::from_str::<DTGCredential>(
1771 r#"{
1772 "@context": ["https://www.w3.org/ns/credentials/v2"],
1773 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1774 "issuer": "did:example:governmentAgencyDid",
1775 "validFrom": "2024-06-18T10:00:00Z",
1776 "taskContext": "thread-abc-123",
1777 "credentialSubject": { "id": "did:example:citizenRDid", "digestMultibase": "abcdf", "wrongContext": {} }
1778 }"#,
1779 ).is_ok() {
1780 panic!("Should have failed due to wrong CredentialSubject!");
1781 }
1782 }
1783
1784 #[test]
1785 fn test_rcard_simple_deserialize() {
1786 let rcard: DTGCredential = match serde_json::from_str(
1787 r#"{
1788 "@context": ["https://www.w3.org/ns/credentials/v2"],
1789 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
1790 "issuer": "did:example:governmentAgencyDid",
1791 "validFrom": "2024-06-18T10:00:00Z",
1792 "credentialSubject": { "id": "did:example:citizenRDid", "card": [] }
1793 }"#,
1794 ) {
1795 Ok(rcard) => rcard,
1796 Err(e) => panic!("Couldn't deserialize R-Card: {}", e),
1797 };
1798
1799 assert!(matches!(rcard.type_(), DTGCredentialType::RCard));
1800 assert!(matches!(rcard.subject(), "did:example:citizenRDid"));
1801 assert!(matches!(
1802 rcard.credential().credential_subject,
1803 CredentialSubject::RCard(_)
1804 ));
1805 }
1806
1807 #[test]
1808 fn test_rcard_bad_deserialize() {
1809 if serde_json::from_str::<DTGCredential>(
1810 r#"{
1811 "@context": ["https://www.w3.org/ns/credentials/v2"],
1812 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
1813 "issuer": "did:example:governmentAgencyDid",
1814 "validFrom": "2024-06-18T10:00:00Z",
1815 "credentialSubject": { "id": "did:example:citizenRDid" }
1816 }"#,
1817 )
1818 .is_ok()
1819 {
1820 panic!("Should have failed due to wrong CredentialSubject!");
1821 }
1822 }
1823 #[test]
1824 fn test_deserialize_unknown() {
1825 match serde_json::from_str::<DTGCredential>(
1826 r#"{
1827 "@context": ["https://www.w3.org/ns/credentials/v2"],
1828 "type": ["VerifiableCredential", "DTGCredential", "UnknownCredential"],
1829 "issuer": "did:example:governmentAgencyDid",
1830 "validFrom": "2024-06-18T10:00:00Z",
1831 "credentialSubject": { "id": "did:example:citizenRDid" }
1832 }"#,
1833 ) {
1834 Ok(_) => panic!("Expected Unknown Credential type"),
1835 Err(e) => {
1836 if e.to_string() == "Unknown credential type" {
1837 } else {
1839 panic!("Wrong error type returned");
1840 }
1841 }
1842 };
1843 }
1844
1845 #[test]
1846 fn test_deserialize_mismatched_credential_subject() {
1847 match serde_json::from_str::<DTGCredential>(
1848 r#"{
1849 "@context": ["https://www.w3.org/ns/credentials/v2"],
1850 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
1851 "issuer": "did:example:governmentAgencyDid",
1852 "validFrom": "2024-06-18T10:00:00Z",
1853 "credentialSubject": { "id": "did:example:citizenRDid" }
1854 }"#,
1855 ) {
1856 Ok(_) => panic!("Expected Unknown Credential type"),
1857 Err(e) => {
1858 if e.to_string() == "Unknown credential type" {
1859 } else {
1861 panic!("Wrong error type returned");
1862 }
1863 }
1864 };
1865 }
1866
1867 #[test]
1868 fn test_proof_signed() {
1869 let cred: DTGCredential = match serde_json::from_str(
1870 r#"{
1871 "@context": ["https://www.w3.org/ns/credentials/v2"],
1872 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1873 "issuer": "did:example:community",
1874 "validFrom": "2024-06-18T10:00:00Z",
1875 "credentialSubject": { "id": "did:example:rDid" },
1876 "proof": {
1877 "type": "DataIntegrityProof",
1878 "cryptosuite": "eddsa-jcs-2022",
1879 "created": "2025-12-04T00:00:00",
1880 "verificationMethod": "did:example:test#key-1",
1881 "proofPurpose": "assertionMethod",
1882 "proofValue": "abcd"
1883 }
1884 }"#,
1885 ) {
1886 Ok(vmc) => vmc,
1887 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1888 };
1889
1890 assert!(cred.signed());
1891 assert!(cred.proof_value().is_some());
1892 }
1893
1894 #[test]
1895 fn test_proof_not_signed() {
1896 let cred: DTGCredential = match serde_json::from_str(
1897 r#"{
1898 "@context": ["https://www.w3.org/ns/credentials/v2"],
1899 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1900 "issuer": "did:example:community",
1901 "validFrom": "2024-06-18T10:00:00Z",
1902 "credentialSubject": { "id": "did:example:rDid" }
1903 }"#,
1904 ) {
1905 Ok(vmc) => vmc,
1906 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1907 };
1908
1909 assert!(!cred.signed());
1910 assert!(cred.proof_value().is_none());
1911 }
1912
1913 #[test]
1914 fn test_helpers() {
1915 let cred: DTGCredential = match serde_json::from_str(
1916 r#"{
1917 "@context": ["https://www.w3.org/ns/credentials/v2"],
1918 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1919 "issuer": "did:example:issuer",
1920 "validFrom": "2024-06-18T00:00:00Z",
1921 "credentialSubject": { "id": "did:example:subject" }
1922 }"#,
1923 ) {
1924 Ok(vmc) => vmc,
1925 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1926 };
1927
1928 assert_eq!(cred.issuer(), "did:example:issuer");
1929 assert_eq!(cred.subject(), "did:example:subject");
1930 assert_eq!(
1931 cred.valid_from()
1932 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1933 "2024-06-18T00:00:00Z"
1934 );
1935 assert_eq!(cred.valid_until(), None);
1936 }
1937
1938 #[test]
1939 fn test_valid_until() {
1940 let cred: DTGCredential = match serde_json::from_str(
1941 r#"{
1942 "@context": ["https://www.w3.org/ns/credentials/v2"],
1943 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1944 "issuer": "did:example:issuer",
1945 "validFrom": "2024-06-18T00:00:00Z",
1946 "validUntil": "2030-01-01T00:00:00Z",
1947 "credentialSubject": { "id": "did:example:subject" }
1948 }"#,
1949 ) {
1950 Ok(vmc) => vmc,
1951 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1952 };
1953
1954 assert_eq!(
1955 cred.valid_until()
1956 .unwrap()
1957 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1958 "2030-01-01T00:00:00Z"
1959 );
1960 }
1961
1962 #[test]
1963 fn test_bad_type() {
1964 assert!(
1965 std::convert::TryInto::<DTGCredentialType>::try_into(
1966 vec!["bad_type".to_string()].as_slice(),
1967 )
1968 .is_err()
1969 );
1970 }
1971
1972 #[test]
1973 fn test_badly_constructed_vwc() {
1974 let mut cred = DTGCommon::default();
1975 cred.type_.push("WitnessCredential".to_string());
1976 cred.task_context = Some("thread-abc-123".to_string());
1979 cred.credential_subject = CredentialSubject::RCard(CredentialSubjectRCard {
1980 id: "did:example:bad".to_string(),
1981 card: Value::Null,
1982 });
1983
1984 assert!(std::convert::TryInto::<DTGCredential>::try_into(cred).is_err());
1985 }
1986
1987 #[test]
1988 fn test_vwc_missing_task_context() {
1989 match serde_json::from_str::<DTGCredential>(
1991 r#"{
1992 "@context": ["https://www.w3.org/ns/credentials/v2"],
1993 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1994 "issuer": "did:example:witness",
1995 "validFrom": "2024-06-18T10:00:00Z",
1996 "credentialSubject": { "id": "did:example:observed" }
1997 }"#,
1998 ) {
1999 Ok(_) => panic!("Expected a VWC without taskContext to be rejected"),
2000 Err(e) => assert_eq!(
2001 e.to_string(),
2002 "WitnessCredential is missing the required taskContext property"
2003 ),
2004 }
2005 }
2006
2007 #[test]
2008 fn test_task_context_round_trip() {
2009 let raw = r#"{
2012 "@context": ["https://www.w3.org/ns/credentials/v2"],
2013 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
2014 "issuer": "did:example:witness",
2015 "validFrom": "2024-06-18T10:00:00Z",
2016 "taskContext": "thread-abc-123",
2017 "credentialSubject": { "id": "did:example:observed" }
2018 }"#;
2019
2020 let cred: DTGCredential = serde_json::from_str(raw).unwrap();
2021 let out = serde_json::to_string(&cred).unwrap();
2022
2023 assert!(out.contains(r#""taskContext":"thread-abc-123""#));
2024 }
2025
2026 #[test]
2027 fn test_task_context_optional_on_other_types() {
2028 let vrc: DTGCredential = serde_json::from_str(
2030 r#"{
2031 "@context": ["https://www.w3.org/ns/credentials/v2"],
2032 "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"],
2033 "issuer": "did:example:issuer",
2034 "validFrom": "2024-06-18T10:00:00Z",
2035 "credentialSubject": { "id": "did:example:subject" }
2036 }"#,
2037 )
2038 .unwrap();
2039
2040 assert_eq!(vrc.task_context(), None);
2041 assert!(!serde_json::to_string(&vrc).unwrap().contains("taskContext"));
2043 }
2044
2045 #[test]
2046 fn test_digest_multibase() {
2047 let vrc = DTGCredential::new_vrc(
2048 "did:example:issuer".to_string(),
2049 "did:example:subject".to_string(),
2050 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2051 .unwrap()
2052 .with_timezone(&Utc),
2053 None,
2054 );
2055
2056 let digest = vrc.digest_multibase().unwrap();
2057
2058 assert!(digest.starts_with('z'));
2060
2061 let (base, bytes) = multibase::decode(&digest).unwrap();
2063 assert_eq!(base, multibase::Base::Base58Btc);
2064 assert_eq!(bytes.len(), 34);
2065 assert_eq!(&bytes[..2], &[0x12, 0x20]);
2066
2067 assert_eq!(digest, vrc.digest_multibase().unwrap());
2069
2070 let other = DTGCredential::new_vrc(
2072 "did:example:issuer".to_string(),
2073 "did:example:someone-else".to_string(),
2074 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2075 .unwrap()
2076 .with_timezone(&Utc),
2077 None,
2078 );
2079 assert_ne!(digest, other.digest_multibase().unwrap());
2080 }
2081
2082 #[test]
2083 fn test_verify_digest() {
2084 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2085 .unwrap()
2086 .with_timezone(&Utc);
2087
2088 let vrc = DTGCredential::new_vrc(
2089 "did:example:issuer".to_string(),
2090 "did:example:subject".to_string(),
2091 valid_from,
2092 None,
2093 );
2094
2095 let vwc = DTGCredential::new_vwc(
2096 "did:example:witness".to_string(),
2097 "did:example:issuer".to_string(),
2099 valid_from,
2100 None,
2101 "thread-abc-123".to_string(),
2102 Some(vrc.digest_multibase().unwrap()),
2103 None,
2104 );
2105
2106 assert!(vwc.verify_digest(&vrc).unwrap());
2107
2108 let other = DTGCredential::new_vrc(
2110 "did:example:issuer".to_string(),
2111 "did:example:someone-else".to_string(),
2112 valid_from,
2113 None,
2114 );
2115 assert!(!vwc.verify_digest(&other).unwrap());
2116 }
2117
2118 #[test]
2119 fn test_verify_digest_without_digest() {
2120 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2121 .unwrap()
2122 .with_timezone(&Utc);
2123
2124 let vrc = DTGCredential::new_vrc(
2125 "did:example:issuer".to_string(),
2126 "did:example:subject".to_string(),
2127 valid_from,
2128 None,
2129 );
2130
2131 let vwc = DTGCredential::new_vwc(
2133 "did:example:witness".to_string(),
2134 "did:example:issuer".to_string(),
2135 valid_from,
2136 None,
2137 "thread-abc-123".to_string(),
2138 None,
2139 None,
2140 );
2141
2142 assert!(!vwc.verify_digest(&vrc).unwrap());
2143 }
2144
2145 #[test]
2150 fn test_digest_is_a_base58btc_multihash_over_the_proofless_jcs_form() {
2151 let vmc = DTGCredential::new_vmc(
2152 "did:example:community".to_string(),
2153 "did:example:member".to_string(),
2154 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2155 .unwrap()
2156 .with_timezone(&Utc),
2157 None,
2158 false,
2159 )
2160 .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
2161
2162 let digest = vmc.digest_multibase().unwrap();
2163
2164 assert!(digest.starts_with('z'), "multibase base58btc prefix");
2166
2167 let (base, bytes) = multibase::decode(&digest).unwrap();
2169 assert_eq!(base, Base::Base58Btc);
2170 assert_eq!(bytes.len(), 34);
2171 assert_eq!(&bytes[..2], &[0x12, 0x20]);
2172
2173 assert_eq!(digest, "zQmTJgyPT2ShMQ2AvCHGDoPGjEWyRC7ZNT3MBpe5PP6Vpvu");
2180
2181 assert_eq!(digest, vmc.digest_multibase().unwrap());
2183 }
2184
2185 #[test]
2188 #[allow(deprecated)]
2189 fn the_superseded_hex_digest_is_unchanged() {
2190 let vmc = DTGCredential::new_vmc(
2191 "did:example:community".to_string(),
2192 "did:example:member".to_string(),
2193 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2194 .unwrap()
2195 .with_timezone(&Utc),
2196 None,
2197 false,
2198 )
2199 .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
2200
2201 assert_eq!(
2202 vmc.digest().unwrap(),
2203 "sha256:49c9d5135ab4b5659a343bc79d351e37d64f05add58408cae6eef022828495c2"
2204 );
2205 }
2206
2207 #[test]
2211 fn a_superseded_digest_value_is_rejected_as_malformed() {
2212 let err = decode_digest_multibase(
2213 "sha256:49c9d5135ab4b5659a343bc79d351e37d64f05add58408cae6eef022828495c2",
2214 )
2215 .unwrap_err();
2216
2217 assert!(
2218 matches!(err, DTGCredentialError::InvalidDigest(_)),
2219 "expected InvalidDigest, got {err:?}"
2220 );
2221 }
2222
2223 #[test]
2226 fn digests_are_compared_by_bytes_not_by_string() {
2227 let multihash = {
2230 let mut v = vec![0x12u8, 0x20];
2231 v.extend_from_slice(&Sha256::digest(b"an edge credential"));
2232 v
2233 };
2234 let b58 = multibase::encode(Base::Base58Btc, &multihash);
2235 let b16 = multibase::encode(Base::Base16Lower, &multihash);
2236
2237 assert_ne!(b58, b16, "the two spellings differ as strings");
2238 assert!(
2239 digests_match(&b58, &b16).unwrap(),
2240 "but name the same digest"
2241 );
2242 }
2243
2244 #[test]
2248 fn an_unaccepted_hash_algorithm_is_rejected_rather_than_mismatched() {
2249 let mut multihash = vec![0x13u8, 0x40];
2251 multihash.extend_from_slice(&[0u8; 64]);
2252 let encoded = multibase::encode(Base::Base58Btc, &multihash);
2253
2254 assert!(matches!(
2255 decode_digest_multibase(&encoded),
2256 Err(DTGCredentialError::UnsupportedDigestAlgorithm(0x13))
2257 ));
2258 }
2259
2260 #[cfg(feature = "affinidi-signing")]
2264 #[tokio::test]
2265 async fn test_digest_is_unchanged_by_signing() {
2266 use affinidi_secrets_resolver::secrets::Secret;
2267
2268 let secret = Secret::generate_ed25519(None, None);
2269
2270 let mut vmc = DTGCredential::new_vmc(
2271 "did:example:community".to_string(),
2272 "did:example:member".to_string(),
2273 Utc::now(),
2274 None,
2275 false,
2276 );
2277
2278 let before = vmc.digest_multibase().unwrap();
2279 vmc.sign(&secret, None).await.expect("signs");
2280 assert!(vmc.signed());
2281 assert_eq!(before, vmc.digest_multibase().unwrap());
2282 }
2283
2284 fn wire(c: &DTGCredential) -> Value {
2286 serde_json::to_value(c.credential()).expect("credential serialises")
2287 }
2288
2289 #[test]
2292 fn test_member_vmc_acknowledges_its_grant() {
2293 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2294 .unwrap()
2295 .with_timezone(&Utc);
2296
2297 let grant = DTGCredential::new_vmc(
2298 "did:example:community".to_string(),
2299 "did:example:member".to_string(),
2300 valid_from,
2301 None,
2302 false,
2303 );
2304
2305 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2306
2307 assert_eq!(ack.issuer(), "did:example:member");
2309 assert_eq!(ack.subject(), "did:example:community");
2310
2311 assert_eq!(grant.subject_digest(), None);
2313 assert_eq!(
2314 ack.subject_digest(),
2315 Some(grant.digest_multibase().unwrap().as_str())
2316 );
2317
2318 assert!(ack.acknowledges(&grant).unwrap());
2319 }
2320
2321 #[test]
2324 fn test_acknowledges_rejects_a_mismatched_pair() {
2325 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2326 .unwrap()
2327 .with_timezone(&Utc);
2328
2329 let grant = DTGCredential::new_vmc(
2330 "did:example:community".to_string(),
2331 "did:example:member".to_string(),
2332 valid_from,
2333 None,
2334 false,
2335 );
2336 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2337
2338 let other_member = DTGCredential::new_vmc(
2340 "did:example:community".to_string(),
2341 "did:example:someone-else".to_string(),
2342 valid_from,
2343 None,
2344 false,
2345 );
2346 assert!(!ack.acknowledges(&other_member).unwrap());
2347
2348 let other_community = DTGCredential::new_vmc(
2350 "did:example:other-community".to_string(),
2351 "did:example:member".to_string(),
2352 valid_from,
2353 None,
2354 false,
2355 );
2356 assert!(!ack.acknowledges(&other_community).unwrap());
2357
2358 let renewed = DTGCredential::new_vmc(
2362 "did:example:community".to_string(),
2363 "did:example:member".to_string(),
2364 valid_from + chrono::Duration::days(365),
2365 None,
2366 false,
2367 );
2368 assert!(!ack.acknowledges(&renewed).unwrap());
2369
2370 let ack_of_ack =
2372 DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2373 assert!(!ack_of_ack.acknowledges(&ack).unwrap());
2374
2375 assert!(!grant.acknowledges(&grant).unwrap());
2377 }
2378
2379 #[test]
2383 fn credential_status_survives_a_round_trip() {
2384 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2385 .unwrap()
2386 .with_timezone(&Utc);
2387
2388 let mut grant = wire(&DTGCredential::new_vmc(
2389 "did:example:community".to_string(),
2390 "did:example:member".to_string(),
2391 valid_from,
2392 None,
2393 false,
2394 ));
2395 let status = serde_json::json!({
2396 "id": "https://community.example/status#7",
2397 "type": "BitstringStatusListEntry",
2398 "statusPurpose": "revocation",
2399 "statusListIndex": "7"
2400 });
2401 grant["credentialStatus"] = status.clone();
2402
2403 let parsed: DTGCredential = serde_json::from_value(grant.clone()).expect("parses");
2404 assert_eq!(
2405 parsed.credential().credential_status.as_ref(),
2406 Some(&status)
2407 );
2408 assert_eq!(wire(&parsed).get("credentialStatus"), Some(&status));
2409 assert_eq!(
2410 parsed.digest_multibase().unwrap(),
2411 digest_multibase_json(&grant).unwrap(),
2412 "the digest must not change under a round trip that preserves every member"
2413 );
2414 }
2415
2416 #[test]
2419 fn unmodelled_top_level_members_survive_a_round_trip() {
2420 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2421 .unwrap()
2422 .with_timezone(&Utc);
2423
2424 let mut grant = wire(&DTGCredential::new_vmc(
2425 "did:example:community".to_string(),
2426 "did:example:member".to_string(),
2427 valid_from,
2428 None,
2429 false,
2430 ));
2431 let schema = serde_json::json!({
2432 "id": "https://community.example/schemas/vmc",
2433 "type": "JsonSchema"
2434 });
2435 grant["credentialSchema"] = schema.clone();
2436
2437 let parsed: DTGCredential = serde_json::from_value(grant.clone()).expect("parses");
2438 assert_eq!(
2439 parsed.credential().extra.get("credentialSchema"),
2440 Some(&schema)
2441 );
2442 assert_eq!(
2443 parsed.digest_multibase().unwrap(),
2444 digest_multibase_json(&grant).unwrap()
2445 );
2446 }
2447
2448 #[test]
2465 fn the_acknowledgement_digests_the_grant_as_it_arrived() {
2466 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2467 .unwrap()
2468 .with_timezone(&Utc);
2469
2470 let mut grant = wire(&DTGCredential::new_vmc(
2471 "did:example:community".to_string(),
2472 "did:example:member".to_string(),
2473 valid_from,
2474 None,
2475 false,
2476 ));
2477 grant["validFrom"] = Value::String("2025-12-11T00:00:00.000+00:00".to_string());
2479
2480 let parsed: DTGCredential = serde_json::from_value(grant.clone()).expect("parses");
2482 assert_ne!(
2483 wire(&parsed).get("validFrom"),
2484 grant.get("validFrom"),
2485 "the model is expected to normalize the timestamp; if it now round-trips \
2486 verbatim, this test has stopped guarding anything"
2487 );
2488
2489 let ack = DTGCredential::new_member_vmc(&grant, valid_from, None).expect("builds");
2490
2491 assert_eq!(
2492 ack.subject_digest(),
2493 Some(digest_multibase_json(&grant).unwrap().as_str()),
2494 "the acknowledgement must digest the grant as received"
2495 );
2496 assert_ne!(
2497 ack.subject_digest(),
2498 Some(parsed.digest_multibase().unwrap().as_str()),
2499 "digesting the parsed model would produce a digest the community cannot match"
2500 );
2501 }
2502
2503 #[test]
2504 fn digest_multibase_json_agrees_with_digest_where_the_model_is_complete() {
2505 let vmc = DTGCredential::new_vmc(
2506 "did:example:community".to_string(),
2507 "did:example:member".to_string(),
2508 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2509 .unwrap()
2510 .with_timezone(&Utc),
2511 None,
2512 false,
2513 )
2514 .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
2515
2516 assert_eq!(
2517 vmc.digest_multibase().unwrap(),
2518 digest_multibase_json(&wire(&vmc)).unwrap()
2519 );
2520 }
2521
2522 #[test]
2525 fn test_acknowledges_is_membership_only() {
2526 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2527 .unwrap()
2528 .with_timezone(&Utc);
2529
2530 let grant = DTGCredential::new_vmc(
2531 "did:example:community".to_string(),
2532 "did:example:member".to_string(),
2533 valid_from,
2534 None,
2535 false,
2536 );
2537 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2538
2539 let vrc = DTGCredential::new_vrc(
2540 "did:example:member".to_string(),
2541 "did:example:community".to_string(),
2542 valid_from,
2543 None,
2544 );
2545 assert!(!ack.acknowledges(&vrc).unwrap());
2546
2547 let vwc = DTGCredential::new_vwc(
2549 "did:example:witness".to_string(),
2550 "did:example:community".to_string(),
2551 valid_from,
2552 None,
2553 "thread-abc-123".to_string(),
2554 Some(grant.digest_multibase().unwrap()),
2555 None,
2556 );
2557 assert!(vwc.verify_digest(&grant).unwrap(), "the digest does match");
2558 assert!(
2559 !vwc.acknowledges(&grant).unwrap(),
2560 "but a VWC is not the member's acknowledgement"
2561 );
2562 }
2563
2564 #[test]
2568 fn test_new_member_vmc_refuses_a_non_grant() {
2569 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2570 .unwrap()
2571 .with_timezone(&Utc);
2572
2573 let vrc = DTGCredential::new_vrc(
2574 "did:example:a".to_string(),
2575 "did:example:b".to_string(),
2576 valid_from,
2577 None,
2578 );
2579 assert!(matches!(
2580 DTGCredential::new_member_vmc(&wire(&vrc), valid_from, None),
2581 Err(DTGCredentialError::NotAMembershipGrant(_))
2582 ));
2583
2584 let grant = DTGCredential::new_vmc(
2585 "did:example:community".to_string(),
2586 "did:example:member".to_string(),
2587 valid_from,
2588 None,
2589 false,
2590 );
2591 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2592 assert!(matches!(
2593 DTGCredential::new_member_vmc(&wire(&ack), valid_from, None),
2594 Err(DTGCredentialError::NotAMembershipGrant(_))
2595 ));
2596 }
2597
2598 #[test]
2603 fn test_member_issued_vmc_deserializes_as_membership_not_witness() {
2604 let vmc: DTGCredential = serde_json::from_str(
2605 r#"{
2606 "@context": ["https://www.w3.org/ns/credentials/v2"],
2607 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
2608 "issuer": "did:example:member",
2609 "validFrom": "2024-06-18T10:00:00Z",
2610 "credentialSubject": {
2611 "id": "did:example:community",
2612 "digestMultibase": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
2613 }
2614 }"#,
2615 )
2616 .expect("deserializes");
2617
2618 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
2619 assert!(matches!(
2620 vmc.credential().credential_subject,
2621 CredentialSubject::Membership(_)
2622 ));
2623 assert_eq!(
2624 vmc.subject_digest(),
2625 Some("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
2626 );
2627 assert_eq!(vmc.subject(), "did:example:community");
2628 }
2629
2630 #[test]
2633 fn test_membership_credential_rejects_a_witness_context() {
2634 let result: Result<DTGCredential, _> = serde_json::from_str(
2635 r#"{
2636 "@context": ["https://www.w3.org/ns/credentials/v2"],
2637 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
2638 "issuer": "did:example:member",
2639 "validFrom": "2024-06-18T10:00:00Z",
2640 "credentialSubject": {
2641 "id": "did:example:community",
2642 "digestMultibase": "sha256:e3b0c4",
2643 "witnessContext": { "event": "not a membership property" }
2644 }
2645 }"#,
2646 );
2647 assert!(result.is_err());
2648 }
2649
2650 #[test]
2653 fn test_the_two_halves_round_trip_over_the_wire() {
2654 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2655 .unwrap()
2656 .with_timezone(&Utc);
2657
2658 let grant = DTGCredential::new_vmc(
2659 "did:example:community".to_string(),
2660 "did:example:member".to_string(),
2661 valid_from,
2662 None,
2663 false,
2664 );
2665 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2666
2667 let grant_json = serde_json::to_value(&grant).unwrap();
2668 assert!(
2669 grant_json["credentialSubject"]
2670 .get("digestMultibase")
2671 .is_none(),
2672 "the grant MUST omit `digestMultibase`: {grant_json}"
2673 );
2674
2675 let ack_json = serde_json::to_value(&ack).unwrap();
2676 assert_eq!(
2677 ack_json["credentialSubject"]["digestMultibase"],
2678 Value::String(grant.digest_multibase().unwrap()),
2679 );
2680
2681 let grant: DTGCredential = serde_json::from_value(grant_json).expect("grant round trips");
2684 let ack: DTGCredential = serde_json::from_value(ack_json).expect("ack round trips");
2685 assert!(ack.acknowledges(&grant).unwrap());
2686 }
2687
2688 #[test]
2689 fn test_iso8601_format_option() {
2690 let now: DateTime<Utc> = DateTime::parse_from_rfc3339(
2691 &Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
2692 )
2693 .unwrap()
2694 .to_utc();
2695 let cred = DTGCommon {
2696 valid_until: Some(now),
2697 ..Default::default()
2698 };
2699
2700 let value = serde_json::to_value(&cred).unwrap();
2701 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
2702 assert_eq!(cred2.valid_until, Some(now));
2703
2704 let cred = DTGCommon::default();
2705 let value = serde_json::to_value(&cred).unwrap();
2706 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
2707 assert_eq!(cred2.valid_until, None);
2708 }
2709
2710 #[cfg(feature = "affinidi-signing")]
2711 #[tokio::test]
2712 async fn test_signing() {
2713 use affinidi_secrets_resolver::secrets::Secret;
2714
2715 let secret = Secret::generate_ed25519(None, None);
2716
2717 let mut cred = DTGCredential::new_vrc(
2718 "did:example:issuer".to_string(),
2719 "did:example:subject".to_string(),
2720 Utc::now(),
2721 None,
2722 );
2723
2724 assert!(cred.sign(&secret, Some(Utc::now())).await.is_ok());
2725
2726 assert!(
2727 cred.verify_proof_with_public_key(secret.get_public_bytes())
2728 .is_ok()
2729 );
2730
2731 let secret2 = Secret::generate_ed25519(None, None);
2732 assert!(
2733 cred.verify_proof_with_public_key(secret2.get_public_bytes())
2734 .is_err()
2735 );
2736 }
2737
2738 #[cfg(feature = "affinidi-signing")]
2746 #[tokio::test]
2747 async fn test_id_is_covered_by_the_proof() {
2748 use affinidi_secrets_resolver::secrets::Secret;
2749
2750 let secret = Secret::generate_ed25519(None, None);
2751
2752 let mut cred = DTGCredential::new_vrc(
2753 "did:example:issuer".to_string(),
2754 "did:example:subject".to_string(),
2755 Utc::now(),
2756 None,
2757 )
2758 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
2759
2760 cred.sign(&secret, Some(Utc::now()))
2761 .await
2762 .expect("signing a credential that carries an id");
2763 assert!(
2764 cred.verify_proof_with_public_key(secret.get_public_bytes())
2765 .is_ok(),
2766 "an id set before signing verifies"
2767 );
2768
2769 cred.set_id("urn:uuid:00000000-0000-0000-0000-000000000000");
2772 assert!(
2773 cred.verify_proof_with_public_key(secret.get_public_bytes())
2774 .is_err(),
2775 "an id changed after signing must break the proof"
2776 );
2777 }
2778
2779 #[cfg(feature = "affinidi-signing")]
2780 #[tokio::test]
2781 async fn test_signing_error() {
2782 use affinidi_secrets_resolver::secrets::Secret;
2783
2784 let secret = Secret::generate_x25519(None, None).unwrap();
2785
2786 let mut cred = DTGCredential::new_vrc(
2787 "did:example:issuer".to_string(),
2788 "did:example:subject".to_string(),
2789 Utc::now(),
2790 None,
2791 );
2792
2793 assert!(cred.sign(&secret, Some(Utc::now())).await.is_err());
2794 }
2795
2796 #[cfg(feature = "affinidi-signing")]
2797 #[test]
2798 fn test_signing_no_proof() {
2799 use crate::DTGCredentialError;
2800 use affinidi_secrets_resolver::secrets::Secret;
2801
2802 let cred = DTGCredential::new_vrc(
2803 "did:example:issuer".to_string(),
2804 "did:example:subject".to_string(),
2805 Utc::now(),
2806 None,
2807 );
2808
2809 let secret = Secret::generate_ed25519(None, None);
2810 match cred.verify_proof_with_public_key(secret.get_public_bytes()) {
2811 Err(DTGCredentialError::NotSigned) => {
2812 }
2814 _ => panic!("Expected NotSigned error!"),
2815 }
2816 }
2817}