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,
645
646 Delegation,
652
653 #[deprecated(
655 since = "0.2.0",
656 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
657 It was removed from the DTG Core Credentials specification in Working Draft 01 \
658 and will be defined by the planned DTG Verifiable Data Structures specification. \
659 This variant will be removed in a future release."
660 )]
661 RCard,
662}
663
664impl Display for DTGCredentialType {
665 #[allow(deprecated)]
666 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667 match self {
668 DTGCredentialType::Membership => write!(f, "MembershipCredential"),
669 DTGCredentialType::Relationship => write!(f, "RelationshipCredential"),
670 DTGCredentialType::Invitation => write!(f, "InvitationCredential"),
671 DTGCredentialType::Persona => write!(f, "PersonaCredential"),
672 DTGCredentialType::Endorsement => write!(f, "EndorsementCredential"),
673 DTGCredentialType::Witness => write!(f, "WitnessCredential"),
674 DTGCredentialType::Authority => write!(f, "AuthorityCredential"),
675 DTGCredentialType::Delegation => write!(f, "DelegationCredential"),
676 DTGCredentialType::RCard => write!(f, "RCardCredential"),
677 }
678 }
679}
680
681const DTG_TYPES: [&str; 9] = [
683 "MembershipCredential",
684 "RelationshipCredential",
685 "InvitationCredential",
686 "PersonaCredential",
687 "EndorsementCredential",
688 "WitnessCredential",
689 "AuthorityCredential",
690 "DelegationCredential",
691 "RCardCredential",
692];
693
694impl TryFrom<&[String]> for DTGCredentialType {
695 type Error = DTGCredentialError;
696
697 #[allow(deprecated)]
698 fn try_from(types: &[String]) -> Result<Self, Self::Error> {
699 if let Some(type_) = DTG_TYPES.iter().find(|t| types.contains(&t.to_string())) {
700 match *type_ {
701 "MembershipCredential" => Ok(DTGCredentialType::Membership),
702 "RelationshipCredential" => Ok(DTGCredentialType::Relationship),
703 "InvitationCredential" => Ok(DTGCredentialType::Invitation),
704 "PersonaCredential" => Ok(DTGCredentialType::Persona),
705 "EndorsementCredential" => Ok(DTGCredentialType::Endorsement),
706 "WitnessCredential" => Ok(DTGCredentialType::Witness),
707 "AuthorityCredential" => Ok(DTGCredentialType::Authority),
708 "DelegationCredential" => Ok(DTGCredentialType::Delegation),
709 "RCardCredential" => Ok(DTGCredentialType::RCard),
710 _ => Err(DTGCredentialError::UnknownCredential),
711 }
712 } else {
713 Err(DTGCredentialError::UnknownCredential)
714 }
715 }
716}
717
718#[derive(Serialize, Deserialize, Debug, Clone)]
720#[serde(rename_all = "camelCase")]
721pub struct DTGCommon {
722 #[serde(rename = "@context")]
727 pub context: Vec<String>,
728
729 #[serde(rename = "type")]
734 pub type_: Vec<String>,
735
736 #[serde(skip_serializing_if = "Option::is_none", default)]
753 pub id: Option<String>,
754
755 pub issuer: String,
757
758 #[serde(serialize_with = "iso8601_format", alias = "issuanceDate")]
760 pub valid_from: DateTime<Utc>,
761
762 #[serde(serialize_with = "iso8601_format_option")]
764 #[serde(
765 skip_serializing_if = "Option::is_none",
766 alias = "expirationDate",
767 default
768 )]
769 pub valid_until: Option<DateTime<Utc>>,
770
771 #[serde(skip_serializing_if = "Option::is_none", default)]
781 pub task_context: Option<String>,
782
783 pub credential_subject: CredentialSubject,
785
786 #[serde(skip_serializing_if = "Option::is_none", default)]
806 pub credential_status: Option<Value>,
807
808 #[serde(skip_serializing_if = "Option::is_none", default)]
810 pub proof: Option<DataIntegrityProof>,
811
812 #[serde(flatten)]
825 pub extra: serde_json::Map<String, Value>,
826}
827
828impl DTGCommon {
829 pub fn signed(&self) -> bool {
833 self.proof.is_some()
834 }
835
836 pub fn id(&self) -> Option<&str> {
838 self.id.as_deref()
839 }
840
841 pub fn issuer(&self) -> &str {
843 &self.issuer
844 }
845
846 #[allow(deprecated)]
848 pub fn subject(&self) -> &str {
849 match &self.credential_subject {
850 CredentialSubject::Basic(subject) => &subject.id,
851 CredentialSubject::Endorsement(subject) => &subject.id,
852 CredentialSubject::Witness(subject) => &subject.id,
853 CredentialSubject::Membership(subject) => &subject.id,
854 CredentialSubject::Authority(subject) => &subject.id,
855 CredentialSubject::Delegation(subject) => &subject.id,
856 CredentialSubject::RCard(subject) => &subject.id,
857 }
858 }
859
860 pub fn authority(&self) -> Option<&AuthorityGrant> {
866 match &self.credential_subject {
867 CredentialSubject::Authority(subject) => Some(&subject.authority),
868 _ => None,
869 }
870 }
871
872 pub fn authority_mut(&mut self) -> Option<&mut AuthorityGrant> {
878 match &mut self.credential_subject {
879 CredentialSubject::Authority(subject) => Some(&mut subject.authority),
880 _ => None,
881 }
882 }
883
884 pub fn delegation(&self) -> Option<&DelegationGrant> {
890 match &self.credential_subject {
891 CredentialSubject::Delegation(subject) => Some(&subject.delegation),
892 _ => None,
893 }
894 }
895
896 pub fn delegation_mut(&mut self) -> Option<&mut DelegationGrant> {
902 match &mut self.credential_subject {
903 CredentialSubject::Delegation(subject) => Some(&mut subject.delegation),
904 _ => None,
905 }
906 }
907
908 pub fn valid_from(&self) -> DateTime<Utc> {
910 self.valid_from
911 }
912
913 pub fn valid_until(&self) -> Option<DateTime<Utc>> {
915 self.valid_until
916 }
917
918 pub fn task_context(&self) -> Option<&str> {
920 self.task_context.as_deref()
921 }
922}
923
924impl Default for DTGCommon {
926 fn default() -> Self {
927 DTGCommon {
928 context: vec![
929 "https://www.w3.org/ns/credentials/v2".to_string(),
930 "https://firstperson.network/credentials/dtg/v1".to_string(),
931 ],
932 type_: vec![
933 "VerifiableCredential".to_string(),
934 "DTGCredential".to_string(),
935 ],
936 id: None,
937 issuer: String::new(),
938 valid_from: Utc::now(),
939 valid_until: None,
940 task_context: None,
941 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic {
942 id: String::new(),
943 }),
944 credential_status: None,
945 proof: None,
946 extra: serde_json::Map::new(),
947 }
948 }
949}
950
951impl TryFrom<DTGCommon> for DTGCredential {
953 type Error = DTGCredentialError;
954
955 #[allow(deprecated)]
956 fn try_from(value: DTGCommon) -> Result<Self, Self::Error> {
957 match &value.type_.as_slice().try_into()? {
958 DTGCredentialType::Membership => {
959 let subject = match &value.credential_subject {
964 CredentialSubject::Membership(subject) => subject.clone(),
967
968 CredentialSubject::Basic(subject) => CredentialSubjectMembership {
970 id: subject.id.clone(),
971 digest_multibase: None,
972 },
973
974 CredentialSubject::Witness(subject) if subject.witness_context.is_none() => {
980 CredentialSubjectMembership {
981 id: subject.id.clone(),
982 digest_multibase: subject.digest_multibase.clone(),
983 }
984 }
985
986 _ => return Err(DTGCredentialError::UnknownCredential),
987 };
988
989 Ok(DTGCredential {
990 type_: DTGCredentialType::Membership,
991 version: value.context.as_slice().try_into()?,
992 credential: DTGCommon {
993 credential_subject: CredentialSubject::Membership(subject),
994 ..value
995 },
996 })
997 }
998 DTGCredentialType::Relationship => Ok(DTGCredential {
999 type_: DTGCredentialType::Relationship,
1000 version: value.context.as_slice().try_into()?,
1001 credential: value,
1002 }),
1003 DTGCredentialType::Invitation => Ok(DTGCredential {
1004 type_: DTGCredentialType::Invitation,
1005 version: value.context.as_slice().try_into()?,
1006 credential: value,
1007 }),
1008 DTGCredentialType::Persona => Ok(DTGCredential {
1009 type_: DTGCredentialType::Persona,
1010 version: value.context.as_slice().try_into()?,
1011 credential: value,
1012 }),
1013 DTGCredentialType::Endorsement => {
1014 if let CredentialSubject::Endorsement { .. } = &value.credential_subject {
1015 Ok(DTGCredential {
1016 type_: DTGCredentialType::Endorsement,
1017 version: value.context.as_slice().try_into()?,
1018 credential: value,
1019 })
1020 } else {
1021 Err(DTGCredentialError::UnknownCredential)
1022 }
1023 }
1024 DTGCredentialType::Witness => {
1025 if value.task_context.is_none() {
1029 return Err(DTGCredentialError::MissingTaskContext);
1030 }
1031
1032 match &value.credential_subject {
1033 CredentialSubject::Witness(_) => Ok(DTGCredential {
1034 type_: DTGCredentialType::Witness,
1035 version: value.context.as_slice().try_into()?,
1036 credential: value,
1037 }),
1038 CredentialSubject::Basic(subject) => {
1039 Ok(DTGCredential {
1041 type_: DTGCredentialType::Witness,
1042 version: value.context.as_slice().try_into()?,
1043 credential: DTGCommon {
1044 credential_subject: CredentialSubject::Witness(
1045 CredentialSubjectWitness {
1046 id: subject.id.clone(),
1047 digest_multibase: None,
1048 witness_context: None,
1049 },
1050 ),
1051 ..value
1052 },
1053 })
1054 }
1055 _ => Err(DTGCredentialError::UnknownCredential),
1056 }
1057 }
1058 DTGCredentialType::Authority => {
1059 match &value.credential_subject {
1065 CredentialSubject::Authority(subject) => {
1066 if subject.authority.actions.is_empty() {
1067 return Err(DTGCredentialError::EmptyAuthorityActions);
1070 }
1071 Ok(DTGCredential {
1072 type_: DTGCredentialType::Authority,
1073 version: value.context.as_slice().try_into()?,
1074 credential: value,
1075 })
1076 }
1077 _ => Err(DTGCredentialError::UnknownCredential),
1078 }
1079 }
1080 DTGCredentialType::Delegation => {
1081 match &value.credential_subject {
1086 CredentialSubject::Delegation(subject) => {
1087 let d = &subject.delegation;
1088
1089 match (&d.accepts, &d.scope) {
1093 (Some(_), Some(_)) => {
1094 return Err(DTGCredentialError::MalformedDelegation(
1095 "carries both `accepts` and `scope`: an acceptance \
1096 consents to the scope of the grant it names rather \
1097 than restating it"
1098 .into(),
1099 ));
1100 }
1101 (Some(_), None) => {
1102 if d.parent.is_some() || d.max_depth.is_some() {
1103 return Err(DTGCredentialError::MalformedDelegation(
1104 "an acceptance carries `accepts` and nothing else".into(),
1105 ));
1106 }
1107 }
1108 (None, Some(scope)) => {
1109 if scope.is_empty() {
1110 return Err(DTGCredentialError::MalformedDelegation(
1111 "a grant's `scope` MUST contain at least one \
1112 entry — emptying it is not how an unbounded \
1113 appointment is expressed, because there is no \
1114 way to express one"
1115 .into(),
1116 ));
1117 }
1118 }
1119 (None, None) => {
1120 return Err(DTGCredentialError::MalformedDelegation(
1121 "carries neither `scope` nor `accepts`, so it is \
1122 neither a grant nor an acceptance"
1123 .into(),
1124 ));
1125 }
1126 }
1127
1128 Ok(DTGCredential {
1129 type_: DTGCredentialType::Delegation,
1130 version: value.context.as_slice().try_into()?,
1131 credential: value,
1132 })
1133 }
1134 _ => Err(DTGCredentialError::UnknownCredential),
1135 }
1136 }
1137 DTGCredentialType::RCard => match &value.credential_subject {
1138 CredentialSubject::RCard { .. } => Ok(DTGCredential {
1139 type_: DTGCredentialType::RCard,
1140 version: value.context.as_slice().try_into()?,
1141 credential: value,
1142 }),
1143 _ => Err(DTGCredentialError::UnknownCredential),
1144 },
1145 }
1146 }
1147}
1148
1149fn iso8601_format<S>(timestamp: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
1152where
1153 S: Serializer,
1154{
1155 s.serialize_str(
1156 timestamp
1157 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
1158 .as_str(),
1159 )
1160}
1161
1162fn iso8601_format_option<S>(timestamp: &Option<DateTime<Utc>>, s: S) -> Result<S::Ok, S::Error>
1163where
1164 S: Serializer,
1165{
1166 if let Some(timestamp) = timestamp {
1167 s.serialize_str(
1168 timestamp
1169 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
1170 .as_str(),
1171 )
1172 } else {
1173 s.serialize_none()
1174 }
1175}
1176
1177#[allow(deprecated)]
1186#[derive(Serialize, Deserialize, Debug, Clone)]
1187#[serde(untagged)]
1188pub enum CredentialSubject {
1189 Endorsement(CredentialSubjectEndorsement),
1191
1192 #[deprecated(
1194 since = "0.2.0",
1195 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1196 See DTGCredentialType::RCard. This variant will be removed in a future release."
1197 )]
1198 RCard(CredentialSubjectRCard),
1199
1200 Basic(CredentialSubjectBasic),
1203
1204 Witness(CredentialSubjectWitness),
1206
1207 Authority(CredentialSubjectAuthority),
1213
1214 Delegation(CredentialSubjectDelegation),
1219
1220 Membership(CredentialSubjectMembership),
1238}
1239
1240#[derive(Serialize, Deserialize, Debug, Clone)]
1242#[serde(deny_unknown_fields)]
1243pub struct CredentialSubjectBasic {
1244 pub id: String,
1245}
1246
1247#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1259#[serde(rename_all = "camelCase", deny_unknown_fields)]
1260pub struct AuthorityGrant {
1261 pub scope: String,
1266
1267 pub actions: Vec<String>,
1274
1275 #[serde(skip_serializing_if = "Option::is_none")]
1292 pub parent: Option<String>,
1293}
1294
1295#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
1312#[serde(rename_all = "camelCase", deny_unknown_fields)]
1313pub struct DelegationGrant {
1314 #[serde(skip_serializing_if = "Option::is_none", default)]
1325 pub scope: Option<Vec<String>>,
1326
1327 #[serde(skip_serializing_if = "Option::is_none", default)]
1330 pub parent: Option<String>,
1331
1332 #[serde(skip_serializing_if = "Option::is_none", default)]
1340 pub max_depth: Option<u32>,
1341
1342 #[serde(skip_serializing_if = "Option::is_none", default)]
1347 pub accepts: Option<String>,
1348}
1349
1350#[derive(Serialize, Deserialize, Debug, Clone)]
1352#[serde(rename_all = "camelCase", deny_unknown_fields)]
1353pub struct CredentialSubjectDelegation {
1354 pub id: String,
1356
1357 pub delegation: DelegationGrant,
1359}
1360
1361#[derive(Serialize, Deserialize, Debug, Clone)]
1363#[serde(rename_all = "camelCase", deny_unknown_fields)]
1364pub struct CredentialSubjectAuthority {
1365 pub id: String,
1367
1368 pub authority: AuthorityGrant,
1370}
1371
1372#[derive(Serialize, Deserialize, Debug, Clone)]
1380#[serde(rename_all = "camelCase", deny_unknown_fields)]
1381pub struct CredentialSubjectMembership {
1382 pub id: String,
1383
1384 #[serde(
1396 rename = "digestMultibase",
1397 alias = "digest",
1398 skip_serializing_if = "Option::is_none",
1399 default
1400 )]
1401 pub digest_multibase: Option<String>,
1402}
1403
1404#[derive(Serialize, Deserialize, Debug, Clone)]
1406#[serde(deny_unknown_fields)]
1407pub struct CredentialSubjectEndorsement {
1408 pub id: String,
1409 pub endorsement: Value,
1411}
1412
1413#[derive(Serialize, Deserialize, Debug, Clone)]
1415#[serde(rename_all = "camelCase", deny_unknown_fields)]
1416pub struct CredentialSubjectWitness {
1417 pub id: String,
1418
1419 #[serde(
1426 rename = "digestMultibase",
1427 alias = "digest",
1428 skip_serializing_if = "Option::is_none",
1429 default
1430 )]
1431 pub digest_multibase: Option<String>,
1432
1433 #[serde(skip_serializing_if = "Option::is_none")]
1435 pub witness_context: Option<WitnessContext>,
1436}
1437
1438#[derive(Serialize, Deserialize, Debug, Clone)]
1440#[serde(rename_all = "camelCase", deny_unknown_fields)]
1441pub struct WitnessContext {
1442 pub event: Option<String>,
1444
1445 pub session_id: Option<String>,
1447
1448 pub method: Option<String>,
1450}
1451
1452#[deprecated(
1454 since = "0.2.0",
1455 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1456 See DTGCredentialType::RCard. This struct will be removed in a future release."
1457)]
1458#[derive(Serialize, Deserialize, Debug, Clone)]
1459#[serde(deny_unknown_fields)]
1460pub struct CredentialSubjectRCard {
1461 pub id: String,
1462
1463 pub card: Value,
1465}
1466
1467#[cfg(test)]
1468#[allow(deprecated)]
1469mod tests {
1470 use crate::{
1471 CredentialSubject, CredentialSubjectRCard, DTGCommon, DTGCredential, DTGCredentialError,
1472 DTGCredentialType, W3CVCVersion, decode_digest_multibase, digest_multibase_json,
1473 digests_match,
1474 };
1475 use chrono::{DateTime, Utc};
1476 use multibase::Base;
1477 use serde_json::Value;
1478 use sha2::{Digest, Sha256};
1479
1480 #[test]
1481 fn test_vmc_vc_1_deserialize() {
1482 let vmc: DTGCredential = match serde_json::from_str(
1484 r#"{
1485"@context": [
1486 "https://www.w3.org/2018/credentials/v1",
1487 "https://firstperson.network/credentials/dtg/v1",
1488 "https://w3id.org/security/suites/ed25519-2020/v1"
1489 ],
1490 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1491 "issuer": "did:web:chess-club.example",
1492 "issuanceDate": "2026-01-06T10:00:00Z",
1493 "expirationDate": "2027-01-06T10:00:00Z",
1494 "credentialSubject": {
1495 "id": "did:key:z6MkpTHR8VNs..."
1496 }
1497 }"#,
1498 ) {
1499 Ok(vmc) => vmc,
1500 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
1501 };
1502
1503 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
1504 assert!(matches!(
1505 vmc.credential().credential_subject,
1506 CredentialSubject::Membership(_)
1507 ));
1508 assert!(matches!(vmc.version, W3CVCVersion::V1_1));
1509 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V1_1));
1510 }
1511
1512 #[test]
1513 fn test_missing_w3c_context() {
1514 assert!(
1516 serde_json::from_str::<DTGCredential>(
1517 r#"{
1518"@context": [
1519 "https://firstperson.network/credentials/dtg/v1",
1520 "https://w3id.org/security/suites/ed25519-2020/v1"
1521 ],
1522 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1523 "issuer": "did:web:chess-club.example",
1524 "issuanceDate": "2026-01-06T10:00:00Z",
1525 "expirationDate": "2027-01-06T10:00:00Z",
1526 "credentialSubject": {
1527 "id": "did:key:z6MkpTHR8VNs..."
1528 }
1529 }"#,
1530 )
1531 .is_err()
1532 );
1533 }
1534
1535 #[test]
1536 fn test_mutable_credential() {
1537 let mut vmc = DTGCredential::new_vmc(
1538 "did:example:issuer".to_string(),
1539 "did:example:subject".to_string(),
1540 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1541 .unwrap()
1542 .with_timezone(&Utc),
1543 None,
1544 false,
1545 );
1546
1547 let cred = vmc.credential_mut();
1548 cred.type_.push("PersonhoodCredential".to_string());
1549 assert!(vmc.is_personhood_credential());
1550 }
1551
1552 #[test]
1553 fn test_vmc_deserialize() {
1554 let vmc: DTGCredential = match serde_json::from_str(
1555 r#"{
1556 "@context": ["https://www.w3.org/ns/credentials/v2"],
1557 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1558 "issuer": "did:example:community",
1559 "validFrom": "2024-06-18T10:00:00Z",
1560 "credentialSubject": { "id": "did:example:rDid" }
1561 }"#,
1562 ) {
1563 Ok(vmc) => vmc,
1564 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
1565 };
1566
1567 assert!(!vmc.is_personhood_credential());
1568 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
1569 assert!(matches!(
1570 vmc.credential().credential_subject,
1571 CredentialSubject::Membership(_)
1572 ));
1573 assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V2_0));
1574 }
1575
1576 #[test]
1577 fn test_vmc_phc_deserialize() {
1578 let vmc: DTGCredential = match serde_json::from_str(
1579 r#"{
1580 "@context": ["https://www.w3.org/ns/credentials/v2"],
1581 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential", "PersonhoodCredential"],
1582 "issuer": "did:example:community",
1583 "validFrom": "2024-06-18T10:00:00Z",
1584 "credentialSubject": { "id": "did:example:rDid" }
1585 }"#,
1586 ) {
1587 Ok(vmc) => vmc,
1588 Err(e) => panic!("Couldn't deserialize VMC: {}", e),
1589 };
1590
1591 assert!(vmc.is_personhood_credential());
1592 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
1593 assert!(matches!(
1594 vmc.credential().credential_subject,
1595 CredentialSubject::Membership(_)
1596 ));
1597 }
1598
1599 #[test]
1600 fn test_vrc_deserialize() {
1601 let vrc: DTGCredential = match serde_json::from_str(
1602 r#"{
1603 "@context": ["https://www.w3.org/ns/credentials/v2"],
1604 "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"],
1605 "issuer": "did:example:governmentAgencyDid",
1606 "validFrom": "2024-06-18T10:00:00Z",
1607 "credentialSubject": { "id": "did:example:citizenRDid" }
1608 }"#,
1609 ) {
1610 Ok(vrc) => vrc,
1611 Err(e) => panic!("Couldn't deserialize VRC: {}", e),
1612 };
1613
1614 assert!(matches!(vrc.type_, DTGCredentialType::Relationship));
1615 assert!(matches!(
1616 vrc.credential().credential_subject,
1617 CredentialSubject::Basic(_)
1618 ));
1619 }
1620
1621 #[test]
1622 fn test_vic_deserialize() {
1623 let vic: DTGCredential = match serde_json::from_str(
1624 r#"{
1625 "@context": ["https://www.w3.org/ns/credentials/v2"],
1626 "type": ["VerifiableCredential", "DTGCredential", "InvitationCredential"],
1627 "issuer": "did:example:governmentAgencyVicDid",
1628 "validFrom": "2024-06-18T10:00:00Z",
1629 "credentialSubject": { "id": "did:example:citizenRDid" }
1630 }"#,
1631 ) {
1632 Ok(vic) => vic,
1633 Err(e) => panic!("Couldn't deserialize VIC: {}", e),
1634 };
1635
1636 assert!(!vic.is_personhood_credential());
1637 assert!(matches!(vic.type_, DTGCredentialType::Invitation));
1638 assert!(matches!(
1639 vic.credential().credential_subject,
1640 CredentialSubject::Basic(_)
1641 ));
1642 }
1643
1644 #[test]
1645 fn test_vpc_deserialize() {
1646 let vpc: DTGCredential = match serde_json::from_str(
1647 r#"{
1648 "@context": ["https://www.w3.org/ns/credentials/v2"],
1649 "type": ["VerifiableCredential", "DTGCredential", "PersonaCredential"],
1650 "issuer": "did:example:governmentAgencyDid",
1651 "validFrom": "2024-06-18T10:00:00Z",
1652 "credentialSubject": { "id": "did:example:citizenRDid" }
1653 }"#,
1654 ) {
1655 Ok(vpc) => vpc,
1656 Err(e) => panic!("Couldn't deserialize VPC: {}", e),
1657 };
1658
1659 assert!(matches!(vpc.type_, DTGCredentialType::Persona));
1660 assert!(matches!(
1661 vpc.credential().credential_subject,
1662 CredentialSubject::Basic(_)
1663 ));
1664 }
1665
1666 #[test]
1667 fn test_vec_deserialize() {
1668 let vec: DTGCredential = match serde_json::from_str(
1669 r#"{
1670 "@context": ["https://www.w3.org/ns/credentials/v2"],
1671 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
1672 "issuer": "did:example:governmentAgencyDid",
1673 "validFrom": "2024-06-18T10:00:00Z",
1674 "credentialSubject": { "id": "did:example:citizenRDid", "endorsement": {} }
1675 }"#,
1676 ) {
1677 Ok(vec) => vec,
1678 Err(e) => panic!("Couldn't deserialize VEC: {}", e),
1679 };
1680
1681 assert!(matches!(vec.type_, DTGCredentialType::Endorsement));
1682 assert!(matches!(vec.subject(), "did:example:citizenRDid"));
1683 assert!(matches!(
1684 vec.credential().credential_subject,
1685 CredentialSubject::Endorsement(_)
1686 ));
1687 }
1688
1689 #[test]
1690 fn test_vec_bad_deserialize() {
1691 match serde_json::from_str::<DTGCredential>(
1692 r#"{
1693 "@context": ["https://www.w3.org/ns/credentials/v2"],
1694 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
1695 "issuer": "did:example:governmentAgencyDid",
1696 "validFrom": "2024-06-18T10:00:00Z",
1697 "credentialSubject": { "id": "did:example:citizenRDid", "other": [] }
1698 }"#,
1699 ) {
1700 Ok(_) => panic!("Expected Unknown Credential type"),
1701 Err(_) => {
1702 }
1704 };
1705 }
1706
1707 #[test]
1708 fn test_vwc_simple_deserialize() {
1709 let vwc: DTGCredential = match serde_json::from_str(
1710 r#"{
1711 "@context": ["https://www.w3.org/ns/credentials/v2"],
1712 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1713 "issuer": "did:example:governmentAgencyDid",
1714 "validFrom": "2024-06-18T10:00:00Z",
1715 "taskContext": "thread-abc-123",
1716 "credentialSubject": { "id": "did:example:citizenRDid" }
1717 }"#,
1718 ) {
1719 Ok(vwc) => vwc,
1720 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
1721 };
1722
1723 assert!(matches!(vwc.type_, DTGCredentialType::Witness));
1724 assert!(matches!(vwc.subject(), "did:example:citizenRDid"));
1725 assert_eq!(vwc.task_context(), Some("thread-abc-123"));
1726 assert!(matches!(
1727 vwc.credential().credential_subject,
1728 CredentialSubject::Witness(_)
1729 ));
1730 }
1731
1732 #[test]
1733 fn test_vwc_full_deserialize() {
1734 let vwc: DTGCredential = match serde_json::from_str(
1735 r#"{
1736 "@context": ["https://www.w3.org/ns/credentials/v2"],
1737 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1738 "issuer": "did:example:governmentAgencyDid",
1739 "validFrom": "2024-06-18T10:00:00Z",
1740 "taskContext": "thread-abc-123",
1741 "credentialSubject": { "id": "did:example:citizenRDid", "digestMultibase": "abcdf", "witnessContext": {} }
1742 }"#,
1743 ) {
1744 Ok(vwc) => vwc,
1745 Err(e) => panic!("Couldn't deserialize VWC: {}", e),
1746 };
1747
1748 assert!(matches!(vwc.type_(), DTGCredentialType::Witness));
1749 assert!(matches!(
1750 vwc.credential().credential_subject,
1751 CredentialSubject::Witness(_)
1752 ));
1753 }
1754
1755 #[test]
1756 fn test_vwc_bad_deserialize() {
1757 if serde_json::from_str::<DTGCredential>(
1758 r#"{
1759 "@context": ["https://www.w3.org/ns/credentials/v2"],
1760 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1761 "issuer": "did:example:governmentAgencyDid",
1762 "validFrom": "2024-06-18T10:00:00Z",
1763 "taskContext": "thread-abc-123",
1764 "credentialSubject": { "id": "did:example:citizenRDid", "digestMultibase": "abcdf", "wrongContext": {} }
1765 }"#,
1766 ).is_ok() {
1767 panic!("Should have failed due to wrong CredentialSubject!");
1768 }
1769 }
1770
1771 #[test]
1772 fn test_rcard_simple_deserialize() {
1773 let rcard: DTGCredential = match serde_json::from_str(
1774 r#"{
1775 "@context": ["https://www.w3.org/ns/credentials/v2"],
1776 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
1777 "issuer": "did:example:governmentAgencyDid",
1778 "validFrom": "2024-06-18T10:00:00Z",
1779 "credentialSubject": { "id": "did:example:citizenRDid", "card": [] }
1780 }"#,
1781 ) {
1782 Ok(rcard) => rcard,
1783 Err(e) => panic!("Couldn't deserialize R-Card: {}", e),
1784 };
1785
1786 assert!(matches!(rcard.type_(), DTGCredentialType::RCard));
1787 assert!(matches!(rcard.subject(), "did:example:citizenRDid"));
1788 assert!(matches!(
1789 rcard.credential().credential_subject,
1790 CredentialSubject::RCard(_)
1791 ));
1792 }
1793
1794 #[test]
1795 fn test_rcard_bad_deserialize() {
1796 if serde_json::from_str::<DTGCredential>(
1797 r#"{
1798 "@context": ["https://www.w3.org/ns/credentials/v2"],
1799 "type": ["VerifiableCredential", "DTGCredential", "RCardCredential"],
1800 "issuer": "did:example:governmentAgencyDid",
1801 "validFrom": "2024-06-18T10:00:00Z",
1802 "credentialSubject": { "id": "did:example:citizenRDid" }
1803 }"#,
1804 )
1805 .is_ok()
1806 {
1807 panic!("Should have failed due to wrong CredentialSubject!");
1808 }
1809 }
1810 #[test]
1811 fn test_deserialize_unknown() {
1812 match serde_json::from_str::<DTGCredential>(
1813 r#"{
1814 "@context": ["https://www.w3.org/ns/credentials/v2"],
1815 "type": ["VerifiableCredential", "DTGCredential", "UnknownCredential"],
1816 "issuer": "did:example:governmentAgencyDid",
1817 "validFrom": "2024-06-18T10:00:00Z",
1818 "credentialSubject": { "id": "did:example:citizenRDid" }
1819 }"#,
1820 ) {
1821 Ok(_) => panic!("Expected Unknown Credential type"),
1822 Err(e) => {
1823 if e.to_string() == "Unknown credential type" {
1824 } else {
1826 panic!("Wrong error type returned");
1827 }
1828 }
1829 };
1830 }
1831
1832 #[test]
1833 fn test_deserialize_mismatched_credential_subject() {
1834 match serde_json::from_str::<DTGCredential>(
1835 r#"{
1836 "@context": ["https://www.w3.org/ns/credentials/v2"],
1837 "type": ["VerifiableCredential", "DTGCredential", "EndorsementCredential"],
1838 "issuer": "did:example:governmentAgencyDid",
1839 "validFrom": "2024-06-18T10:00:00Z",
1840 "credentialSubject": { "id": "did:example:citizenRDid" }
1841 }"#,
1842 ) {
1843 Ok(_) => panic!("Expected Unknown Credential type"),
1844 Err(e) => {
1845 if e.to_string() == "Unknown credential type" {
1846 } else {
1848 panic!("Wrong error type returned");
1849 }
1850 }
1851 };
1852 }
1853
1854 #[test]
1855 fn test_proof_signed() {
1856 let cred: DTGCredential = match serde_json::from_str(
1857 r#"{
1858 "@context": ["https://www.w3.org/ns/credentials/v2"],
1859 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1860 "issuer": "did:example:community",
1861 "validFrom": "2024-06-18T10:00:00Z",
1862 "credentialSubject": { "id": "did:example:rDid" },
1863 "proof": {
1864 "type": "DataIntegrityProof",
1865 "cryptosuite": "eddsa-jcs-2022",
1866 "created": "2025-12-04T00:00:00",
1867 "verificationMethod": "did:example:test#key-1",
1868 "proofPurpose": "assertionMethod",
1869 "proofValue": "abcd"
1870 }
1871 }"#,
1872 ) {
1873 Ok(vmc) => vmc,
1874 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1875 };
1876
1877 assert!(cred.signed());
1878 assert!(cred.proof_value().is_some());
1879 }
1880
1881 #[test]
1882 fn test_proof_not_signed() {
1883 let cred: DTGCredential = match serde_json::from_str(
1884 r#"{
1885 "@context": ["https://www.w3.org/ns/credentials/v2"],
1886 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1887 "issuer": "did:example:community",
1888 "validFrom": "2024-06-18T10:00:00Z",
1889 "credentialSubject": { "id": "did:example:rDid" }
1890 }"#,
1891 ) {
1892 Ok(vmc) => vmc,
1893 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1894 };
1895
1896 assert!(!cred.signed());
1897 assert!(cred.proof_value().is_none());
1898 }
1899
1900 #[test]
1901 fn test_helpers() {
1902 let cred: DTGCredential = match serde_json::from_str(
1903 r#"{
1904 "@context": ["https://www.w3.org/ns/credentials/v2"],
1905 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1906 "issuer": "did:example:issuer",
1907 "validFrom": "2024-06-18T00:00:00Z",
1908 "credentialSubject": { "id": "did:example:subject" }
1909 }"#,
1910 ) {
1911 Ok(vmc) => vmc,
1912 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1913 };
1914
1915 assert_eq!(cred.issuer(), "did:example:issuer");
1916 assert_eq!(cred.subject(), "did:example:subject");
1917 assert_eq!(
1918 cred.valid_from()
1919 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1920 "2024-06-18T00:00:00Z"
1921 );
1922 assert_eq!(cred.valid_until(), None);
1923 }
1924
1925 #[test]
1926 fn test_valid_until() {
1927 let cred: DTGCredential = match serde_json::from_str(
1928 r#"{
1929 "@context": ["https://www.w3.org/ns/credentials/v2"],
1930 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1931 "issuer": "did:example:issuer",
1932 "validFrom": "2024-06-18T00:00:00Z",
1933 "validUntil": "2030-01-01T00:00:00Z",
1934 "credentialSubject": { "id": "did:example:subject" }
1935 }"#,
1936 ) {
1937 Ok(vmc) => vmc,
1938 Err(e) => panic!("Couldn't deserialize credential: {}", e),
1939 };
1940
1941 assert_eq!(
1942 cred.valid_until()
1943 .unwrap()
1944 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1945 "2030-01-01T00:00:00Z"
1946 );
1947 }
1948
1949 #[test]
1950 fn test_bad_type() {
1951 assert!(
1952 std::convert::TryInto::<DTGCredentialType>::try_into(
1953 vec!["bad_type".to_string()].as_slice(),
1954 )
1955 .is_err()
1956 );
1957 }
1958
1959 #[test]
1960 fn test_badly_constructed_vwc() {
1961 let mut cred = DTGCommon::default();
1962 cred.type_.push("WitnessCredential".to_string());
1963 cred.task_context = Some("thread-abc-123".to_string());
1966 cred.credential_subject = CredentialSubject::RCard(CredentialSubjectRCard {
1967 id: "did:example:bad".to_string(),
1968 card: Value::Null,
1969 });
1970
1971 assert!(std::convert::TryInto::<DTGCredential>::try_into(cred).is_err());
1972 }
1973
1974 #[test]
1975 fn test_vwc_missing_task_context() {
1976 match serde_json::from_str::<DTGCredential>(
1978 r#"{
1979 "@context": ["https://www.w3.org/ns/credentials/v2"],
1980 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
1981 "issuer": "did:example:witness",
1982 "validFrom": "2024-06-18T10:00:00Z",
1983 "credentialSubject": { "id": "did:example:observed" }
1984 }"#,
1985 ) {
1986 Ok(_) => panic!("Expected a VWC without taskContext to be rejected"),
1987 Err(e) => assert_eq!(
1988 e.to_string(),
1989 "WitnessCredential is missing the required taskContext property"
1990 ),
1991 }
1992 }
1993
1994 #[test]
1995 fn test_task_context_round_trip() {
1996 let raw = r#"{
1999 "@context": ["https://www.w3.org/ns/credentials/v2"],
2000 "type": ["VerifiableCredential", "DTGCredential", "WitnessCredential"],
2001 "issuer": "did:example:witness",
2002 "validFrom": "2024-06-18T10:00:00Z",
2003 "taskContext": "thread-abc-123",
2004 "credentialSubject": { "id": "did:example:observed" }
2005 }"#;
2006
2007 let cred: DTGCredential = serde_json::from_str(raw).unwrap();
2008 let out = serde_json::to_string(&cred).unwrap();
2009
2010 assert!(out.contains(r#""taskContext":"thread-abc-123""#));
2011 }
2012
2013 #[test]
2014 fn test_task_context_optional_on_other_types() {
2015 let vrc: DTGCredential = serde_json::from_str(
2017 r#"{
2018 "@context": ["https://www.w3.org/ns/credentials/v2"],
2019 "type": ["VerifiableCredential", "DTGCredential", "RelationshipCredential"],
2020 "issuer": "did:example:issuer",
2021 "validFrom": "2024-06-18T10:00:00Z",
2022 "credentialSubject": { "id": "did:example:subject" }
2023 }"#,
2024 )
2025 .unwrap();
2026
2027 assert_eq!(vrc.task_context(), None);
2028 assert!(!serde_json::to_string(&vrc).unwrap().contains("taskContext"));
2030 }
2031
2032 #[test]
2033 fn test_digest_multibase() {
2034 let vrc = DTGCredential::new_vrc(
2035 "did:example:issuer".to_string(),
2036 "did:example:subject".to_string(),
2037 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2038 .unwrap()
2039 .with_timezone(&Utc),
2040 None,
2041 );
2042
2043 let digest = vrc.digest_multibase().unwrap();
2044
2045 assert!(digest.starts_with('z'));
2047
2048 let (base, bytes) = multibase::decode(&digest).unwrap();
2050 assert_eq!(base, multibase::Base::Base58Btc);
2051 assert_eq!(bytes.len(), 34);
2052 assert_eq!(&bytes[..2], &[0x12, 0x20]);
2053
2054 assert_eq!(digest, vrc.digest_multibase().unwrap());
2056
2057 let other = DTGCredential::new_vrc(
2059 "did:example:issuer".to_string(),
2060 "did:example:someone-else".to_string(),
2061 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2062 .unwrap()
2063 .with_timezone(&Utc),
2064 None,
2065 );
2066 assert_ne!(digest, other.digest_multibase().unwrap());
2067 }
2068
2069 #[test]
2070 fn test_verify_digest() {
2071 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2072 .unwrap()
2073 .with_timezone(&Utc);
2074
2075 let vrc = DTGCredential::new_vrc(
2076 "did:example:issuer".to_string(),
2077 "did:example:subject".to_string(),
2078 valid_from,
2079 None,
2080 );
2081
2082 let vwc = DTGCredential::new_vwc(
2083 "did:example:witness".to_string(),
2084 "did:example:issuer".to_string(),
2086 valid_from,
2087 None,
2088 "thread-abc-123".to_string(),
2089 Some(vrc.digest_multibase().unwrap()),
2090 None,
2091 );
2092
2093 assert!(vwc.verify_digest(&vrc).unwrap());
2094
2095 let other = DTGCredential::new_vrc(
2097 "did:example:issuer".to_string(),
2098 "did:example:someone-else".to_string(),
2099 valid_from,
2100 None,
2101 );
2102 assert!(!vwc.verify_digest(&other).unwrap());
2103 }
2104
2105 #[test]
2106 fn test_verify_digest_without_digest() {
2107 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2108 .unwrap()
2109 .with_timezone(&Utc);
2110
2111 let vrc = DTGCredential::new_vrc(
2112 "did:example:issuer".to_string(),
2113 "did:example:subject".to_string(),
2114 valid_from,
2115 None,
2116 );
2117
2118 let vwc = DTGCredential::new_vwc(
2120 "did:example:witness".to_string(),
2121 "did:example:issuer".to_string(),
2122 valid_from,
2123 None,
2124 "thread-abc-123".to_string(),
2125 None,
2126 None,
2127 );
2128
2129 assert!(!vwc.verify_digest(&vrc).unwrap());
2130 }
2131
2132 #[test]
2137 fn test_digest_is_a_base58btc_multihash_over_the_proofless_jcs_form() {
2138 let vmc = DTGCredential::new_vmc(
2139 "did:example:community".to_string(),
2140 "did:example:member".to_string(),
2141 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2142 .unwrap()
2143 .with_timezone(&Utc),
2144 None,
2145 false,
2146 )
2147 .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
2148
2149 let digest = vmc.digest_multibase().unwrap();
2150
2151 assert!(digest.starts_with('z'), "multibase base58btc prefix");
2153
2154 let (base, bytes) = multibase::decode(&digest).unwrap();
2156 assert_eq!(base, Base::Base58Btc);
2157 assert_eq!(bytes.len(), 34);
2158 assert_eq!(&bytes[..2], &[0x12, 0x20]);
2159
2160 assert_eq!(digest, "zQmTJgyPT2ShMQ2AvCHGDoPGjEWyRC7ZNT3MBpe5PP6Vpvu");
2167
2168 assert_eq!(digest, vmc.digest_multibase().unwrap());
2170 }
2171
2172 #[test]
2175 #[allow(deprecated)]
2176 fn the_superseded_hex_digest_is_unchanged() {
2177 let vmc = DTGCredential::new_vmc(
2178 "did:example:community".to_string(),
2179 "did:example:member".to_string(),
2180 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2181 .unwrap()
2182 .with_timezone(&Utc),
2183 None,
2184 false,
2185 )
2186 .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
2187
2188 assert_eq!(
2189 vmc.digest().unwrap(),
2190 "sha256:49c9d5135ab4b5659a343bc79d351e37d64f05add58408cae6eef022828495c2"
2191 );
2192 }
2193
2194 #[test]
2198 fn a_superseded_digest_value_is_rejected_as_malformed() {
2199 let err = decode_digest_multibase(
2200 "sha256:49c9d5135ab4b5659a343bc79d351e37d64f05add58408cae6eef022828495c2",
2201 )
2202 .unwrap_err();
2203
2204 assert!(
2205 matches!(err, DTGCredentialError::InvalidDigest(_)),
2206 "expected InvalidDigest, got {err:?}"
2207 );
2208 }
2209
2210 #[test]
2213 fn digests_are_compared_by_bytes_not_by_string() {
2214 let multihash = {
2217 let mut v = vec![0x12u8, 0x20];
2218 v.extend_from_slice(&Sha256::digest(b"an edge credential"));
2219 v
2220 };
2221 let b58 = multibase::encode(Base::Base58Btc, &multihash);
2222 let b16 = multibase::encode(Base::Base16Lower, &multihash);
2223
2224 assert_ne!(b58, b16, "the two spellings differ as strings");
2225 assert!(
2226 digests_match(&b58, &b16).unwrap(),
2227 "but name the same digest"
2228 );
2229 }
2230
2231 #[test]
2235 fn an_unaccepted_hash_algorithm_is_rejected_rather_than_mismatched() {
2236 let mut multihash = vec![0x13u8, 0x40];
2238 multihash.extend_from_slice(&[0u8; 64]);
2239 let encoded = multibase::encode(Base::Base58Btc, &multihash);
2240
2241 assert!(matches!(
2242 decode_digest_multibase(&encoded),
2243 Err(DTGCredentialError::UnsupportedDigestAlgorithm(0x13))
2244 ));
2245 }
2246
2247 #[cfg(feature = "affinidi-signing")]
2251 #[tokio::test]
2252 async fn test_digest_is_unchanged_by_signing() {
2253 use affinidi_secrets_resolver::secrets::Secret;
2254
2255 let secret = Secret::generate_ed25519(None, None);
2256
2257 let mut vmc = DTGCredential::new_vmc(
2258 "did:example:community".to_string(),
2259 "did:example:member".to_string(),
2260 Utc::now(),
2261 None,
2262 false,
2263 );
2264
2265 let before = vmc.digest_multibase().unwrap();
2266 vmc.sign(&secret, None).await.expect("signs");
2267 assert!(vmc.signed());
2268 assert_eq!(before, vmc.digest_multibase().unwrap());
2269 }
2270
2271 fn wire(c: &DTGCredential) -> Value {
2273 serde_json::to_value(c.credential()).expect("credential serialises")
2274 }
2275
2276 #[test]
2279 fn test_member_vmc_acknowledges_its_grant() {
2280 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2281 .unwrap()
2282 .with_timezone(&Utc);
2283
2284 let grant = DTGCredential::new_vmc(
2285 "did:example:community".to_string(),
2286 "did:example:member".to_string(),
2287 valid_from,
2288 None,
2289 false,
2290 );
2291
2292 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2293
2294 assert_eq!(ack.issuer(), "did:example:member");
2296 assert_eq!(ack.subject(), "did:example:community");
2297
2298 assert_eq!(grant.subject_digest(), None);
2300 assert_eq!(
2301 ack.subject_digest(),
2302 Some(grant.digest_multibase().unwrap().as_str())
2303 );
2304
2305 assert!(ack.acknowledges(&grant).unwrap());
2306 }
2307
2308 #[test]
2311 fn test_acknowledges_rejects_a_mismatched_pair() {
2312 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2313 .unwrap()
2314 .with_timezone(&Utc);
2315
2316 let grant = DTGCredential::new_vmc(
2317 "did:example:community".to_string(),
2318 "did:example:member".to_string(),
2319 valid_from,
2320 None,
2321 false,
2322 );
2323 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2324
2325 let other_member = DTGCredential::new_vmc(
2327 "did:example:community".to_string(),
2328 "did:example:someone-else".to_string(),
2329 valid_from,
2330 None,
2331 false,
2332 );
2333 assert!(!ack.acknowledges(&other_member).unwrap());
2334
2335 let other_community = DTGCredential::new_vmc(
2337 "did:example:other-community".to_string(),
2338 "did:example:member".to_string(),
2339 valid_from,
2340 None,
2341 false,
2342 );
2343 assert!(!ack.acknowledges(&other_community).unwrap());
2344
2345 let renewed = DTGCredential::new_vmc(
2349 "did:example:community".to_string(),
2350 "did:example:member".to_string(),
2351 valid_from + chrono::Duration::days(365),
2352 None,
2353 false,
2354 );
2355 assert!(!ack.acknowledges(&renewed).unwrap());
2356
2357 let ack_of_ack =
2359 DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2360 assert!(!ack_of_ack.acknowledges(&ack).unwrap());
2361
2362 assert!(!grant.acknowledges(&grant).unwrap());
2364 }
2365
2366 #[test]
2370 fn credential_status_survives_a_round_trip() {
2371 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2372 .unwrap()
2373 .with_timezone(&Utc);
2374
2375 let mut grant = wire(&DTGCredential::new_vmc(
2376 "did:example:community".to_string(),
2377 "did:example:member".to_string(),
2378 valid_from,
2379 None,
2380 false,
2381 ));
2382 let status = serde_json::json!({
2383 "id": "https://community.example/status#7",
2384 "type": "BitstringStatusListEntry",
2385 "statusPurpose": "revocation",
2386 "statusListIndex": "7"
2387 });
2388 grant["credentialStatus"] = status.clone();
2389
2390 let parsed: DTGCredential = serde_json::from_value(grant.clone()).expect("parses");
2391 assert_eq!(
2392 parsed.credential().credential_status.as_ref(),
2393 Some(&status)
2394 );
2395 assert_eq!(wire(&parsed).get("credentialStatus"), Some(&status));
2396 assert_eq!(
2397 parsed.digest_multibase().unwrap(),
2398 digest_multibase_json(&grant).unwrap(),
2399 "the digest must not change under a round trip that preserves every member"
2400 );
2401 }
2402
2403 #[test]
2406 fn unmodelled_top_level_members_survive_a_round_trip() {
2407 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2408 .unwrap()
2409 .with_timezone(&Utc);
2410
2411 let mut grant = wire(&DTGCredential::new_vmc(
2412 "did:example:community".to_string(),
2413 "did:example:member".to_string(),
2414 valid_from,
2415 None,
2416 false,
2417 ));
2418 let schema = serde_json::json!({
2419 "id": "https://community.example/schemas/vmc",
2420 "type": "JsonSchema"
2421 });
2422 grant["credentialSchema"] = schema.clone();
2423
2424 let parsed: DTGCredential = serde_json::from_value(grant.clone()).expect("parses");
2425 assert_eq!(
2426 parsed.credential().extra.get("credentialSchema"),
2427 Some(&schema)
2428 );
2429 assert_eq!(
2430 parsed.digest_multibase().unwrap(),
2431 digest_multibase_json(&grant).unwrap()
2432 );
2433 }
2434
2435 #[test]
2452 fn the_acknowledgement_digests_the_grant_as_it_arrived() {
2453 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2454 .unwrap()
2455 .with_timezone(&Utc);
2456
2457 let mut grant = wire(&DTGCredential::new_vmc(
2458 "did:example:community".to_string(),
2459 "did:example:member".to_string(),
2460 valid_from,
2461 None,
2462 false,
2463 ));
2464 grant["validFrom"] = Value::String("2025-12-11T00:00:00.000+00:00".to_string());
2466
2467 let parsed: DTGCredential = serde_json::from_value(grant.clone()).expect("parses");
2469 assert_ne!(
2470 wire(&parsed).get("validFrom"),
2471 grant.get("validFrom"),
2472 "the model is expected to normalize the timestamp; if it now round-trips \
2473 verbatim, this test has stopped guarding anything"
2474 );
2475
2476 let ack = DTGCredential::new_member_vmc(&grant, valid_from, None).expect("builds");
2477
2478 assert_eq!(
2479 ack.subject_digest(),
2480 Some(digest_multibase_json(&grant).unwrap().as_str()),
2481 "the acknowledgement must digest the grant as received"
2482 );
2483 assert_ne!(
2484 ack.subject_digest(),
2485 Some(parsed.digest_multibase().unwrap().as_str()),
2486 "digesting the parsed model would produce a digest the community cannot match"
2487 );
2488 }
2489
2490 #[test]
2491 fn digest_multibase_json_agrees_with_digest_where_the_model_is_complete() {
2492 let vmc = DTGCredential::new_vmc(
2493 "did:example:community".to_string(),
2494 "did:example:member".to_string(),
2495 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2496 .unwrap()
2497 .with_timezone(&Utc),
2498 None,
2499 false,
2500 )
2501 .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
2502
2503 assert_eq!(
2504 vmc.digest_multibase().unwrap(),
2505 digest_multibase_json(&wire(&vmc)).unwrap()
2506 );
2507 }
2508
2509 #[test]
2512 fn test_acknowledges_is_membership_only() {
2513 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2514 .unwrap()
2515 .with_timezone(&Utc);
2516
2517 let grant = DTGCredential::new_vmc(
2518 "did:example:community".to_string(),
2519 "did:example:member".to_string(),
2520 valid_from,
2521 None,
2522 false,
2523 );
2524 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2525
2526 let vrc = DTGCredential::new_vrc(
2527 "did:example:member".to_string(),
2528 "did:example:community".to_string(),
2529 valid_from,
2530 None,
2531 );
2532 assert!(!ack.acknowledges(&vrc).unwrap());
2533
2534 let vwc = DTGCredential::new_vwc(
2536 "did:example:witness".to_string(),
2537 "did:example:community".to_string(),
2538 valid_from,
2539 None,
2540 "thread-abc-123".to_string(),
2541 Some(grant.digest_multibase().unwrap()),
2542 None,
2543 );
2544 assert!(vwc.verify_digest(&grant).unwrap(), "the digest does match");
2545 assert!(
2546 !vwc.acknowledges(&grant).unwrap(),
2547 "but a VWC is not the member's acknowledgement"
2548 );
2549 }
2550
2551 #[test]
2555 fn test_new_member_vmc_refuses_a_non_grant() {
2556 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2557 .unwrap()
2558 .with_timezone(&Utc);
2559
2560 let vrc = DTGCredential::new_vrc(
2561 "did:example:a".to_string(),
2562 "did:example:b".to_string(),
2563 valid_from,
2564 None,
2565 );
2566 assert!(matches!(
2567 DTGCredential::new_member_vmc(&wire(&vrc), valid_from, None),
2568 Err(DTGCredentialError::NotAMembershipGrant(_))
2569 ));
2570
2571 let grant = DTGCredential::new_vmc(
2572 "did:example:community".to_string(),
2573 "did:example:member".to_string(),
2574 valid_from,
2575 None,
2576 false,
2577 );
2578 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2579 assert!(matches!(
2580 DTGCredential::new_member_vmc(&wire(&ack), valid_from, None),
2581 Err(DTGCredentialError::NotAMembershipGrant(_))
2582 ));
2583 }
2584
2585 #[test]
2590 fn test_member_issued_vmc_deserializes_as_membership_not_witness() {
2591 let vmc: DTGCredential = serde_json::from_str(
2592 r#"{
2593 "@context": ["https://www.w3.org/ns/credentials/v2"],
2594 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
2595 "issuer": "did:example:member",
2596 "validFrom": "2024-06-18T10:00:00Z",
2597 "credentialSubject": {
2598 "id": "did:example:community",
2599 "digestMultibase": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
2600 }
2601 }"#,
2602 )
2603 .expect("deserializes");
2604
2605 assert!(matches!(vmc.type_, DTGCredentialType::Membership));
2606 assert!(matches!(
2607 vmc.credential().credential_subject,
2608 CredentialSubject::Membership(_)
2609 ));
2610 assert_eq!(
2611 vmc.subject_digest(),
2612 Some("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
2613 );
2614 assert_eq!(vmc.subject(), "did:example:community");
2615 }
2616
2617 #[test]
2620 fn test_membership_credential_rejects_a_witness_context() {
2621 let result: Result<DTGCredential, _> = serde_json::from_str(
2622 r#"{
2623 "@context": ["https://www.w3.org/ns/credentials/v2"],
2624 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
2625 "issuer": "did:example:member",
2626 "validFrom": "2024-06-18T10:00:00Z",
2627 "credentialSubject": {
2628 "id": "did:example:community",
2629 "digestMultibase": "sha256:e3b0c4",
2630 "witnessContext": { "event": "not a membership property" }
2631 }
2632 }"#,
2633 );
2634 assert!(result.is_err());
2635 }
2636
2637 #[test]
2640 fn test_the_two_halves_round_trip_over_the_wire() {
2641 let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2642 .unwrap()
2643 .with_timezone(&Utc);
2644
2645 let grant = DTGCredential::new_vmc(
2646 "did:example:community".to_string(),
2647 "did:example:member".to_string(),
2648 valid_from,
2649 None,
2650 false,
2651 );
2652 let ack = DTGCredential::new_member_vmc(&wire(&grant), valid_from, None).expect("builds");
2653
2654 let grant_json = serde_json::to_value(&grant).unwrap();
2655 assert!(
2656 grant_json["credentialSubject"]
2657 .get("digestMultibase")
2658 .is_none(),
2659 "the grant MUST omit `digestMultibase`: {grant_json}"
2660 );
2661
2662 let ack_json = serde_json::to_value(&ack).unwrap();
2663 assert_eq!(
2664 ack_json["credentialSubject"]["digestMultibase"],
2665 Value::String(grant.digest_multibase().unwrap()),
2666 );
2667
2668 let grant: DTGCredential = serde_json::from_value(grant_json).expect("grant round trips");
2671 let ack: DTGCredential = serde_json::from_value(ack_json).expect("ack round trips");
2672 assert!(ack.acknowledges(&grant).unwrap());
2673 }
2674
2675 #[test]
2676 fn test_iso8601_format_option() {
2677 let now: DateTime<Utc> = DateTime::parse_from_rfc3339(
2678 &Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
2679 )
2680 .unwrap()
2681 .to_utc();
2682 let cred = DTGCommon {
2683 valid_until: Some(now),
2684 ..Default::default()
2685 };
2686
2687 let value = serde_json::to_value(&cred).unwrap();
2688 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
2689 assert_eq!(cred2.valid_until, Some(now));
2690
2691 let cred = DTGCommon::default();
2692 let value = serde_json::to_value(&cred).unwrap();
2693 let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
2694 assert_eq!(cred2.valid_until, None);
2695 }
2696
2697 #[cfg(feature = "affinidi-signing")]
2698 #[tokio::test]
2699 async fn test_signing() {
2700 use affinidi_secrets_resolver::secrets::Secret;
2701
2702 let secret = Secret::generate_ed25519(None, None);
2703
2704 let mut cred = DTGCredential::new_vrc(
2705 "did:example:issuer".to_string(),
2706 "did:example:subject".to_string(),
2707 Utc::now(),
2708 None,
2709 );
2710
2711 assert!(cred.sign(&secret, Some(Utc::now())).await.is_ok());
2712
2713 assert!(
2714 cred.verify_proof_with_public_key(secret.get_public_bytes())
2715 .is_ok()
2716 );
2717
2718 let secret2 = Secret::generate_ed25519(None, None);
2719 assert!(
2720 cred.verify_proof_with_public_key(secret2.get_public_bytes())
2721 .is_err()
2722 );
2723 }
2724
2725 #[cfg(feature = "affinidi-signing")]
2733 #[tokio::test]
2734 async fn test_id_is_covered_by_the_proof() {
2735 use affinidi_secrets_resolver::secrets::Secret;
2736
2737 let secret = Secret::generate_ed25519(None, None);
2738
2739 let mut cred = DTGCredential::new_vrc(
2740 "did:example:issuer".to_string(),
2741 "did:example:subject".to_string(),
2742 Utc::now(),
2743 None,
2744 )
2745 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
2746
2747 cred.sign(&secret, Some(Utc::now()))
2748 .await
2749 .expect("signing a credential that carries an id");
2750 assert!(
2751 cred.verify_proof_with_public_key(secret.get_public_bytes())
2752 .is_ok(),
2753 "an id set before signing verifies"
2754 );
2755
2756 cred.set_id("urn:uuid:00000000-0000-0000-0000-000000000000");
2759 assert!(
2760 cred.verify_proof_with_public_key(secret.get_public_bytes())
2761 .is_err(),
2762 "an id changed after signing must break the proof"
2763 );
2764 }
2765
2766 #[cfg(feature = "affinidi-signing")]
2767 #[tokio::test]
2768 async fn test_signing_error() {
2769 use affinidi_secrets_resolver::secrets::Secret;
2770
2771 let secret = Secret::generate_x25519(None, None).unwrap();
2772
2773 let mut cred = DTGCredential::new_vrc(
2774 "did:example:issuer".to_string(),
2775 "did:example:subject".to_string(),
2776 Utc::now(),
2777 None,
2778 );
2779
2780 assert!(cred.sign(&secret, Some(Utc::now())).await.is_err());
2781 }
2782
2783 #[cfg(feature = "affinidi-signing")]
2784 #[test]
2785 fn test_signing_no_proof() {
2786 use crate::DTGCredentialError;
2787 use affinidi_secrets_resolver::secrets::Secret;
2788
2789 let cred = DTGCredential::new_vrc(
2790 "did:example:issuer".to_string(),
2791 "did:example:subject".to_string(),
2792 Utc::now(),
2793 None,
2794 );
2795
2796 let secret = Secret::generate_ed25519(None, None);
2797 match cred.verify_proof_with_public_key(secret.get_public_bytes()) {
2798 Err(DTGCredentialError::NotSigned) => {
2799 }
2801 _ => panic!("Expected NotSigned error!"),
2802 }
2803 }
2804}