Skip to main content

dtg_credentials/
lib.rs

1/*! Decentralized Trust Graph (DTG) Credentials
2*/
3
4use affinidi_data_integrity::DataIntegrityProof;
5#[cfg(feature = "affinidi-signing")]
6use affinidi_data_integrity::{DataIntegrityError, SignOptions, VerifyOptions};
7#[cfg(feature = "affinidi-signing")]
8use affinidi_secrets_resolver::secrets::Secret;
9use chrono::{DateTime, Utc};
10use multibase::Base;
11use serde::{Deserialize, Serialize, Serializer};
12use serde_json::Value;
13use sha2::{Digest, Sha256};
14use std::fmt::Display;
15use thiserror::Error;
16
17pub mod authority;
18pub mod create;
19pub mod delegation;
20
21/// What W3C VC Format is the credential using?
22#[derive(Clone, Copy, Debug)]
23pub enum W3CVCVersion {
24    /// <https://www.w3.org/2018/credentials/v1>
25    V1_1,
26
27    /// <https://www.w3.org/ns/credentials/v2>
28    V2_0,
29}
30
31impl TryFrom<&[String]> for W3CVCVersion {
32    type Error = DTGCredentialError;
33
34    /// Will return the W3C Version from the context array
35    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/// Errors related to DTG Credentials
47///
48/// New variants may be added in minor releases; match with a wildcard arm.
49#[derive(Error, Debug)]
50#[non_exhaustive]
51pub enum DTGCredentialError {
52    #[error("Unknown credential type")]
53    UnknownCredential,
54
55    #[cfg(feature = "affinidi-signing")]
56    #[error("Data Integrity Error: {0}")]
57    DataIntegrity(#[from] DataIntegrityError),
58
59    #[error("Credential is not signed")]
60    NotSigned,
61
62    #[error("Unknown W3C VC Version")]
63    UnknownVCVersion,
64
65    /// An AuthorityCredential (VAC) carried an empty `actions` list.
66    ///
67    /// Emptiness is never a wildcard: a VAC conferring no actions confers nothing, and is
68    /// rejected rather than treated as unrestricted.
69    #[error("AuthorityCredential carries an empty actions list, which confers nothing")]
70    EmptyAuthorityActions,
71
72    /// [DTGCredential::attenuate] was called on a credential that is not a VAC.
73    #[error("not an AuthorityCredential, so there is no authority to attenuate")]
74    NotAnAuthorityCredential,
75
76    /// [DTGCredential::attenuate] was called on a VAC with no `id`.
77    ///
78    /// No longer produced. Working Draft 02 makes `authority.parent` a **digest** of the
79    /// parent rather than its `id`, precisely so that no credential needs a top-level
80    /// identifier merely in order to be referenced.
81    #[deprecated(
82        since = "0.7.0",
83        note = "Never returned. `authority.parent` is a digest as of Working Draft 02, so a \
84                parent VAC no longer needs an `id` to be attenuated. This variant will be \
85                removed in a future release."
86    )]
87    #[error("cannot attenuate a credential with no id — the derived VAC could not name it")]
88    AttenuationParentHasNoId,
89
90    /// A digest value was not a well-formed `digestMultibase`.
91    ///
92    /// Either the multibase envelope or the multihash inside it failed to decode. A
93    /// `sha256:<hex>` value produced against Working Draft 01 lands here, which is the
94    /// intended outcome: it is reported rather than silently compared as unequal.
95    #[error("not a well-formed digestMultibase value: {0}")]
96    InvalidDigest(String),
97
98    /// A digest named a hash algorithm this library does not implement.
99    ///
100    /// The specification permits a governing party to require a stronger hash, and carries
101    /// the algorithm in the value itself. A verifier MUST reject an algorithm it does not
102    /// accept rather than treating it as a mismatch — hence a distinct error.
103    #[error("digest uses multihash algorithm 0x{0:x}, which this library does not accept")]
104    UnsupportedDigestAlgorithm(u64),
105
106    /// A DelegationCredential (VDC) was not a well-formed grant or acceptance.
107    #[error("malformed DelegationCredential: {0}")]
108    MalformedDelegation(String),
109
110    /// A delegation acknowledgement was built against something that is not a
111    /// delegation grant.
112    #[error("Not a delegation grant: {0}")]
113    NotADelegationGrant(String),
114
115    /// An attenuation attempted to confer more than its parent held.
116    #[error("attenuation would widen the parent grant: {0}")]
117    AttenuationWidens(String),
118
119    /// A WitnessCredential (VWC) was missing the REQUIRED `taskContext` property
120    #[error("WitnessCredential is missing the required taskContext property")]
121    MissingTaskContext,
122
123    /// A document a credential was to cite as its `taskContext` is not a Trust Task
124    /// document that can be named: it is not a JSON object, or it has no string `id`.
125    #[error("cannot cite this document as a taskContext: {0}")]
126    MalformedTaskDocument(String),
127
128    /// [DTGCredential::new_vwc_for_session] was given something other than the
129    /// `witness/session` document that opened the witness session.
130    ///
131    /// A VWC names the *innermost* exchange that attests the witnessing (Trust Tasks
132    /// §4.9.1): the party's own `witness/session`, not the `witness/session/submit`
133    /// exchanged on its thread and not the relationship exchange that contains it.
134    #[error("not the witness/session document that opened the session: {0}")]
135    NotAWitnessSession(String),
136
137    /// The credential could not be canonicalized (JCS, RFC 8785) for digesting
138    #[error("Could not canonicalize credential: {0}")]
139    Canonicalization(String),
140
141    /// A credential was not of the type an operation requires
142    #[error("Expected a {expected}, got a {got}")]
143    WrongCredentialType { expected: String, got: String },
144
145    /// A membership acknowledgement was built against something that is not a
146    /// community-issued membership grant
147    #[error("Not a community-issued membership grant: {0}")]
148    NotAMembershipGrant(String),
149
150    /// A credential's `validUntil` is not after its `validFrom`.
151    ///
152    /// A window that closes before, or at the instant, it opens describes a credential
153    /// that is never valid. It is refused where a credential is built or signed rather than
154    /// left for every verifier to notice. A `validFrom` in the past is not refused:
155    /// backdating is how a re-issued credential keeps the date the original took effect.
156    ///
157    /// Compared at whole seconds, the precision the wire form carries.
158    #[error("validUntil {valid_until} is not after validFrom {valid_from}")]
159    InvalidValidityWindow {
160        valid_from: DateTime<Utc>,
161        valid_until: DateTime<Utc>,
162    },
163
164    /// A JSON document was nested more deeply than [`MAX_JSON_DEPTH`] allows.
165    ///
166    /// Digesting, signing and verifying all walk a credential recursively, so a value deep
167    /// enough exhausts the stack and aborts the process. It is refused before any of that
168    /// work starts.
169    #[error("JSON is nested more than {max} levels deep")]
170    JsonTooDeep { max: usize },
171
172    /// A grant names a different party as its subject from the one answering it.
173    ///
174    /// Returned by [DTGCredential::new_member_vmc_for] and
175    /// [DTGCredential::new_delegate_vdc_for]. A party answers a grant for itself, so a grant
176    /// naming anyone else is refused rather than answered in that party's name.
177    #[error("the grant names `{found}` as its subject, not `{expected}`")]
178    NotTheGrantSubject { expected: String, found: String },
179
180    /// An acknowledgement or acceptance would remain valid after the grant it answers.
181    ///
182    /// `valid_until` is `None` where the answer was open-ended against a grant that expires.
183    #[error("would remain valid after the grant it answers, which expires at {grant_valid_until}")]
184    OutlivesGrant {
185        valid_until: Option<DateTime<Utc>>,
186        grant_valid_until: DateTime<Utc>,
187    },
188
189    /// A proof verified, but was made with a verification method that does not belong to
190    /// the credential's issuer.
191    #[error("the proof was made by `{verification_method}`, which is not the issuer `{issuer}`")]
192    ProofNotFromIssuer {
193        issuer: String,
194        verification_method: String,
195    },
196
197    /// A credential was not in force at the instant it was checked against.
198    #[error("the credential is not valid at {at}")]
199    NotValidAt { at: DateTime<Utc> },
200
201    /// A credential in its wire form lacks a member it needs, or carries one that cannot be
202    /// read.
203    #[error("malformed credential: {0}")]
204    MalformedCredential(String),
205}
206
207/// Defined DTG Credentials
208#[derive(Serialize, Deserialize, Debug, Clone)]
209#[serde(try_from = "DTGCommon")]
210pub struct DTGCredential {
211    /// The DTG Credential inner struct
212    #[serde(flatten)]
213    credential: DTGCommon,
214
215    /// Type of the credential
216    #[serde(skip)]
217    type_: DTGCredentialType,
218
219    /// W3C VC Version
220    #[serde(skip)]
221    version: W3CVCVersion,
222}
223
224impl DTGCredential {
225    /// get the raw credential
226    pub fn credential(&self) -> &DTGCommon {
227        &self.credential
228    }
229
230    /// Get the raw credential as mutable
231    pub fn credential_mut(&mut self) -> &mut DTGCommon {
232        &mut self.credential
233    }
234
235    /// Has this credential been signed?
236    pub fn signed(&self) -> bool {
237        self.credential.signed()
238    }
239
240    /// get the credential type
241    pub fn type_(&self) -> DTGCredentialType {
242        self.type_.clone()
243    }
244
245    /// This credential's own identifier, if it has one.
246    ///
247    /// `None` for a credential built by one of the `new_*` constructors and never given one
248    /// with [DTGCredential::with_id]. See [DTGCommon::id] for why a counterparty may require
249    /// it.
250    pub fn id(&self) -> Option<&str> {
251        self.credential.id()
252    }
253
254    /// Returns the Issuer DID
255    pub fn issuer(&self) -> &str {
256        self.credential.issuer()
257    }
258
259    /// Returns the Subject DID
260    pub fn subject(&self) -> &str {
261        self.credential.subject()
262    }
263
264    /// Returns the valid_from timestamp
265    pub fn valid_from(&self) -> DateTime<Utc> {
266        self.credential.valid_from()
267    }
268
269    /// Returns the valid until timestamp
270    pub fn valid_until(&self) -> Option<DateTime<Utc>> {
271        self.credential.valid_until()
272    }
273
274    /// The `id` naming the trust task exchange this credential cites, if set. See
275    /// [DTGCommon::task_context].
276    ///
277    /// This is always `Some` for [DTGCredentialType::Witness] credentials, where the spec
278    /// makes `taskContext` REQUIRED.
279    pub fn task_context(&self) -> Option<&str> {
280        self.credential.task_context()
281    }
282
283    /// The task digest of the Trust Task document `taskContext` names, if set. See
284    /// [DTGCommon::task_digest_multibase].
285    pub fn task_digest_multibase(&self) -> Option<&str> {
286        self.credential.task_digest_multibase()
287    }
288
289    /// Does this credential cite `document` — the Trust Task document its `taskContext`
290    /// names — and is it bound to that document's content?
291    ///
292    /// Both halves of the citation have to hold:
293    ///
294    /// 1. `taskContext` equals the document's `id`, which **locates** the exchange;
295    /// 2. `taskDigestMultibase` matches the task digest recomputed from `document`, which
296    ///    **binds** the credential to it.
297    ///
298    /// Returns `Ok(false)` where either fails, and where the credential carries no
299    /// `taskContext` or no `taskDigestMultibase`. The last case is deliberate: Trust Tasks
300    /// §4.9.3 forbids falling back to comparing `id`s alone, because an `id` is a name
301    /// anyone may reuse on a counterfeit.
302    ///
303    /// # Compares bytes, not strings
304    ///
305    /// The digest is recomputed with the top-level `proof` removed, so a signed and an
306    /// unsigned copy of the same document agree, and compared as **decoded multihash bytes**.
307    /// A task digest may be base58btc or base64url: two conforming encodings of one digest
308    /// are different strings, and a string comparison would reject an honest citation.
309    ///
310    /// # What this does not check
311    ///
312    /// That the exchange completed, which needs the outcome evidence of DTG Core
313    /// Credentials §Outcome Interpretability, and that the document was attributable, which
314    /// needs its own proof. A task digest attests content, not authenticity. It is
315    /// load-bearing because it is the credential's issuer who signed it.
316    ///
317    /// # Errors
318    ///
319    /// [DTGCredentialError::InvalidDigest] if the carried value is not a well-formed
320    /// multibase multihash, and [DTGCredentialError::UnsupportedDigestAlgorithm] if it
321    /// names a hash this library does not implement. Trust Tasks §4.9.3 requires such a
322    /// citation to be treated as unverified, never recomputed under another algorithm, so
323    /// it is reported rather than folded into `Ok(false)`. [DTGCredentialError::JsonTooDeep]
324    /// if `document` is nested past [`MAX_JSON_DEPTH`].
325    pub fn cites_task(&self, document: &Value) -> Result<bool, DTGCredentialError> {
326        let (Some(task_context), Some(carried)) =
327            (self.task_context(), self.task_digest_multibase())
328        else {
329            return Ok(false);
330        };
331
332        if document.get("id").and_then(Value::as_str) != Some(task_context) {
333            return Ok(false);
334        }
335
336        digests_match(carried, &task_digest_multibase_json(document)?)
337    }
338
339    /// This credential's digest, in the encoding a credential that references it carries —
340    /// a member-issued VMC acknowledging a membership grant, a VWC attesting an edge
341    /// credential, or the `parent` of an attenuated VAC.
342    ///
343    /// Per DTG Core Credentials [Digest Encoding], that is the SHA-256 hash of the
344    /// credential's JSON representation **excluding its top-level `proof` member**,
345    /// canonicalized with the JSON Canonicalization Scheme
346    /// ([JCS, RFC 8785](https://datatracker.ietf.org/doc/html/rfc8785)), wrapped in a
347    /// `sha2-256` multihash and encoded base58btc with a multibase `z` prefix.
348    ///
349    /// [Digest Encoding]: https://github.com/trustoverip/dtgwg-cred-spec
350    ///
351    /// # Why `proof` is excluded
352    ///
353    /// The digest binds to what the credential *says*, not to a particular signature over
354    /// it. A referencing credential therefore survives a re-proofing of its referent: a
355    /// re-signed grant carrying identical claims still satisfies an acknowledgement made
356    /// against the earlier signature. It also means the digest can be computed before the
357    /// referent is signed, and is stable whichever of its proofs a holder happens to have.
358    ///
359    /// # Prefer the wire form for a credential you received
360    ///
361    /// This digests the model. [`DTGCommon::extra`] carries top-level members this library
362    /// does not model through a round trip, so for most received credentials the two agree
363    /// — but a member *inside* `credentialSubject` that the subject types do not model is
364    /// still not represented. Where you still hold the bytes a counterparty sent, digest
365    /// those with [`digest_multibase_json`].
366    ///
367    /// # Errors
368    ///
369    /// [DTGCredentialError::JsonTooDeep] if an open JSON member takes the credential past
370    /// [`MAX_JSON_DEPTH`], checked before the credential is cloned or serialized.
371    pub fn digest_multibase(&self) -> Result<String, DTGCredentialError> {
372        self.credential.check_depth()?;
373
374        let unsigned = DTGCommon {
375            proof: None,
376            ..self.credential.clone()
377        };
378        let value = serde_json::to_value(&unsigned)
379            .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
380        digest_multibase_json(&value)
381    }
382
383    /// This credential's digest in the superseded `sha256:<hex>` encoding.
384    #[deprecated(
385        since = "0.7.0",
386        note = "Working Draft 02 replaced the `sha256:<hex>` digest with a base58btc \
387                multibase multihash under the property name `digestMultibase`. Use \
388                DTGCredential::digest_multibase. This method will be removed in a future \
389                release."
390    )]
391    pub fn digest(&self) -> Result<String, DTGCredentialError> {
392        self.credential.check_depth()?;
393
394        let unsigned = DTGCommon {
395            proof: None,
396            ..self.credential.clone()
397        };
398        let value = serde_json::to_value(&unsigned)
399            .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
400        #[allow(deprecated)]
401        digest_json(&value)
402    }
403
404    /// The digest this credential carries of the credential it references, if it carries one.
405    ///
406    /// `Some` for a member-issued VMC (which MUST carry one), for a VWC bound to the edge
407    /// credential it attests, for an attenuated VAC (`authority.parent`), and for a
408    /// derived or accepting VDC (`delegation.parent` / `delegation.accepts`). `None` for a
409    /// community-issued VMC, which MUST omit it, and for a credential that references
410    /// nothing.
411    pub fn subject_digest(&self) -> Option<&str> {
412        match &self.credential.credential_subject {
413            CredentialSubject::Membership(subject) => subject.digest_multibase.as_deref(),
414            CredentialSubject::Witness(subject) => subject.digest_multibase.as_deref(),
415            CredentialSubject::Authority(subject) => subject.authority.parent.as_deref(),
416            CredentialSubject::Delegation(subject) => subject
417                .delegation
418                .accepts
419                .as_deref()
420                .or(subject.delegation.parent.as_deref()),
421            _ => None,
422        }
423    }
424
425    /// Checks that the digest this credential carries matches the credential it claims to
426    /// reference.
427    ///
428    /// Answers one question only — whether the hashes agree. It does not check that the two
429    /// credentials are of the types the reference requires, nor that their issuers and
430    /// subjects line up. For a membership acknowledgement, [DTGCredential::acknowledges]
431    /// checks all of that together and is what a verifier completing an edge should call.
432    ///
433    /// # Compares bytes, not strings
434    ///
435    /// The specification requires a verifier to decode the multibase envelope and the
436    /// multihash inside it, and to compare the algorithm identifier and the raw digest —
437    /// never the encoded strings. Two equal digests can be written differently, and a
438    /// string comparison would report a mismatch where the credentials agree.
439    ///
440    /// Returns `Ok(false)` if the digests do not match, or if this credential carries no
441    /// digest, in which case there is nothing to rely on.
442    ///
443    /// # Errors
444    ///
445    /// [DTGCredentialError::InvalidDigest] if the carried value is not a well-formed
446    /// `digestMultibase` — a Working Draft 01 `sha256:<hex>` value among them — and
447    /// [DTGCredentialError::UnsupportedDigestAlgorithm] if it names a hash this library
448    /// does not implement. Both are reported rather than folded into `Ok(false)`: a digest
449    /// that cannot be read is not a digest that disagrees.
450    pub fn verify_digest(&self, referenced: &DTGCredential) -> Result<bool, DTGCredentialError> {
451        let Some(carried) = self.subject_digest() else {
452            return Ok(false);
453        };
454
455        digests_match(carried, &referenced.digest_multibase()?)
456    }
457
458    /// Does this member-issued VMC acknowledge `grant`, completing that membership edge?
459    ///
460    /// A membership edge is complete only when both VMCs of the pair exist and are valid:
461    /// the community-issued VMC that grants membership, and the member-issued VMC that
462    /// acknowledges it. This checks everything that binds the two together:
463    ///
464    /// 1. `grant` is a `MembershipCredential` carrying no `digest` — a community-issued grant
465    /// 2. `self` is a `MembershipCredential` carrying one — a member-issued acknowledgement
466    /// 3. the two name the same pair of parties, in mirrored roles: this credential's issuer
467    ///    is the grant's subject, and its subject is the grant's issuer
468    /// 4. the `digest` matches the grant
469    ///
470    /// Returns `Ok(false)` where any of those does not hold, rather than distinguishing
471    /// them: a caller deciding whether an edge is complete has one decision to make, and
472    /// every failing case answers it the same way.
473    ///
474    /// # What this does not check
475    ///
476    /// Neither credential's proof, and neither validity window. Both are the caller's to
477    /// verify — proof verification needs a resolver this crate does not hold, and whether a
478    /// window is current is a question about an instant the caller chooses. An edge is
479    /// complete when both VMCs are *valid* as well as bound, and this covers only the
480    /// binding.
481    ///
482    /// # Security
483    ///
484    /// `Ok(true)` is binding evidence, not membership. A pair binds whether or not anybody
485    /// signed either half: an acknowledgement can be built against a grant the community
486    /// never issued, and this accepts the two together. Before treating an edge as complete,
487    /// verify the grant's proof against the community's key and the acknowledgement's
488    /// against the member's — each made by a verification method of that credential's
489    /// issuer — and check both windows at the instant you care about.
490    /// `verify_grant_with_public_key`, under the `affinidi-signing` feature, does that for
491    /// the grant in its wire form.
492    pub fn acknowledges(&self, grant: &DTGCredential) -> Result<bool, DTGCredentialError> {
493        if !matches!(self.type_, DTGCredentialType::Membership)
494            || !matches!(grant.type_, DTGCredentialType::Membership)
495        {
496            return Ok(false);
497        }
498
499        // The grant is the half that MUST omit `digest`; a credential carrying one is an
500        // acknowledgement, and an acknowledgement of an acknowledgement is not an edge.
501        if grant.subject_digest().is_some() {
502            return Ok(false);
503        }
504
505        if self.issuer() != grant.subject() || self.subject() != grant.issuer() {
506            return Ok(false);
507        }
508
509        self.verify_digest(grant)
510    }
511
512    /// Does this delegate-issued VDC accept `grant`, completing that delegation edge?
513    ///
514    /// A delegation edge is complete only when both VDCs exist and are valid: the
515    /// delegator's grant, and the delegate's acceptance of it. This checks everything that
516    /// binds the two together:
517    ///
518    /// 1. `grant` is a `DelegationCredential` carrying `scope` and no `accepts` — a grant
519    /// 2. `self` is a `DelegationCredential` carrying `accepts` — an acceptance
520    /// 3. the two name the same pair of parties in mirrored roles: this credential's issuer
521    ///    is the grant's subject, and its subject is the grant's issuer
522    /// 4. the `accepts` digest matches the grant
523    ///
524    /// Returns `Ok(false)` where any of those does not hold, rather than distinguishing
525    /// them: a caller deciding whether an edge is complete has one decision to make, and
526    /// every failing case answers it the same way.
527    ///
528    /// # What this does not check
529    ///
530    /// Neither credential's proof, neither validity window, and neither's revocation
531    /// status. Nor does it establish that the *delegator* may perform the act in question
532    /// — that is a separate question, asked of the delegator at the time of the act, which
533    /// a VDC moves but never answers. This covers the binding.
534    ///
535    /// # Security
536    ///
537    /// As with [DTGCredential::acknowledges], `Ok(true)` is binding evidence only. Verify
538    /// both proofs, each against a verification method of its own credential's issuer, and
539    /// both windows, before accepting anybody as acting under the delegation.
540    pub fn accepts(&self, grant: &DTGCredential) -> Result<bool, DTGCredentialError> {
541        if !matches!(self.type_, DTGCredentialType::Delegation)
542            || !matches!(grant.type_, DTGCredentialType::Delegation)
543        {
544            return Ok(false);
545        }
546
547        let (Some(acceptance), Some(appointment)) =
548            (self.credential.delegation(), grant.credential.delegation())
549        else {
550            return Ok(false);
551        };
552
553        // The grant is the half carrying `scope` and no `accepts`; accepting an acceptance
554        // is not an edge.
555        if appointment.accepts.is_some() || appointment.scope.is_none() {
556            return Ok(false);
557        }
558        let Some(carried) = &acceptance.accepts else {
559            return Ok(false);
560        };
561
562        if self.issuer() != grant.subject() || self.subject() != grant.issuer() {
563            return Ok(false);
564        }
565
566        digests_match(carried, &grant.digest_multibase()?)
567    }
568
569    /// Returns the proof value if signed else None
570    pub fn proof_value(&self) -> Option<&str> {
571        if let Some(proof) = &self.credential.proof {
572            proof.proof_value.as_deref()
573        } else {
574            None
575        }
576    }
577
578    /// Checks the invariants this library holds a credential to before putting a proof on
579    /// it.
580    ///
581    /// - The validity window is well formed: `validUntil`, where present, is after
582    ///   `validFrom` ([DTGCredentialError::InvalidValidityWindow]).
583    /// - No open JSON member — `endorsement`, `credentialStatus`, an unmodelled top-level
584    ///   member — takes the document past [`MAX_JSON_DEPTH`]
585    ///   ([DTGCredentialError::JsonTooDeep]). The check does not recurse.
586    ///
587    /// [DTGCredential::sign] calls this first, so this library never signs a credential
588    /// that fails it, and [DTGCredential::verify_proof_with_public_key] calls it before
589    /// examining a proof. The `new_*` constructors that return a plain `Self` have no way to
590    /// refuse, so a credential built by one of them is checked here rather than there. If
591    /// you sign with another backend, call this yourself before you do.
592    ///
593    /// A `validFrom` in the past is accepted. Backdating is legitimate — re-issuing a
594    /// credential with the date the original took effect is the usual case — so only the
595    /// ordering of the two ends is checked, never either end against the clock.
596    pub fn validate(&self) -> Result<(), DTGCredentialError> {
597        crate::create::check_window(self.valid_from(), self.valid_until())?;
598        self.credential.check_depth()
599    }
600
601    #[cfg(feature = "affinidi-signing")]
602    /// Sign the credential using W3C Data Integrity Proof with JCS EdDSA 2022
603    /// signing_secret: The secret key to use to sign the credential
604    /// create_time: Optional creation time for the proof, defaults to now if None
605    ///
606    /// # Errors
607    ///
608    /// Anything [DTGCredential::validate] refuses, before any signing is attempted.
609    pub async fn sign(
610        &mut self,
611        signing_secret: &Secret,
612        create_time: Option<DateTime<Utc>>,
613    ) -> Result<DataIntegrityProof, DTGCredentialError> {
614        self.validate()?;
615
616        let mut options = SignOptions::new();
617        if let Some(ts) = create_time {
618            options = options.with_created(ts);
619        }
620
621        let proof = DataIntegrityProof::sign(self, signing_secret, options).await?;
622
623        self.credential.proof = Some(proof.clone());
624        Ok(proof)
625    }
626
627    #[cfg(feature = "affinidi-signing")]
628    /// Verify the credential if you already know the public key bytes
629    /// otherwise use the affinidi_tdk:verify_data() method
630    /// public_key_bytes: The public key bytes to use to verify the credential
631    ///
632    /// # Errors
633    ///
634    /// Anything [DTGCredential::validate] refuses, before the proof is examined: a
635    /// credential this library would not have signed does not verify either.
636    pub fn verify_proof_with_public_key(
637        &self,
638        public_key_bytes: &[u8],
639    ) -> Result<(), DTGCredentialError> {
640        self.validate()?;
641
642        let proof = if let Some(proof) = &self.credential.proof {
643            proof.clone()
644        } else {
645            use tracing::warn;
646
647            warn!("Trying to verify a DTG Credential that has no proof");
648            return Err(DTGCredentialError::NotSigned);
649        };
650
651        let unsigned = DTGCommon {
652            proof: None,
653            ..self.credential.clone()
654        };
655
656        proof.verify_with_public_key(&unsigned, public_key_bytes, VerifyOptions::new())?;
657        Ok(())
658    }
659
660    /// Is this credential a W3C VC Version 1.1 or 2.0 credential?
661    pub fn get_w3c_vc_version(&self) -> W3CVCVersion {
662        self.version
663    }
664
665    /// returns true if this credential a personhood credential (PHC)
666    pub fn is_personhood_credential(&self) -> bool {
667        if let DTGCredentialType::Membership = self.type_ {
668            self.credential
669                .type_
670                .contains(&"PersonhoodCredential".to_string())
671        } else {
672            false
673        }
674    }
675}
676
677/// The `sha2-256` multihash code, per the [multicodec] table.
678///
679/// [multicodec]: https://www.w3.org/TR/cid-1.0/#multihash
680const MULTIHASH_SHA2_256: u64 = 0x12;
681
682/// The deepest JSON document this library will digest, sign or verify.
683///
684/// Depth counts from the top of the credential: the document itself is depth 1, and each
685/// value inside an object or array is one deeper than its container. A VEC's `endorsement`
686/// therefore sits at depth 3, and a top-level member such as `credentialStatus` at depth 2.
687///
688/// # Why there is a bound
689///
690/// Digesting, signing and verifying clone, serialize and canonicalize a credential, and each
691/// of those recurses once per level of nesting. A value nested a few thousand levels deep
692/// exhausts the stack, and a stack overflow aborts the process — it is not an error a caller
693/// can handle. The members this library holds as open JSON are where such a value gets in:
694/// a VEC's `endorsement`, `credentialStatus`, and the unmodelled members in
695/// [`DTGCommon::extra`].
696///
697/// # Why this value
698///
699/// `serde_json` already refuses to parse JSON nested 128 levels deep, so a credential that
700/// arrived over the wire is bounded before it gets here. 64 stays well under that — nothing
701/// this library signs is too deep for a stock verifier to parse back — and is still far more
702/// than any credential in the specification needs.
703///
704/// # What it cannot do
705///
706/// A `serde_json::Value` is dropped recursively as well. A caller already holding a value
707/// deep enough to overflow the stack will overflow it when that value goes out of scope,
708/// whatever this library returns. The parser is the real boundary: `serde_json` applies its
709/// limit by default, so leave it on.
710pub const MAX_JSON_DEPTH: usize = 64;
711
712/// Is any value reachable from `roots` deeper than [`MAX_JSON_DEPTH`]?
713///
714/// Each root is paired with the depth it sits at in the enclosing document. The walk keeps
715/// an explicit stack rather than recursing: its job is to refuse a value too deep to process
716/// safely, so it must not be what exhausts the call stack.
717fn exceeds_max_depth<'a>(roots: impl IntoIterator<Item = (&'a Value, usize)>) -> bool {
718    let mut pending: Vec<(&Value, usize)> = roots.into_iter().collect();
719    while let Some((value, depth)) = pending.pop() {
720        if depth > MAX_JSON_DEPTH {
721            return true;
722        }
723        match value {
724            Value::Array(items) => pending.extend(items.iter().map(|item| (item, depth + 1))),
725            Value::Object(members) => {
726                pending.extend(members.values().map(|member| (member, depth + 1)))
727            }
728            _ => {}
729        }
730    }
731    false
732}
733
734/// Refuses a JSON document nested more deeply than [`MAX_JSON_DEPTH`].
735pub(crate) fn check_json_depth(doc: &Value) -> Result<(), DTGCredentialError> {
736    if exceeds_max_depth([(doc, 1)]) {
737        Err(DTGCredentialError::JsonTooDeep {
738            max: MAX_JSON_DEPTH,
739        })
740    } else {
741        Ok(())
742    }
743}
744
745/// Strips a credential's top-level `proof` member, if it has one.
746fn proofless(doc: &Value) -> Value {
747    match doc {
748        Value::Object(members) => {
749            let mut members = members.clone();
750            members.remove("proof");
751            Value::Object(members)
752        }
753        // Not an object: canonicalize as-is. A shape check belongs to the caller, which
754        // has a better error to give than this would.
755        other => other.clone(),
756    }
757}
758
759/// The digest a DTG credential carries of another credential, computed over that
760/// credential in its **wire form**.
761///
762/// This is the encoding DTG Core Credentials calls `digestMultibase`, and every
763/// cross-credential reference in the specification uses it: the member-issued VMC's
764/// `digestMultibase` of the grant it acknowledges, the VWC's of the edge credential it
765/// attests, an attenuated VAC's `authority.parent`, and a VDC's `delegation.parent` and
766/// `delegation.accepts`.
767///
768/// Four steps, per [CID v1.0](https://www.w3.org/TR/cid-1.0/):
769///
770/// 1. canonicalize `doc` with its top-level `proof` member removed, using JCS (RFC 8785);
771/// 2. SHA-256 the resulting UTF-8 bytes;
772/// 3. prefix the `sha2-256` multihash header (`0x12`) and the length (`0x20`);
773/// 4. encode base58btc with the multibase `z` prefix.
774///
775/// # Digest what you received, not what you parsed
776///
777/// Take the document as it arrived. [`DTGCommon::extra`] preserves unmodelled *top-level*
778/// members through a round trip, but the subject types do not model every member a
779/// `credentialSubject` may carry, so a parse-then-re-serialise of an unusual credential
780/// can still differ from the bytes its issuer hashed. Where you hold those bytes, hash
781/// them.
782///
783/// # Why `proof` is excluded
784///
785/// The digest binds to what the credential says, not to a signature over it, so a
786/// reference survives its referent being re-signed. A re-issued credential carries
787/// different claims and therefore a different digest, which is what makes renewal force
788/// re-acknowledgement.
789///
790/// # Errors
791///
792/// [DTGCredentialError::JsonTooDeep] if `doc` is nested more deeply than
793/// [`MAX_JSON_DEPTH`], checked before anything clones or canonicalizes it.
794pub fn digest_multibase_json(doc: &Value) -> Result<String, DTGCredentialError> {
795    check_json_depth(doc)?;
796
797    let canonical = serde_json_canonicalizer::to_vec(&proofless(doc))
798        .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
799
800    let digest = Sha256::digest(&canonical);
801
802    // multihash prefix: 0x12 = sha2-256, 0x20 = 32 byte digest length. Both are varints,
803    // and both are single-byte at these values.
804    let mut multihash = Vec::with_capacity(2 + digest.len());
805    multihash.push(MULTIHASH_SHA2_256 as u8);
806    multihash.push(digest.len() as u8);
807    multihash.extend_from_slice(&digest);
808
809    Ok(multibase::encode(Base::Base58Btc, &multihash))
810}
811
812/// The *task digest* of a Trust Task document, the value a credential carries as
813/// `taskDigestMultibase` alongside the `taskContext` that names the document.
814///
815/// Trust Tasks §4.9.3 *Binding a Citation to the Document It Names* defines it as
816///
817/// ```text
818/// taskDigest = multibase( multihash( H( JCS( document ∖ proof ) ) ) )
819/// ```
820///
821/// where `document ∖ proof` removes the **top-level** `proof` only — a `proof` inside
822/// `payload`, in an embedded presentation or credential, is content and stays. That is
823/// the computation DTG Core Credentials §Digest Encoding already fixes for every other
824/// digest-valued member, with a Trust Task document as the input instead of a credential,
825/// so this is [`digest_multibase_json`] under the name of the question it answers: `H` is
826/// SHA-256 and the encoding base58btc, the single form an issuer of a DTG credential emits.
827///
828/// # Not the digest of the document as it arrived
829///
830/// Trust Tasks names two digests over a document, and they differ only in `proof`. The
831/// task digest asks *what the document says*, so a signed and an unsigned copy have one
832/// value. A *step digest* asks *which serialization arrived* and includes the `proof` —
833/// the document identity `idConflict` is keyed on, and what a `witness/session/submit`
834/// response's `vwcDigestMultibase` is taken over. A function computing one of these must
835/// never stand in for the other; whichever it picks, it is wrong for the other question.
836///
837/// # Errors
838///
839/// [DTGCredentialError::JsonTooDeep] if `document` is nested more deeply than
840/// [`MAX_JSON_DEPTH`].
841pub fn task_digest_multibase_json(document: &Value) -> Result<String, DTGCredentialError> {
842    digest_multibase_json(document)
843}
844
845/// Verifies a grant **in its wire form** before it is answered: that its issuer signed it,
846/// and that it is in force at `at`.
847///
848/// Call this on the JSON a community or delegator sent, before passing that JSON to
849/// [DTGCredential::new_member_vmc_for] or [DTGCredential::new_delegate_vdc_for]. Those
850/// constructors bind an answer to a grant; they do not establish that anybody signed it.
851///
852/// Checks, in order:
853///
854/// 1. the document is within [`MAX_JSON_DEPTH`] and carries a `proof`, else
855///    [DTGCredentialError::JsonTooDeep] or [DTGCredentialError::NotSigned];
856/// 2. the proof verifies under `public_key` over the document with its top-level `proof`
857///    removed, else [DTGCredentialError::DataIntegrity];
858/// 3. the proof's `verificationMethod` belongs to the grant's `issuer` — the DID before its
859///    `#` fragment is exactly the issuer — else [DTGCredentialError::ProofNotFromIssuer];
860/// 4. the validity window is well formed and contains `at`, else
861///    [DTGCredentialError::InvalidValidityWindow] or [DTGCredentialError::NotValidAt].
862///
863/// A document with no `issuer` or `validFrom`, or with a timestamp or a single `proof` that
864/// cannot be read, is [DTGCredentialError::MalformedCredential].
865///
866/// # Where `public_key` comes from
867///
868/// Resolve it from the issuer's DID document, for the verification method the proof names,
869/// and confirm that method is authorized for assertion. Step 3 ties the proof to the issuer
870/// only as far as the key does: a key taken from the grant itself, or from whoever sent it,
871/// establishes nothing about the issuer.
872///
873/// # What this does not check
874///
875/// Revocation — a `credentialStatus` entry is not resolved — and the grant's shape beyond
876/// the members read above. The constructors check the shape.
877#[cfg(feature = "affinidi-signing")]
878pub fn verify_grant_with_public_key(
879    grant: &Value,
880    public_key: &[u8],
881    at: DateTime<Utc>,
882) -> Result<(), DTGCredentialError> {
883    check_json_depth(grant)?;
884
885    let object = grant
886        .as_object()
887        .ok_or_else(|| DTGCredentialError::MalformedCredential("not a JSON object".into()))?;
888    let Some(proof) = object.get("proof") else {
889        return Err(DTGCredentialError::NotSigned);
890    };
891    let proof: DataIntegrityProof = serde_json::from_value(proof.clone())
892        .map_err(|e| DTGCredentialError::MalformedCredential(format!("unreadable `proof`: {e}")))?;
893
894    proof.verify_with_public_key(&proofless(grant), public_key, VerifyOptions::new())?;
895
896    let issuer = create::issuer_of(object)
897        .ok_or_else(|| DTGCredentialError::MalformedCredential("no `issuer`".into()))?;
898    let method_did = proof
899        .verification_method
900        .split_once('#')
901        .map_or(proof.verification_method.as_str(), |(did, _)| did);
902    if method_did != issuer {
903        return Err(DTGCredentialError::ProofNotFromIssuer {
904            issuer,
905            verification_method: proof.verification_method,
906        });
907    }
908
909    let valid_from = create::read_timestamp(object, "validFrom", "issuanceDate")
910        .map_err(DTGCredentialError::MalformedCredential)?
911        .ok_or_else(|| DTGCredentialError::MalformedCredential("no `validFrom`".into()))?;
912    let valid_until = create::read_timestamp(object, "validUntil", "expirationDate")
913        .map_err(DTGCredentialError::MalformedCredential)?;
914    create::check_window(valid_from, valid_until)?;
915    if valid_from > at || valid_until.is_some_and(|until| until < at) {
916        return Err(DTGCredentialError::NotValidAt { at });
917    }
918
919    Ok(())
920}
921
922/// Decodes a `digestMultibase` value into the algorithm it names and the raw digest bytes.
923///
924/// The specification requires verifiers to compare digests this way rather than as
925/// strings, so that two encodings of the same digest are recognised as equal and an
926/// algorithm the verifier does not accept is *rejected* rather than reported as a
927/// mismatch.
928///
929/// # Errors
930///
931/// [DTGCredentialError::InvalidDigest] if the multibase or multihash envelope is
932/// malformed, or if the declared length does not match the bytes present.
933/// [DTGCredentialError::UnsupportedDigestAlgorithm] if the multihash names anything other
934/// than `sha2-256`.
935pub fn decode_digest_multibase(digest: &str) -> Result<(u64, Vec<u8>), DTGCredentialError> {
936    let (_, bytes) = multibase::decode(digest)
937        .map_err(|e| DTGCredentialError::InvalidDigest(format!("multibase: {e}")))?;
938
939    // Both the code and the length are varints. Every algorithm this library accepts has a
940    // single-byte code and a single-byte length, so a two-byte header is all that is read;
941    // a continuation bit in either is an algorithm we would reject anyway.
942    let (&code, rest) = bytes
943        .split_first()
944        .ok_or_else(|| DTGCredentialError::InvalidDigest("empty multihash".into()))?;
945    if code & 0x80 != 0 {
946        return Err(DTGCredentialError::InvalidDigest(
947            "multi-byte multihash code, which names no algorithm this library accepts".into(),
948        ));
949    }
950    let (&length, raw) = rest
951        .split_first()
952        .ok_or_else(|| DTGCredentialError::InvalidDigest("multihash has no length".into()))?;
953
954    if code as u64 != MULTIHASH_SHA2_256 {
955        return Err(DTGCredentialError::UnsupportedDigestAlgorithm(code as u64));
956    }
957    if length as usize != raw.len() {
958        return Err(DTGCredentialError::InvalidDigest(format!(
959            "multihash declares {length} bytes but carries {}",
960            raw.len()
961        )));
962    }
963
964    Ok((code as u64, raw.to_vec()))
965}
966
967/// Do two `digestMultibase` values refer to the same credential?
968///
969/// Decodes both and compares the algorithm and the raw digest bytes, as
970/// [`decode_digest_multibase`] describes. Never compares the encoded strings.
971pub fn digests_match(left: &str, right: &str) -> Result<bool, DTGCredentialError> {
972    Ok(decode_digest_multibase(left)? == decode_digest_multibase(right)?)
973}
974
975/// A credential's digest in the superseded `sha256:<hex>` encoding.
976#[deprecated(
977    since = "0.7.0",
978    note = "Working Draft 02 replaced the `sha256:<hex>` digest with a base58btc multibase \
979            multihash under the property name `digestMultibase`. Use \
980            digest_multibase_json. This function will be removed in a future release."
981)]
982pub fn digest_json(doc: &Value) -> Result<String, DTGCredentialError> {
983    check_json_depth(doc)?;
984
985    let canonical = serde_json_canonicalizer::to_vec(&proofless(doc))
986        .map_err(|e| DTGCredentialError::Canonicalization(e.to_string()))?;
987
988    const HEX: &[u8; 16] = b"0123456789abcdef";
989    let mut out = String::with_capacity("sha256:".len() + 64);
990    out.push_str("sha256:");
991    for byte in Sha256::digest(&canonical) {
992        out.push(HEX[(byte >> 4) as usize] as char);
993        out.push(HEX[(byte & 0x0f) as usize] as char);
994    }
995    Ok(out)
996}
997
998/// TDG VC Type Identifiers
999///
1000/// `PartialEq` is derived so that a consumer can assert by equality
1001/// (`assert_eq!(cred.credential_type(), &DTGCredentialType::Delegation)`) rather than by
1002/// pattern (`matches!`), which reports the actual variant on failure.
1003#[derive(Debug, Clone, PartialEq, Eq)]
1004#[non_exhaustive]
1005pub enum DTGCredentialType {
1006    Membership,
1007    Relationship,
1008    Invitation,
1009    Persona,
1010    Endorsement,
1011    Witness,
1012
1013    /// Verifiable Authority Credential (VAC) — confers authority on a party to perform
1014    /// specified actions within a named scope governed by the issuer.
1015    ///
1016    /// Merged into DTG Core Credentials at Working Draft 02
1017    /// (`trustoverip/dtgwg-cred-spec` PR #29). Key control at invocation — a VAC is not a
1018    /// bearer credential — is implemented in [crate::authority::verify_chain], ahead of
1019    /// PR #41 which states it normatively and removes the `audience` property it made
1020    /// redundant. Two further changes are in flight and not implemented here: revocation
1021    /// (PR #39) and a `maxAttenuation` ceiling (PR #40).
1022    Authority,
1023
1024    /// Verifiable Delegation Credential (VDC) — establishes that one entity may act in
1025    /// another's name.
1026    ///
1027    /// Merged into DTG Core Credentials at Working Draft 02
1028    /// (`trustoverip/dtgwg-cred-spec` PR #19).
1029    Delegation,
1030
1031    /// R-Card is no longer a DTG credential type.
1032    #[deprecated(
1033        since = "0.2.0",
1034        note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1035                It was removed from the DTG Core Credentials specification in Working Draft 01 \
1036                and will be defined by the planned DTG Verifiable Data Structures specification. \
1037                This variant will be removed in a future release."
1038    )]
1039    RCard,
1040}
1041
1042impl Display for DTGCredentialType {
1043    #[allow(deprecated)]
1044    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1045        match self {
1046            DTGCredentialType::Membership => write!(f, "MembershipCredential"),
1047            DTGCredentialType::Relationship => write!(f, "RelationshipCredential"),
1048            DTGCredentialType::Invitation => write!(f, "InvitationCredential"),
1049            DTGCredentialType::Persona => write!(f, "PersonaCredential"),
1050            DTGCredentialType::Endorsement => write!(f, "EndorsementCredential"),
1051            DTGCredentialType::Witness => write!(f, "WitnessCredential"),
1052            DTGCredentialType::Authority => write!(f, "AuthorityCredential"),
1053            DTGCredentialType::Delegation => write!(f, "DelegationCredential"),
1054            DTGCredentialType::RCard => write!(f, "RCardCredential"),
1055        }
1056    }
1057}
1058
1059/// This helps with matching the right credential type to the [DTGCredentialType]
1060const DTG_TYPES: [&str; 9] = [
1061    "MembershipCredential",
1062    "RelationshipCredential",
1063    "InvitationCredential",
1064    "PersonaCredential",
1065    "EndorsementCredential",
1066    "WitnessCredential",
1067    "AuthorityCredential",
1068    "DelegationCredential",
1069    "RCardCredential",
1070];
1071
1072impl TryFrom<&[String]> for DTGCredentialType {
1073    type Error = DTGCredentialError;
1074
1075    #[allow(deprecated)]
1076    fn try_from(types: &[String]) -> Result<Self, Self::Error> {
1077        if let Some(type_) = DTG_TYPES.iter().find(|t| types.contains(&t.to_string())) {
1078            match *type_ {
1079                "MembershipCredential" => Ok(DTGCredentialType::Membership),
1080                "RelationshipCredential" => Ok(DTGCredentialType::Relationship),
1081                "InvitationCredential" => Ok(DTGCredentialType::Invitation),
1082                "PersonaCredential" => Ok(DTGCredentialType::Persona),
1083                "EndorsementCredential" => Ok(DTGCredentialType::Endorsement),
1084                "WitnessCredential" => Ok(DTGCredentialType::Witness),
1085                "AuthorityCredential" => Ok(DTGCredentialType::Authority),
1086                "DelegationCredential" => Ok(DTGCredentialType::Delegation),
1087                "RCardCredential" => Ok(DTGCredentialType::RCard),
1088                _ => Err(DTGCredentialError::UnknownCredential),
1089            }
1090        } else {
1091            Err(DTGCredentialError::UnknownCredential)
1092        }
1093    }
1094}
1095
1096/// All DTG Credentials follow a common structure.
1097#[derive(Serialize, Deserialize, Debug, Clone)]
1098#[serde(rename_all = "camelCase")]
1099pub struct DTGCommon {
1100    /// JSON-LD links to contexts
1101    /// Must contain at least:
1102    /// - <https://www.w3.org/ns/credentials/v2>
1103    /// - <https://firstperson.network/credentials/dtg/v1>
1104    #[serde(rename = "@context")]
1105    pub context: Vec<String>,
1106
1107    /// Credential type identifiers
1108    /// Must contain at least:
1109    /// DTGCredential
1110    /// VerifiableCredential
1111    #[serde(rename = "type")]
1112    pub type_: Vec<String>,
1113
1114    /// OPTIONAL identifier for this specific credential, per the W3C VC Data Model.
1115    ///
1116    /// When present it MUST be a single URL. A `urn:uuid:` URN is the usual choice for a
1117    /// credential with no dereferenceable home.
1118    ///
1119    /// This is the handle a holder or verifier stores the credential *under*, so it is what
1120    /// makes re-delivery of the same credential idempotent and re-issuance of a different one
1121    /// recognisable as a renewal rather than a duplicate. A counterparty that keys credentials
1122    /// by `id` cannot accept one that has none — so issue with an `id` unless you know nobody
1123    /// on the other side needs it.
1124    ///
1125    /// # Set it before signing
1126    ///
1127    /// A Data Integrity proof covers the credential minus its `proof`, which includes this
1128    /// property. Set it while building — [DTGCredential::with_id] — never after
1129    /// [DTGCredential::sign], which would leave a document whose proof no longer verifies.
1130    #[serde(skip_serializing_if = "Option::is_none", default)]
1131    pub id: Option<String>,
1132
1133    /// DID of the entity issuing this credential
1134    pub issuer: String,
1135
1136    /// ISO 8601 format of when this credentials become valid from
1137    #[serde(serialize_with = "iso8601_format", alias = "issuanceDate")]
1138    pub valid_from: DateTime<Utc>,
1139
1140    /// ISO 8601 format of when these credentials are valid to
1141    #[serde(serialize_with = "iso8601_format_option")]
1142    #[serde(
1143        skip_serializing_if = "Option::is_none",
1144        alias = "expirationDate",
1145        default
1146    )]
1147    pub valid_until: Option<DateTime<Utc>>,
1148
1149    /// Names the trust task exchange this credential cites: the `id` of the document that
1150    /// initiated the innermost exchange attesting what the credential states (Trust Tasks
1151    /// §4.9.1). For `witness/session` that document's `threadId` is its own `id`, so the
1152    /// earlier description of this member as the exchange's `threadId` gives the same
1153    /// value there; it does not in general, since a `threadId` need not be unique.
1154    ///
1155    /// Carry [`DTGCommon::task_digest_multibase`] with it, which binds the credential to
1156    /// the document this only names.
1157    ///
1158    /// REQUIRED for [DTGCredentialType::Witness] credentials, OPTIONAL for all other DTG
1159    /// credential types. A DTG credential without a `taskContext` MUST be interpretable
1160    /// standing alone, independent of any exchange.
1161    ///
1162    /// NOTE: A verifier MUST NOT interpret a `taskContext`-bearing credential as proof that
1163    /// the associated trust task completed unless the matching trust task outcome evidence is
1164    /// also present and verified.
1165    #[serde(skip_serializing_if = "Option::is_none", default)]
1166    pub task_context: Option<String>,
1167
1168    /// The *task digest* of the Trust Task document [`DTGCommon::task_context`] names.
1169    ///
1170    /// `taskContext` locates the exchange a credential cites; this binds the credential to
1171    /// it. An `id` is only a name, and anyone can write a different document that reuses
1172    /// it, so a verifier pairing a credential with the cited document by `id` alone accepts
1173    /// a counterfeit.
1174    ///
1175    /// Computed as Trust Tasks §4.9.3 *Binding a Citation to the Document It Names* defines
1176    /// a task digest: the document with its **top-level** `proof` removed (a `proof` inside
1177    /// `payload` stays), canonicalized with JCS (RFC 8785), hashed, multihash-tagged and
1178    /// multibase-encoded. An issuer uses `sha2-256` and base58btc, as for every other
1179    /// digest-valued member of DTG Core Credentials. [`task_digest_multibase_json`] computes
1180    /// it; [DTGCredential::with_task_citation] sets it together with `taskContext`.
1181    ///
1182    /// REQUIRED on a VWC issued through `witness/session` + `witness/session/submit` (the
1183    /// latter's Conformance, item 1), and proposed as REQUIRED wherever `taskContext` is
1184    /// REQUIRED in DTG Core Credentials (trustoverip/dtgwg-cred-spec#56). It is `Option`
1185    /// here, and a VWC without one still deserializes, because every VWC issued before the
1186    /// member existed lacks it. [DTGCredential::cites_task] reports such a credential as
1187    /// citing nothing rather than falling back to comparing `id`s.
1188    #[serde(skip_serializing_if = "Option::is_none", default)]
1189    pub task_digest_multibase: Option<String>,
1190
1191    /// The assertion between the entities involved
1192    pub credential_subject: CredentialSubject,
1193
1194    /// A W3C VC status mechanism through which a verifier determines whether this
1195    /// credential has been revoked.
1196    ///
1197    /// Held as an opaque [`Value`]: the mechanism is chosen by the governing VTC or VTN,
1198    /// and this library neither selects one nor resolves it. `BitstringStatusListEntry` is
1199    /// the common choice.
1200    ///
1201    /// CONDITIONAL on a VDC — REQUIRED where the appointment outlives the freshness window
1202    /// the governing party defines for delegations, and permitted to be absent otherwise,
1203    /// with short validity and re-issuance preferred wherever the delegator is reachable.
1204    /// A status check is a live lookup that reveals the verification event to whoever
1205    /// hosts the status list.
1206    ///
1207    /// # Modelled so that digests survive a round trip
1208    ///
1209    /// Every VMC issued against a status list carries this, and before it was modelled a
1210    /// parse-then-re-serialise dropped it silently — producing a digest its issuer would
1211    /// not recognise. See [`DTGCommon::extra`], which closes the same gap for members this
1212    /// library does not name at all.
1213    #[serde(skip_serializing_if = "Option::is_none", default)]
1214    pub credential_status: Option<Value>,
1215
1216    /// Cryptographic proof of credential authenticity
1217    #[serde(skip_serializing_if = "Option::is_none", default)]
1218    pub proof: Option<DataIntegrityProof>,
1219
1220    /// Top-level members this library does not model, preserved verbatim.
1221    ///
1222    /// A DTG credential may legitimately carry properties beyond the ones named here —
1223    /// `credentialSchema`, `termsOfUse`, `evidence`, an extension a governing party
1224    /// defines. Without somewhere to keep them, a parse-then-re-serialise round trip drops
1225    /// them, and the digest computed over the result matches nothing the issuer signed.
1226    ///
1227    /// Capturing them makes [DTGCredential::digest_multibase] agree with
1228    /// [`digest_multibase_json`] over the wire form for any credential whose extra members
1229    /// are top-level. It is not a complete answer — the `credentialSubject` types still
1230    /// reject members they do not model — so where you hold the bytes a counterparty sent,
1231    /// hashing those remains the safe habit.
1232    #[serde(flatten)]
1233    pub extra: serde_json::Map<String, Value>,
1234}
1235
1236impl DTGCommon {
1237    /// Has this credential been signed?
1238    /// Returns true if a proof exists
1239    /// NOTE: This does NOT validate the proof itself
1240    pub fn signed(&self) -> bool {
1241        self.proof.is_some()
1242    }
1243
1244    /// This credential's own identifier, if it has one. See [DTGCommon::id].
1245    pub fn id(&self) -> Option<&str> {
1246        self.id.as_deref()
1247    }
1248
1249    /// Returns the issuer DID
1250    pub fn issuer(&self) -> &str {
1251        &self.issuer
1252    }
1253
1254    /// Returns the subject DID
1255    #[allow(deprecated)]
1256    pub fn subject(&self) -> &str {
1257        match &self.credential_subject {
1258            CredentialSubject::Basic(subject) => &subject.id,
1259            CredentialSubject::Endorsement(subject) => &subject.id,
1260            CredentialSubject::Witness(subject) => &subject.id,
1261            CredentialSubject::Membership(subject) => &subject.id,
1262            CredentialSubject::Authority(subject) => &subject.id,
1263            CredentialSubject::Delegation(subject) => &subject.id,
1264            CredentialSubject::RCard(subject) => &subject.id,
1265        }
1266    }
1267
1268    /// The `authority` grant, when this credential is a VAC.
1269    ///
1270    /// `None` for every other credential type — the accessor is deliberately fallible
1271    /// rather than panicking, so a caller handed a credential of unknown type can ask
1272    /// without first matching on `type_`.
1273    pub fn authority(&self) -> Option<&AuthorityGrant> {
1274        match &self.credential_subject {
1275            CredentialSubject::Authority(subject) => Some(&subject.authority),
1276            _ => None,
1277        }
1278    }
1279
1280    /// Mutable access to the `authority` grant, when this credential is a VAC.
1281    ///
1282    /// Present so that a caller can construct chains this library's own
1283    /// [DTGCredential::attenuate] would refuse — which is exactly what a verifier must be
1284    /// tested against, since nothing stops another implementation emitting such JSON.
1285    pub fn authority_mut(&mut self) -> Option<&mut AuthorityGrant> {
1286        match &mut self.credential_subject {
1287            CredentialSubject::Authority(subject) => Some(&mut subject.authority),
1288            _ => None,
1289        }
1290    }
1291
1292    /// The `delegation` object, when this credential is a VDC.
1293    ///
1294    /// `None` for every other credential type, for the same reason [DTGCommon::authority]
1295    /// is fallible: a caller handed a credential of unknown type can ask without first
1296    /// matching on `type_`.
1297    pub fn delegation(&self) -> Option<&DelegationGrant> {
1298        match &self.credential_subject {
1299            CredentialSubject::Delegation(subject) => Some(&subject.delegation),
1300            _ => None,
1301        }
1302    }
1303
1304    /// Mutable access to the `delegation` object, when this credential is a VDC.
1305    ///
1306    /// Present for the same reason as [DTGCommon::authority_mut]: a verifier must be
1307    /// testable against chains this library's own constructors would refuse to build,
1308    /// since nothing stops another implementation emitting such JSON.
1309    pub fn delegation_mut(&mut self) -> Option<&mut DelegationGrant> {
1310        match &mut self.credential_subject {
1311            CredentialSubject::Delegation(subject) => Some(&mut subject.delegation),
1312            _ => None,
1313        }
1314    }
1315
1316    /// The credential is valid from this timestamp
1317    pub fn valid_from(&self) -> DateTime<Utc> {
1318        self.valid_from
1319    }
1320
1321    /// The credential is valid until this timestamp, if set
1322    pub fn valid_until(&self) -> Option<DateTime<Utc>> {
1323        self.valid_until
1324    }
1325
1326    /// The `id` naming the trust task exchange this credential cites, if set. See
1327    /// [DTGCommon::task_context].
1328    pub fn task_context(&self) -> Option<&str> {
1329        self.task_context.as_deref()
1330    }
1331
1332    /// The task digest of the document `taskContext` names, if set. See
1333    /// [DTGCommon::task_digest_multibase].
1334    pub fn task_digest_multibase(&self) -> Option<&str> {
1335        self.task_digest_multibase.as_deref()
1336    }
1337
1338    /// Refuses a credential whose open JSON members take the document past
1339    /// [`MAX_JSON_DEPTH`].
1340    ///
1341    /// Every other member is a type this library defines, none more than four levels deep,
1342    /// so the open members are the only place the bound can be crossed.
1343    #[allow(deprecated)]
1344    fn check_depth(&self) -> Result<(), DTGCredentialError> {
1345        // The document is depth 1, so a top-level member sits at 2 and a member of
1346        // `credentialSubject` at 3.
1347        let mut roots: Vec<(&Value, usize)> =
1348            self.extra.values().map(|member| (member, 2)).collect();
1349        if let Some(status) = &self.credential_status {
1350            roots.push((status, 2));
1351        }
1352        match &self.credential_subject {
1353            CredentialSubject::Endorsement(subject) => roots.push((&subject.endorsement, 3)),
1354            CredentialSubject::RCard(subject) => roots.push((&subject.card, 3)),
1355            _ => {}
1356        }
1357
1358        if exceeds_max_depth(roots) {
1359            Err(DTGCredentialError::JsonTooDeep {
1360                max: MAX_JSON_DEPTH,
1361            })
1362        } else {
1363            Ok(())
1364        }
1365    }
1366}
1367
1368/// Helps ensure default starting point is correct
1369impl Default for DTGCommon {
1370    fn default() -> Self {
1371        DTGCommon {
1372            context: vec![
1373                "https://www.w3.org/ns/credentials/v2".to_string(),
1374                "https://firstperson.network/credentials/dtg/v1".to_string(),
1375            ],
1376            type_: vec![
1377                "VerifiableCredential".to_string(),
1378                "DTGCredential".to_string(),
1379            ],
1380            id: None,
1381            issuer: String::new(),
1382            valid_from: Utc::now(),
1383            valid_until: None,
1384            task_context: None,
1385            task_digest_multibase: None,
1386            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic {
1387                id: String::new(),
1388            }),
1389            credential_status: None,
1390            proof: None,
1391            extra: serde_json::Map::new(),
1392        }
1393    }
1394}
1395
1396/// Post deserialize setup of a CredentialSubject and CredntialType
1397impl TryFrom<DTGCommon> for DTGCredential {
1398    type Error = DTGCredentialError;
1399
1400    #[allow(deprecated)]
1401    fn try_from(value: DTGCommon) -> Result<Self, Self::Error> {
1402        match &value.type_.as_slice().try_into()? {
1403            DTGCredentialType::Membership => {
1404                // Normalize whichever variant the untagged subject match landed on into
1405                // `Membership`, so a caller matching on the subject of a VMC sees one shape
1406                // rather than two. See [CredentialSubject::Membership] for why the untagged
1407                // match cannot make this decision itself.
1408                let subject = match &value.credential_subject {
1409                    // Already normalized — a credential built by `new_vmc` /
1410                    // `new_member_vmc` rather than deserialized.
1411                    CredentialSubject::Membership(subject) => subject.clone(),
1412
1413                    // `{ id }` — the community-issued grant, which MUST omit `digest`.
1414                    CredentialSubject::Basic(subject) => CredentialSubjectMembership {
1415                        id: subject.id.clone(),
1416                        digest_multibase: None,
1417                    },
1418
1419                    // `{ id, digest }` — the member-issued acknowledgement. Shape-identical
1420                    // to a VWC subject, which wins the untagged match; on a
1421                    // MembershipCredential it is this. A `witnessContext` alongside it is
1422                    // not: that property belongs to a VWC and has no meaning here, so a VMC
1423                    // carrying one is malformed rather than merely surprising.
1424                    CredentialSubject::Witness(subject) if subject.witness_context.is_none() => {
1425                        CredentialSubjectMembership {
1426                            id: subject.id.clone(),
1427                            digest_multibase: subject.digest_multibase.clone(),
1428                        }
1429                    }
1430
1431                    _ => return Err(DTGCredentialError::UnknownCredential),
1432                };
1433
1434                Ok(DTGCredential {
1435                    type_: DTGCredentialType::Membership,
1436                    version: value.context.as_slice().try_into()?,
1437                    credential: DTGCommon {
1438                        credential_subject: CredentialSubject::Membership(subject),
1439                        ..value
1440                    },
1441                })
1442            }
1443            DTGCredentialType::Relationship => Ok(DTGCredential {
1444                type_: DTGCredentialType::Relationship,
1445                version: value.context.as_slice().try_into()?,
1446                credential: value,
1447            }),
1448            DTGCredentialType::Invitation => Ok(DTGCredential {
1449                type_: DTGCredentialType::Invitation,
1450                version: value.context.as_slice().try_into()?,
1451                credential: value,
1452            }),
1453            DTGCredentialType::Persona => Ok(DTGCredential {
1454                type_: DTGCredentialType::Persona,
1455                version: value.context.as_slice().try_into()?,
1456                credential: value,
1457            }),
1458            DTGCredentialType::Endorsement => {
1459                if let CredentialSubject::Endorsement { .. } = &value.credential_subject {
1460                    Ok(DTGCredential {
1461                        type_: DTGCredentialType::Endorsement,
1462                        version: value.context.as_slice().try_into()?,
1463                        credential: value,
1464                    })
1465                } else {
1466                    Err(DTGCredentialError::UnknownCredential)
1467                }
1468            }
1469            DTGCredentialType::Witness => {
1470                // taskContext is REQUIRED on a VWC: the meaning of a witness attestation
1471                // depends on the conditions it was made under, which live in the trust task
1472                // exchange it is bound to.
1473                if value.task_context.is_none() {
1474                    return Err(DTGCredentialError::MissingTaskContext);
1475                }
1476
1477                match &value.credential_subject {
1478                    CredentialSubject::Witness(_) => Ok(DTGCredential {
1479                        type_: DTGCredentialType::Witness,
1480                        version: value.context.as_slice().try_into()?,
1481                        credential: value,
1482                    }),
1483                    CredentialSubject::Basic(subject) => {
1484                        // If Witness CredentialSubject only contains id, it is still valid
1485                        Ok(DTGCredential {
1486                            type_: DTGCredentialType::Witness,
1487                            version: value.context.as_slice().try_into()?,
1488                            credential: DTGCommon {
1489                                credential_subject: CredentialSubject::Witness(
1490                                    CredentialSubjectWitness {
1491                                        id: subject.id.clone(),
1492                                        digest_multibase: None,
1493                                        witness_context: None,
1494                                    },
1495                                ),
1496                                ..value
1497                            },
1498                        })
1499                    }
1500                    _ => Err(DTGCredentialError::UnknownCredential),
1501                }
1502            }
1503            DTGCredentialType::Authority => {
1504                // A VAC's subject must actually carry the grant. `Basic` — a bare `{ id }` —
1505                // is the shape a caller lands on when the `authority` member is missing
1506                // entirely, and a credential that confers nothing is malformed rather than
1507                // merely empty. There is no normalization to do here (unlike VMC/VWC, whose
1508                // shapes collide): `authority` is unique to this subject.
1509                match &value.credential_subject {
1510                    CredentialSubject::Authority(subject) => {
1511                        if subject.authority.actions.is_empty() {
1512                            // Emptiness is never a wildcard. Refusing here means a caller
1513                            // cannot construct one by deserialization either.
1514                            return Err(DTGCredentialError::EmptyAuthorityActions);
1515                        }
1516                        Ok(DTGCredential {
1517                            type_: DTGCredentialType::Authority,
1518                            version: value.context.as_slice().try_into()?,
1519                            credential: value,
1520                        })
1521                    }
1522                    _ => Err(DTGCredentialError::UnknownCredential),
1523                }
1524            }
1525            DTGCredentialType::Delegation => {
1526                // A VDC's subject must carry the appointment. `Basic` — a bare `{ id }` —
1527                // is where a caller lands when `delegation` is missing entirely, and a
1528                // credential that appoints nobody to nothing is malformed rather than
1529                // merely empty.
1530                match &value.credential_subject {
1531                    CredentialSubject::Delegation(subject) => {
1532                        let d = &subject.delegation;
1533
1534                        // The two halves are distinguished by `accepts`, and each half has
1535                        // exactly one shape. Refusing the mixtures here means a caller
1536                        // cannot construct one by deserialization either.
1537                        match (&d.accepts, &d.scope) {
1538                            (Some(_), Some(_)) => {
1539                                return Err(DTGCredentialError::MalformedDelegation(
1540                                    "carries both `accepts` and `scope`: an acceptance \
1541                                     consents to the scope of the grant it names rather \
1542                                     than restating it"
1543                                        .into(),
1544                                ));
1545                            }
1546                            (Some(_), None) => {
1547                                if d.parent.is_some() || d.max_depth.is_some() {
1548                                    return Err(DTGCredentialError::MalformedDelegation(
1549                                        "an acceptance carries `accepts` and nothing else".into(),
1550                                    ));
1551                                }
1552                            }
1553                            (None, Some(scope)) => {
1554                                if scope.is_empty() {
1555                                    return Err(DTGCredentialError::MalformedDelegation(
1556                                        "a grant's `scope` MUST contain at least one \
1557                                         entry — emptying it is not how an unbounded \
1558                                         appointment is expressed, because there is no \
1559                                         way to express one"
1560                                            .into(),
1561                                    ));
1562                                }
1563                            }
1564                            (None, None) => {
1565                                return Err(DTGCredentialError::MalformedDelegation(
1566                                    "carries neither `scope` nor `accepts`, so it is \
1567                                     neither a grant nor an acceptance"
1568                                        .into(),
1569                                ));
1570                            }
1571                        }
1572
1573                        Ok(DTGCredential {
1574                            type_: DTGCredentialType::Delegation,
1575                            version: value.context.as_slice().try_into()?,
1576                            credential: value,
1577                        })
1578                    }
1579                    _ => Err(DTGCredentialError::UnknownCredential),
1580                }
1581            }
1582            DTGCredentialType::RCard => match &value.credential_subject {
1583                CredentialSubject::RCard { .. } => Ok(DTGCredential {
1584                    type_: DTGCredentialType::RCard,
1585                    version: value.context.as_slice().try_into()?,
1586                    credential: value,
1587                }),
1588                _ => Err(DTGCredentialError::UnknownCredential),
1589            },
1590        }
1591    }
1592}
1593
1594/// This correctly formats timestamps into the correct iso8601 specification for W3C Verifiable
1595/// Credentials
1596fn iso8601_format<S>(timestamp: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error>
1597where
1598    S: Serializer,
1599{
1600    s.serialize_str(
1601        timestamp
1602            .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
1603            .as_str(),
1604    )
1605}
1606
1607fn iso8601_format_option<S>(timestamp: &Option<DateTime<Utc>>, s: S) -> Result<S::Ok, S::Error>
1608where
1609    S: Serializer,
1610{
1611    if let Some(timestamp) = timestamp {
1612        s.serialize_str(
1613            timestamp
1614                .to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
1615                .as_str(),
1616        )
1617    } else {
1618        s.serialize_none()
1619    }
1620}
1621
1622// ****************************************************************************
1623// Credential Subject types
1624// ****************************************************************************
1625// NOTE: The DTG credential spec overloads the JSON attributes for different credential payloads.
1626// The following enum will map the credential subject schema to correct Struct type
1627
1628/// This represents all possible credential subjects
1629/// The order of the enum is important as it will match on first match
1630#[allow(deprecated)]
1631#[derive(Serialize, Deserialize, Debug, Clone)]
1632#[serde(untagged)]
1633pub enum CredentialSubject {
1634    /// Verifiable Endorsement Credential subject
1635    Endorsement(CredentialSubjectEndorsement),
1636
1637    /// R-Card Credential subject
1638    #[deprecated(
1639        since = "0.2.0",
1640        note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1641                See DTGCredentialType::RCard. This variant will be removed in a future release."
1642    )]
1643    RCard(CredentialSubjectRCard),
1644
1645    /// Credential Subject of just `id`
1646    /// Used by a community-issued VMC, and by VRC, VIC and VPC
1647    Basic(CredentialSubjectBasic),
1648
1649    /// Verifiable Witness Credential subject
1650    Witness(CredentialSubjectWitness),
1651
1652    /// Verifiable Authority Credential subject.
1653    ///
1654    /// Unambiguous under the untagged match: no other DTG subject carries an `authority`
1655    /// member, and `deny_unknown_fields` keeps a subject that does not have one from
1656    /// landing here.
1657    Authority(CredentialSubjectAuthority),
1658
1659    /// Verifiable Delegation Credential subject.
1660    ///
1661    /// Unambiguous for the same reason as [CredentialSubject::Authority]: `delegation` is
1662    /// carried by no other DTG subject.
1663    Delegation(CredentialSubjectDelegation),
1664
1665    /// Membership Credential subject, carrying the OPTIONAL `digest` that a member-issued
1666    /// VMC MUST set.
1667    ///
1668    /// # Never selected by the untagged match, deliberately
1669    ///
1670    /// This variant sits last because its two shapes are already claimed above: `{ id }` is
1671    /// [CredentialSubject::Basic], and `{ id, digest }` is indistinguishable from a VWC
1672    /// subject with no `witnessContext`, which [CredentialSubject::Witness] takes first.
1673    /// Nothing in the subject object itself separates a membership acknowledgement from a
1674    /// witness attestation — only the credential's `type` does.
1675    ///
1676    /// So the shape is not decided here. `TryFrom<DTGCommon> for DTGCredential` normalizes
1677    /// whichever variant the untagged match landed on into this one when `type` includes
1678    /// `MembershipCredential`, the same way it already re-wraps a `Basic` subject as
1679    /// `Witness` on a VWC. Deserialization is therefore deterministic rather than
1680    /// order-dependent, and a `Membership` subject reaching a matcher has been through that
1681    /// normalization.
1682    Membership(CredentialSubjectMembership),
1683}
1684
1685/// id of the credential subject only
1686#[derive(Serialize, Deserialize, Debug, Clone)]
1687#[serde(deny_unknown_fields)]
1688pub struct CredentialSubjectBasic {
1689    pub id: String,
1690}
1691
1692/// The `authority` object a [CredentialSubject::Authority] carries.
1693///
1694/// # Attenuation
1695///
1696/// A holder may derive a narrower VAC from one they hold without involving the issuer. An
1697/// attenuated VAC sets [AuthorityGrant::parent] to the **digest** of the credential it
1698/// derives from, and MUST NOT widen `actions`, `scope`, or the validity window. Verification walks
1699/// the chain to a VAC issued by the party governing the scope — see
1700/// [crate::authority::verify_chain], which is where the security of this credential
1701/// actually lives. Issuing one is a struct and a signature; refusing a widening link is the
1702/// part that matters.
1703#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1704#[serde(rename_all = "camelCase", deny_unknown_fields)]
1705pub struct AuthorityGrant {
1706    /// The DID or URI the authority applies to.
1707    ///
1708    /// Matched exactly. A verifier rejects a VAC whose `scope` is not the resource being
1709    /// accessed; nothing here implies containment between scopes.
1710    pub scope: String,
1711
1712    /// The permitted actions, from a vocabulary the governing party defines.
1713    ///
1714    /// MUST NOT be empty. An empty list confers nothing — emptiness is never a wildcard,
1715    /// which is the failure mode this rule exists to prevent. Action strings are compared
1716    /// exactly and case-sensitively, and no action implies another: `admin` does not grant
1717    /// `write` unless both are listed.
1718    pub actions: Vec<String>,
1719
1720    /// The **digest** of the VAC this one was attenuated from, as
1721    /// [DTGCredential::digest_multibase] computes it.
1722    ///
1723    /// Absent means this VAC was issued directly by the party governing the scope, and is
1724    /// therefore a chain root.
1725    ///
1726    /// # A digest, not an identifier
1727    ///
1728    /// Working Draft 02 made this deliberate rather than incidental. A digest names
1729    /// nothing that can be fetched, so verification cannot come to depend on network
1730    /// availability, a verifier cannot be induced to make a request against an address of
1731    /// the holder's choosing, and nobody hosting an identifier learns when a credential is
1732    /// used. It also binds an attenuated VAC to the exact claims its issuer narrowed from:
1733    /// re-issuing a parent with different claims does not re-parent the children of the
1734    /// old one, while re-proofing it with identical claims leaves them undisturbed,
1735    /// because the digest excludes `proof`.
1736    #[serde(skip_serializing_if = "Option::is_none")]
1737    pub parent: Option<String>,
1738}
1739
1740/// The `delegation` object a [CredentialSubject::Delegation] carries.
1741///
1742/// A VDC is one of a **pair**. The delegator issues a *grant* — carrying `scope`, and
1743/// optionally `parent` and `maxDepth` — and the delegate answers with an *acceptance*
1744/// carrying `accepts` and nothing else. The two together form a complete DTG edge, and a
1745/// verifier MUST have both: a grant alone establishes what the delegator appointed, not
1746/// what the delegate agreed to.
1747///
1748/// # A VDC is not authority
1749///
1750/// It never supplies permission the delegator did not itself hold. A verifier presented
1751/// with one substitutes the delegator for the delegate and then asks the permission
1752/// question it would have asked of the delegator directly — live, at the time of the act.
1753/// The reach of a delegated act is the *intersection* of what the delegator may do and
1754/// what the chain appoints the delegate for. See [AuthorityGrant] for the credential that
1755/// answers the permission question.
1756#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
1757#[serde(rename_all = "camelCase", deny_unknown_fields)]
1758pub struct DelegationGrant {
1759    /// The acts the delegate may perform in the delegator's name.
1760    ///
1761    /// REQUIRED on a grant and MUST contain at least one entry — a VDC MUST NOT express an
1762    /// unbounded appointment by omitting or emptying it. MUST be omitted on an acceptance,
1763    /// which consents to the scope of the grant it names rather than restating it.
1764    ///
1765    /// Entries are opaque strings compared for exact equality. The specification defines no
1766    /// wildcard, prefix or hierarchical semantics, so the subset test on a chain is set
1767    /// inclusion over exact matches; a governing vocabulary that wants structure must put
1768    /// it in the terms themselves.
1769    #[serde(skip_serializing_if = "Option::is_none", default)]
1770    pub scope: Option<Vec<String>>,
1771
1772    /// The digest of the VDC this delegation was derived from, when the delegator is
1773    /// itself acting under a delegation. A VDC with no `parent` is a **root delegation**.
1774    #[serde(skip_serializing_if = "Option::is_none", default)]
1775    pub parent: Option<String>,
1776
1777    /// The number of further re-delegations permitted below this one.
1778    ///
1779    /// `0` prohibits re-delegation, and so does **absence** — the default is a single hop.
1780    /// Setting it above `0` is the delegator's explicit authorisation to re-delegate;
1781    /// there is no other. Note that this is the opposite default from a VAC, where
1782    /// attenuation is permitted unless forbidden: a delegate speaks in the principal's
1783    /// name, so the principal keeps the register of who may do so.
1784    #[serde(skip_serializing_if = "Option::is_none", default)]
1785    pub max_depth: Option<u32>,
1786
1787    /// The digest of the grant being accepted.
1788    ///
1789    /// REQUIRED on an acceptance and MUST be omitted on a grant. Its presence is what
1790    /// distinguishes the two halves of a delegation edge.
1791    #[serde(skip_serializing_if = "Option::is_none", default)]
1792    pub accepts: Option<String>,
1793}
1794
1795/// Delegation Credential subject
1796#[derive(Serialize, Deserialize, Debug, Clone)]
1797#[serde(rename_all = "camelCase", deny_unknown_fields)]
1798pub struct CredentialSubjectDelegation {
1799    /// DID of the delegate on a grant; DID of the delegator on an acceptance.
1800    pub id: String,
1801
1802    /// The appointment itself.
1803    pub delegation: DelegationGrant,
1804}
1805
1806/// Verifiable Authority Credential (VAC) subject.
1807#[derive(Serialize, Deserialize, Debug, Clone)]
1808#[serde(rename_all = "camelCase", deny_unknown_fields)]
1809pub struct CredentialSubjectAuthority {
1810    /// DID of the party receiving the authority.
1811    pub id: String,
1812
1813    /// What the subject may do, and where.
1814    pub authority: AuthorityGrant,
1815}
1816
1817/// Membership Credential subject
1818///
1819/// The two directions of a membership edge share this shape and are told apart by
1820/// `digest`: a community-issued VMC (the membership grant) MUST omit it, and a
1821/// member-issued VMC (the membership acknowledgement) MUST carry it. Where both endpoints
1822/// are community identifiers, as in VTN membership, `digestMultibase` is the only
1823/// discriminator — the issuer and subject rules cannot separate the directions.
1824#[derive(Serialize, Deserialize, Debug, Clone)]
1825#[serde(rename_all = "camelCase", deny_unknown_fields)]
1826pub struct CredentialSubjectMembership {
1827    pub id: String,
1828
1829    /// Digest of the community-issued VMC this acknowledges, as
1830    /// [DTGCredential::digest_multibase] computes it.
1831    ///
1832    /// REQUIRED on the member-issued VMC, and MUST be omitted on the community-issued VMC.
1833    /// `Option` rather than two structs because the same property distinguishes the two
1834    /// directions: a type that could not represent both could not deserialize the pair.
1835    ///
1836    /// Serializes as `digestMultibase`. The Working Draft 01 name `digest` is accepted on
1837    /// the wire so that credentials issued against that draft still parse; the *value*
1838    /// encoding also changed, so such a credential parses and then fails to compare, with
1839    /// [DTGCredentialError::InvalidDigest] rather than a silent mismatch.
1840    #[serde(
1841        rename = "digestMultibase",
1842        alias = "digest",
1843        skip_serializing_if = "Option::is_none",
1844        default
1845    )]
1846    pub digest_multibase: Option<String>,
1847}
1848
1849/// Endorsement Credential subject
1850#[derive(Serialize, Deserialize, Debug, Clone)]
1851#[serde(deny_unknown_fields)]
1852pub struct CredentialSubjectEndorsement {
1853    pub id: String,
1854    /// There is no spec for the endorsement content, so we use a generic JSON value
1855    pub endorsement: Value,
1856}
1857
1858/// Witness Credential subject
1859#[derive(Serialize, Deserialize, Debug, Clone)]
1860#[serde(rename_all = "camelCase", deny_unknown_fields)]
1861pub struct CredentialSubjectWitness {
1862    pub id: String,
1863
1864    /// Digest of the witnessed edge credential, as [DTGCredential::digest_multibase]
1865    /// computes it. REQUIRED by the specification — a VWC without one names the observed
1866    /// party and the exchange, but not which edge was witnessed.
1867    ///
1868    /// Serializes as `digestMultibase`; the Working Draft 01 name `digest` is accepted on
1869    /// the wire.
1870    #[serde(
1871        rename = "digestMultibase",
1872        alias = "digest",
1873        skip_serializing_if = "Option::is_none",
1874        default
1875    )]
1876    pub digest_multibase: Option<String>,
1877
1878    /// There is no spec for the witness context content, so we use a generic JSON value
1879    #[serde(skip_serializing_if = "Option::is_none")]
1880    pub witness_context: Option<WitnessContext>,
1881}
1882
1883/// Witness Credential Context
1884#[derive(Serialize, Deserialize, Debug, Clone)]
1885#[serde(rename_all = "camelCase", deny_unknown_fields)]
1886pub struct WitnessContext {
1887    /// Human-readable event name
1888    pub event: Option<String>,
1889
1890    /// Session or nonce identifier
1891    pub session_id: Option<String>,
1892
1893    ///Verification method used
1894    pub method: Option<String>,
1895}
1896
1897/// R-Card Credential subject
1898#[deprecated(
1899    since = "0.2.0",
1900    note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1901            See DTGCredentialType::RCard. This struct will be removed in a future release."
1902)]
1903#[derive(Serialize, Deserialize, Debug, Clone)]
1904#[serde(deny_unknown_fields)]
1905pub struct CredentialSubjectRCard {
1906    pub id: String,
1907
1908    /// JCard spec, generic JSON value
1909    pub card: Value,
1910}
1911
1912#[cfg(test)]
1913#[allow(deprecated)]
1914mod tests {
1915    use crate::{
1916        CredentialSubject, CredentialSubjectRCard, DTGCommon, DTGCredential, DTGCredentialError,
1917        DTGCredentialType, W3CVCVersion, decode_digest_multibase, digest_multibase_json,
1918        digests_match,
1919    };
1920    use chrono::{DateTime, Utc};
1921    use multibase::Base;
1922    use serde_json::Value;
1923    use sha2::{Digest, Sha256};
1924
1925    #[test]
1926    fn test_vmc_vc_1_deserialize() {
1927        // tests deserialize a W3C VC Version 1.1 credential
1928        let vmc: DTGCredential = match serde_json::from_str(
1929            r#"{
1930"@context": [
1931    "https://www.w3.org/2018/credentials/v1",
1932    "https://firstperson.network/credentials/dtg/v1",
1933    "https://w3id.org/security/suites/ed25519-2020/v1"
1934  ],
1935  "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1936  "issuer": "did:web:chess-club.example",
1937  "issuanceDate": "2026-01-06T10:00:00Z",
1938  "expirationDate": "2027-01-06T10:00:00Z",
1939  "credentialSubject": {
1940    "id": "did:key:z6MkpTHR8VNs..."
1941  }
1942            }"#,
1943        ) {
1944            Ok(vmc) => vmc,
1945            Err(e) => panic!("Couldn't deserialize VMC: {}", e),
1946        };
1947
1948        assert!(matches!(vmc.type_, DTGCredentialType::Membership));
1949        assert!(matches!(
1950            vmc.credential().credential_subject,
1951            CredentialSubject::Membership(_)
1952        ));
1953        assert!(matches!(vmc.version, W3CVCVersion::V1_1));
1954        assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V1_1));
1955    }
1956
1957    #[test]
1958    fn test_missing_w3c_context() {
1959        // tests deserialize a W3C VC Version 1.1 credential
1960        assert!(
1961            serde_json::from_str::<DTGCredential>(
1962                r#"{
1963"@context": [
1964    "https://firstperson.network/credentials/dtg/v1",
1965    "https://w3id.org/security/suites/ed25519-2020/v1"
1966  ],
1967  "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1968  "issuer": "did:web:chess-club.example",
1969  "issuanceDate": "2026-01-06T10:00:00Z",
1970  "expirationDate": "2027-01-06T10:00:00Z",
1971  "credentialSubject": {
1972    "id": "did:key:z6MkpTHR8VNs..."
1973  }
1974            }"#,
1975            )
1976            .is_err()
1977        );
1978    }
1979
1980    #[test]
1981    fn test_mutable_credential() {
1982        let mut vmc = DTGCredential::new_vmc(
1983            "did:example:issuer".to_string(),
1984            "did:example:subject".to_string(),
1985            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1986                .unwrap()
1987                .with_timezone(&Utc),
1988            None,
1989            false,
1990        );
1991
1992        let cred = vmc.credential_mut();
1993        cred.type_.push("PersonhoodCredential".to_string());
1994        assert!(vmc.is_personhood_credential());
1995    }
1996
1997    #[test]
1998    fn test_vmc_deserialize() {
1999        let vmc: DTGCredential = match serde_json::from_str(
2000            r#"{
2001                "@context": ["https://www.w3.org/ns/credentials/v2"],
2002                "type": ["VerifiableCredential", "DTGCredential",  "MembershipCredential"],
2003                "issuer": "did:example:community",
2004                "validFrom": "2024-06-18T10:00:00Z",
2005                "credentialSubject": { "id": "did:example:rDid" }
2006            }"#,
2007        ) {
2008            Ok(vmc) => vmc,
2009            Err(e) => panic!("Couldn't deserialize VMC: {}", e),
2010        };
2011
2012        assert!(!vmc.is_personhood_credential());
2013        assert!(matches!(vmc.type_, DTGCredentialType::Membership));
2014        assert!(matches!(
2015            vmc.credential().credential_subject,
2016            CredentialSubject::Membership(_)
2017        ));
2018        assert!(matches!(vmc.get_w3c_vc_version(), W3CVCVersion::V2_0));
2019    }
2020
2021    #[test]
2022    fn test_vmc_phc_deserialize() {
2023        let vmc: DTGCredential = match serde_json::from_str(
2024            r#"{
2025                "@context": ["https://www.w3.org/ns/credentials/v2"],
2026                "type": ["VerifiableCredential", "DTGCredential",  "MembershipCredential", "PersonhoodCredential"],
2027                "issuer": "did:example:community",
2028                "validFrom": "2024-06-18T10:00:00Z",
2029                "credentialSubject": { "id": "did:example:rDid" }
2030            }"#,
2031        ) {
2032            Ok(vmc) => vmc,
2033            Err(e) => panic!("Couldn't deserialize VMC: {}", e),
2034        };
2035
2036        assert!(vmc.is_personhood_credential());
2037        assert!(matches!(vmc.type_, DTGCredentialType::Membership));
2038        assert!(matches!(
2039            vmc.credential().credential_subject,
2040            CredentialSubject::Membership(_)
2041        ));
2042    }
2043
2044    #[test]
2045    fn test_vrc_deserialize() {
2046        let vrc: DTGCredential = match serde_json::from_str(
2047            r#"{
2048                "@context": ["https://www.w3.org/ns/credentials/v2"],
2049                "type": ["VerifiableCredential", "DTGCredential",  "RelationshipCredential"],
2050                "issuer": "did:example:governmentAgencyDid",
2051                "validFrom": "2024-06-18T10:00:00Z",
2052                "credentialSubject": { "id": "did:example:citizenRDid" }
2053            }"#,
2054        ) {
2055            Ok(vrc) => vrc,
2056            Err(e) => panic!("Couldn't deserialize VRC: {}", e),
2057        };
2058
2059        assert!(matches!(vrc.type_, DTGCredentialType::Relationship));
2060        assert!(matches!(
2061            vrc.credential().credential_subject,
2062            CredentialSubject::Basic(_)
2063        ));
2064    }
2065
2066    #[test]
2067    fn test_vic_deserialize() {
2068        let vic: DTGCredential = match serde_json::from_str(
2069            r#"{
2070                "@context": ["https://www.w3.org/ns/credentials/v2"],
2071                "type": ["VerifiableCredential", "DTGCredential",  "InvitationCredential"],
2072                "issuer": "did:example:governmentAgencyVicDid",
2073                "validFrom": "2024-06-18T10:00:00Z",
2074                "credentialSubject": { "id": "did:example:citizenRDid" }
2075            }"#,
2076        ) {
2077            Ok(vic) => vic,
2078            Err(e) => panic!("Couldn't deserialize VIC: {}", e),
2079        };
2080
2081        assert!(!vic.is_personhood_credential());
2082        assert!(matches!(vic.type_, DTGCredentialType::Invitation));
2083        assert!(matches!(
2084            vic.credential().credential_subject,
2085            CredentialSubject::Basic(_)
2086        ));
2087    }
2088
2089    #[test]
2090    fn test_vpc_deserialize() {
2091        let vpc: DTGCredential = match serde_json::from_str(
2092            r#"{
2093                "@context": ["https://www.w3.org/ns/credentials/v2"],
2094                "type": ["VerifiableCredential", "DTGCredential",  "PersonaCredential"],
2095                "issuer": "did:example:governmentAgencyDid",
2096                "validFrom": "2024-06-18T10:00:00Z",
2097                "credentialSubject": { "id": "did:example:citizenRDid" }
2098            }"#,
2099        ) {
2100            Ok(vpc) => vpc,
2101            Err(e) => panic!("Couldn't deserialize VPC: {}", e),
2102        };
2103
2104        assert!(matches!(vpc.type_, DTGCredentialType::Persona));
2105        assert!(matches!(
2106            vpc.credential().credential_subject,
2107            CredentialSubject::Basic(_)
2108        ));
2109    }
2110
2111    #[test]
2112    fn test_vec_deserialize() {
2113        let vec: DTGCredential = match serde_json::from_str(
2114            r#"{
2115                "@context": ["https://www.w3.org/ns/credentials/v2"],
2116                "type": ["VerifiableCredential", "DTGCredential",  "EndorsementCredential"],
2117                "issuer": "did:example:governmentAgencyDid",
2118                "validFrom": "2024-06-18T10:00:00Z",
2119                "credentialSubject": { "id": "did:example:citizenRDid", "endorsement": {} }
2120            }"#,
2121        ) {
2122            Ok(vec) => vec,
2123            Err(e) => panic!("Couldn't deserialize VEC: {}", e),
2124        };
2125
2126        assert!(matches!(vec.type_, DTGCredentialType::Endorsement));
2127        assert!(matches!(vec.subject(), "did:example:citizenRDid"));
2128        assert!(matches!(
2129            vec.credential().credential_subject,
2130            CredentialSubject::Endorsement(_)
2131        ));
2132    }
2133
2134    #[test]
2135    fn test_vec_bad_deserialize() {
2136        match serde_json::from_str::<DTGCredential>(
2137            r#"{
2138                "@context": ["https://www.w3.org/ns/credentials/v2"],
2139                "type": ["VerifiableCredential", "DTGCredential",  "EndorsementCredential"],
2140                "issuer": "did:example:governmentAgencyDid",
2141                "validFrom": "2024-06-18T10:00:00Z",
2142                "credentialSubject": { "id": "did:example:citizenRDid", "other": [] }
2143            }"#,
2144        ) {
2145            Ok(_) => panic!("Expected Unknown Credential type"),
2146            Err(_) => {
2147                // Good
2148            }
2149        };
2150    }
2151
2152    #[test]
2153    fn test_vwc_simple_deserialize() {
2154        let vwc: DTGCredential = match serde_json::from_str(
2155            r#"{
2156                "@context": ["https://www.w3.org/ns/credentials/v2"],
2157                "type": ["VerifiableCredential", "DTGCredential",  "WitnessCredential"],
2158                "issuer": "did:example:governmentAgencyDid",
2159                "validFrom": "2024-06-18T10:00:00Z",
2160                "taskContext": "thread-abc-123",
2161                "credentialSubject": { "id": "did:example:citizenRDid" }
2162            }"#,
2163        ) {
2164            Ok(vwc) => vwc,
2165            Err(e) => panic!("Couldn't deserialize VWC: {}", e),
2166        };
2167
2168        assert!(matches!(vwc.type_, DTGCredentialType::Witness));
2169        assert!(matches!(vwc.subject(), "did:example:citizenRDid"));
2170        assert_eq!(vwc.task_context(), Some("thread-abc-123"));
2171        assert!(matches!(
2172            vwc.credential().credential_subject,
2173            CredentialSubject::Witness(_)
2174        ));
2175    }
2176
2177    #[test]
2178    fn test_vwc_full_deserialize() {
2179        let vwc: DTGCredential = match serde_json::from_str(
2180            r#"{
2181                "@context": ["https://www.w3.org/ns/credentials/v2"],
2182                "type": ["VerifiableCredential", "DTGCredential",  "WitnessCredential"],
2183                "issuer": "did:example:governmentAgencyDid",
2184                "validFrom": "2024-06-18T10:00:00Z",
2185                "taskContext": "thread-abc-123",
2186                "credentialSubject": { "id": "did:example:citizenRDid", "digestMultibase": "abcdf", "witnessContext": {} }
2187            }"#,
2188        ) {
2189            Ok(vwc) => vwc,
2190            Err(e) => panic!("Couldn't deserialize VWC: {}", e),
2191        };
2192
2193        assert!(matches!(vwc.type_(), DTGCredentialType::Witness));
2194        assert!(matches!(
2195            vwc.credential().credential_subject,
2196            CredentialSubject::Witness(_)
2197        ));
2198    }
2199
2200    #[test]
2201    fn test_vwc_bad_deserialize() {
2202        if serde_json::from_str::<DTGCredential>(
2203            r#"{
2204                "@context": ["https://www.w3.org/ns/credentials/v2"],
2205                "type": ["VerifiableCredential", "DTGCredential",  "WitnessCredential"],
2206                "issuer": "did:example:governmentAgencyDid",
2207                "validFrom": "2024-06-18T10:00:00Z",
2208                "taskContext": "thread-abc-123",
2209                "credentialSubject": { "id": "did:example:citizenRDid", "digestMultibase": "abcdf", "wrongContext": {}  }
2210            }"#,
2211        ).is_ok() {
2212            panic!("Should have failed due to wrong CredentialSubject!");
2213        }
2214    }
2215
2216    #[test]
2217    fn test_rcard_simple_deserialize() {
2218        let rcard: DTGCredential = match serde_json::from_str(
2219            r#"{
2220                "@context": ["https://www.w3.org/ns/credentials/v2"],
2221                "type": ["VerifiableCredential", "DTGCredential",  "RCardCredential"],
2222                "issuer": "did:example:governmentAgencyDid",
2223                "validFrom": "2024-06-18T10:00:00Z",
2224                "credentialSubject": { "id": "did:example:citizenRDid", "card": [] }
2225            }"#,
2226        ) {
2227            Ok(rcard) => rcard,
2228            Err(e) => panic!("Couldn't deserialize R-Card: {}", e),
2229        };
2230
2231        assert!(matches!(rcard.type_(), DTGCredentialType::RCard));
2232        assert!(matches!(rcard.subject(), "did:example:citizenRDid"));
2233        assert!(matches!(
2234            rcard.credential().credential_subject,
2235            CredentialSubject::RCard(_)
2236        ));
2237    }
2238
2239    #[test]
2240    fn test_rcard_bad_deserialize() {
2241        if serde_json::from_str::<DTGCredential>(
2242            r#"{
2243                "@context": ["https://www.w3.org/ns/credentials/v2"],
2244                "type": ["VerifiableCredential", "DTGCredential",  "RCardCredential"],
2245                "issuer": "did:example:governmentAgencyDid",
2246                "validFrom": "2024-06-18T10:00:00Z",
2247                "credentialSubject": { "id": "did:example:citizenRDid"  }
2248            }"#,
2249        )
2250        .is_ok()
2251        {
2252            panic!("Should have failed due to wrong CredentialSubject!");
2253        }
2254    }
2255    #[test]
2256    fn test_deserialize_unknown() {
2257        match serde_json::from_str::<DTGCredential>(
2258            r#"{
2259                "@context": ["https://www.w3.org/ns/credentials/v2"],
2260                "type": ["VerifiableCredential", "DTGCredential",  "UnknownCredential"],
2261                "issuer": "did:example:governmentAgencyDid",
2262                "validFrom": "2024-06-18T10:00:00Z",
2263                "credentialSubject": { "id": "did:example:citizenRDid" }
2264            }"#,
2265        ) {
2266            Ok(_) => panic!("Expected Unknown Credential type"),
2267            Err(e) => {
2268                if e.to_string() == "Unknown credential type" {
2269                    // test passed
2270                } else {
2271                    panic!("Wrong error type returned");
2272                }
2273            }
2274        };
2275    }
2276
2277    #[test]
2278    fn test_deserialize_mismatched_credential_subject() {
2279        match serde_json::from_str::<DTGCredential>(
2280            r#"{
2281                "@context": ["https://www.w3.org/ns/credentials/v2"],
2282                "type": ["VerifiableCredential", "DTGCredential",  "EndorsementCredential"],
2283                "issuer": "did:example:governmentAgencyDid",
2284                "validFrom": "2024-06-18T10:00:00Z",
2285                "credentialSubject": { "id": "did:example:citizenRDid" }
2286            }"#,
2287        ) {
2288            Ok(_) => panic!("Expected Unknown Credential type"),
2289            Err(e) => {
2290                if e.to_string() == "Unknown credential type" {
2291                    // test passed
2292                } else {
2293                    panic!("Wrong error type returned");
2294                }
2295            }
2296        };
2297    }
2298
2299    #[test]
2300    fn test_proof_signed() {
2301        let cred: DTGCredential = match serde_json::from_str(
2302            r#"{
2303                "@context": ["https://www.w3.org/ns/credentials/v2"],
2304                "type": ["VerifiableCredential", "DTGCredential",  "MembershipCredential"],
2305                "issuer": "did:example:community",
2306                "validFrom": "2024-06-18T10:00:00Z",
2307                "credentialSubject": { "id": "did:example:rDid" },
2308                "proof": {
2309                    "type": "DataIntegrityProof",
2310                    "cryptosuite": "eddsa-jcs-2022",
2311                    "created": "2025-12-04T00:00:00",
2312                    "verificationMethod": "did:example:test#key-1",
2313                    "proofPurpose": "assertionMethod",
2314                    "proofValue": "abcd"
2315                }
2316            }"#,
2317        ) {
2318            Ok(vmc) => vmc,
2319            Err(e) => panic!("Couldn't deserialize credential: {}", e),
2320        };
2321
2322        assert!(cred.signed());
2323        assert!(cred.proof_value().is_some());
2324    }
2325
2326    #[test]
2327    fn test_proof_not_signed() {
2328        let cred: DTGCredential = match serde_json::from_str(
2329            r#"{
2330                "@context": ["https://www.w3.org/ns/credentials/v2"],
2331                "type": ["VerifiableCredential", "DTGCredential",  "MembershipCredential"],
2332                "issuer": "did:example:community",
2333                "validFrom": "2024-06-18T10:00:00Z",
2334                "credentialSubject": { "id": "did:example:rDid" }
2335            }"#,
2336        ) {
2337            Ok(vmc) => vmc,
2338            Err(e) => panic!("Couldn't deserialize credential: {}", e),
2339        };
2340
2341        assert!(!cred.signed());
2342        assert!(cred.proof_value().is_none());
2343    }
2344
2345    #[test]
2346    fn test_helpers() {
2347        let cred: DTGCredential = match serde_json::from_str(
2348            r#"{
2349                "@context": ["https://www.w3.org/ns/credentials/v2"],
2350                "type": ["VerifiableCredential", "DTGCredential",  "MembershipCredential"],
2351                "issuer": "did:example:issuer",
2352                "validFrom": "2024-06-18T00:00:00Z",
2353                "credentialSubject": { "id": "did:example:subject" }
2354            }"#,
2355        ) {
2356            Ok(vmc) => vmc,
2357            Err(e) => panic!("Couldn't deserialize credential: {}", e),
2358        };
2359
2360        assert_eq!(cred.issuer(), "did:example:issuer");
2361        assert_eq!(cred.subject(), "did:example:subject");
2362        assert_eq!(
2363            cred.valid_from()
2364                .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
2365            "2024-06-18T00:00:00Z"
2366        );
2367        assert_eq!(cred.valid_until(), None);
2368    }
2369
2370    #[test]
2371    fn test_valid_until() {
2372        let cred: DTGCredential = match serde_json::from_str(
2373            r#"{
2374                "@context": ["https://www.w3.org/ns/credentials/v2"],
2375                "type": ["VerifiableCredential", "DTGCredential",  "MembershipCredential"],
2376                "issuer": "did:example:issuer",
2377                "validFrom": "2024-06-18T00:00:00Z",
2378                "validUntil": "2030-01-01T00:00:00Z",
2379                "credentialSubject": { "id": "did:example:subject" }
2380            }"#,
2381        ) {
2382            Ok(vmc) => vmc,
2383            Err(e) => panic!("Couldn't deserialize credential: {}", e),
2384        };
2385
2386        assert_eq!(
2387            cred.valid_until()
2388                .unwrap()
2389                .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
2390            "2030-01-01T00:00:00Z"
2391        );
2392    }
2393
2394    #[test]
2395    fn test_bad_type() {
2396        assert!(
2397            std::convert::TryInto::<DTGCredentialType>::try_into(
2398                vec!["bad_type".to_string()].as_slice(),
2399            )
2400            .is_err()
2401        );
2402    }
2403
2404    #[test]
2405    fn test_badly_constructed_vwc() {
2406        let mut cred = DTGCommon::default();
2407        cred.type_.push("WitnessCredential".to_string());
2408        // taskContext is set so this exercises the credentialSubject mismatch, not the
2409        // missing-taskContext path covered by test_vwc_missing_task_context()
2410        cred.task_context = Some("thread-abc-123".to_string());
2411        cred.credential_subject = CredentialSubject::RCard(CredentialSubjectRCard {
2412            id: "did:example:bad".to_string(),
2413            card: Value::Null,
2414        });
2415
2416        assert!(std::convert::TryInto::<DTGCredential>::try_into(cred).is_err());
2417    }
2418
2419    #[test]
2420    fn test_vwc_missing_task_context() {
2421        // taskContext is REQUIRED on a VWC
2422        match serde_json::from_str::<DTGCredential>(
2423            r#"{
2424                "@context": ["https://www.w3.org/ns/credentials/v2"],
2425                "type": ["VerifiableCredential", "DTGCredential",  "WitnessCredential"],
2426                "issuer": "did:example:witness",
2427                "validFrom": "2024-06-18T10:00:00Z",
2428                "credentialSubject": { "id": "did:example:observed" }
2429            }"#,
2430        ) {
2431            Ok(_) => panic!("Expected a VWC without taskContext to be rejected"),
2432            Err(e) => assert_eq!(
2433                e.to_string(),
2434                "WitnessCredential is missing the required taskContext property"
2435            ),
2436        }
2437    }
2438
2439    #[test]
2440    fn test_task_context_round_trip() {
2441        // taskContext must survive deserialize -> serialize, otherwise a credential signed
2442        // elsewhere would fail verification here (and vice versa)
2443        let raw = r#"{
2444                "@context": ["https://www.w3.org/ns/credentials/v2"],
2445                "type": ["VerifiableCredential", "DTGCredential",  "WitnessCredential"],
2446                "issuer": "did:example:witness",
2447                "validFrom": "2024-06-18T10:00:00Z",
2448                "taskContext": "thread-abc-123",
2449                "credentialSubject": { "id": "did:example:observed" }
2450            }"#;
2451
2452        let cred: DTGCredential = serde_json::from_str(raw).unwrap();
2453        let out = serde_json::to_string(&cred).unwrap();
2454
2455        assert!(out.contains(r#""taskContext":"thread-abc-123""#));
2456    }
2457
2458    #[test]
2459    fn test_task_context_optional_on_other_types() {
2460        // taskContext is OPTIONAL everywhere except the VWC
2461        let vrc: DTGCredential = serde_json::from_str(
2462            r#"{
2463                "@context": ["https://www.w3.org/ns/credentials/v2"],
2464                "type": ["VerifiableCredential", "DTGCredential",  "RelationshipCredential"],
2465                "issuer": "did:example:issuer",
2466                "validFrom": "2024-06-18T10:00:00Z",
2467                "credentialSubject": { "id": "did:example:subject" }
2468            }"#,
2469        )
2470        .unwrap();
2471
2472        assert_eq!(vrc.task_context(), None);
2473        // and it is omitted from the serialization entirely when absent
2474        assert!(!serde_json::to_string(&vrc).unwrap().contains("taskContext"));
2475    }
2476
2477    #[test]
2478    fn test_digest_multibase() {
2479        let vrc = DTGCredential::new_vrc(
2480            "did:example:issuer".to_string(),
2481            "did:example:subject".to_string(),
2482            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2483                .unwrap()
2484                .with_timezone(&Utc),
2485            None,
2486        );
2487
2488        let digest = vrc.digest_multibase().unwrap();
2489
2490        // base58btc multibase prefix
2491        assert!(digest.starts_with('z'));
2492
2493        // decodes to a sha2-256 multihash: 0x12 0x20 followed by 32 digest bytes
2494        let (base, bytes) = multibase::decode(&digest).unwrap();
2495        assert_eq!(base, multibase::Base::Base58Btc);
2496        assert_eq!(bytes.len(), 34);
2497        assert_eq!(&bytes[..2], &[0x12, 0x20]);
2498
2499        // stable across calls
2500        assert_eq!(digest, vrc.digest_multibase().unwrap());
2501
2502        // and distinct for a different credential
2503        let other = DTGCredential::new_vrc(
2504            "did:example:issuer".to_string(),
2505            "did:example:someone-else".to_string(),
2506            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2507                .unwrap()
2508                .with_timezone(&Utc),
2509            None,
2510        );
2511        assert_ne!(digest, other.digest_multibase().unwrap());
2512    }
2513
2514    #[test]
2515    fn test_verify_digest() {
2516        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2517            .unwrap()
2518            .with_timezone(&Utc);
2519
2520        let vrc = DTGCredential::new_vrc(
2521            "did:example:issuer".to_string(),
2522            "did:example:subject".to_string(),
2523            valid_from,
2524            None,
2525        );
2526
2527        let vwc = DTGCredential::new_vwc(
2528            "did:example:witness".to_string(),
2529            // the DID of the issuer of the VRC being attested
2530            "did:example:issuer".to_string(),
2531            valid_from,
2532            None,
2533            "thread-abc-123".to_string(),
2534            Some(vrc.digest_multibase().unwrap()),
2535            None,
2536        );
2537
2538        assert!(vwc.verify_digest(&vrc).unwrap());
2539
2540        // a different VRC must not match
2541        let other = DTGCredential::new_vrc(
2542            "did:example:issuer".to_string(),
2543            "did:example:someone-else".to_string(),
2544            valid_from,
2545            None,
2546        );
2547        assert!(!vwc.verify_digest(&other).unwrap());
2548    }
2549
2550    #[test]
2551    fn test_verify_digest_without_digest() {
2552        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2553            .unwrap()
2554            .with_timezone(&Utc);
2555
2556        let vrc = DTGCredential::new_vrc(
2557            "did:example:issuer".to_string(),
2558            "did:example:subject".to_string(),
2559            valid_from,
2560            None,
2561        );
2562
2563        // digest is OPTIONAL - with none present there is nothing to rely on
2564        let vwc = DTGCredential::new_vwc(
2565            "did:example:witness".to_string(),
2566            "did:example:issuer".to_string(),
2567            valid_from,
2568            None,
2569            "thread-abc-123".to_string(),
2570            None,
2571            None,
2572        );
2573
2574        assert!(!vwc.verify_digest(&vrc).unwrap());
2575    }
2576
2577    /// The digest encoding is the interoperability surface: a credential referencing another
2578    /// is compared against a value some other implementation produced. Pinned against a
2579    /// literal rather than a recomputation, because a test that recomputes agrees with
2580    /// whatever the code does and would follow the encoding silently if it drifted.
2581    #[test]
2582    fn test_digest_is_a_base58btc_multihash_over_the_proofless_jcs_form() {
2583        let vmc = DTGCredential::new_vmc(
2584            "did:example:community".to_string(),
2585            "did:example:member".to_string(),
2586            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2587                .unwrap()
2588                .with_timezone(&Utc),
2589            None,
2590            false,
2591        )
2592        .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
2593
2594        let digest = vmc.digest_multibase().unwrap();
2595
2596        // Multibase base58btc.
2597        assert!(digest.starts_with('z'), "multibase base58btc prefix");
2598
2599        // Decodes to a sha2-256 multihash: 0x12 0x20 followed by 32 digest bytes.
2600        let (base, bytes) = multibase::decode(&digest).unwrap();
2601        assert_eq!(base, Base::Base58Btc);
2602        assert_eq!(bytes.len(), 34);
2603        assert_eq!(&bytes[..2], &[0x12, 0x20]);
2604
2605        // Computed outside this crate over the JCS canonical form of the document below,
2606        // then wrapped per CID v1.0 §2.4-2.5:
2607        //   {"@context":[...],"credentialSubject":{"id":"did:example:member"},
2608        //    "id":"urn:uuid:2a4e...","issuer":"did:example:community",
2609        //    "type":[...],"validFrom":"2025-12-11T00:00:00Z"}
2610        // whose SHA-256 is 49c9d5135ab4b5659a343bc79d351e37d64f05add58408cae6eef022828495c2.
2611        assert_eq!(digest, "zQmTJgyPT2ShMQ2AvCHGDoPGjEWyRC7ZNT3MBpe5PP6Vpvu");
2612
2613        // Stable across calls.
2614        assert_eq!(digest, vmc.digest_multibase().unwrap());
2615    }
2616
2617    /// The superseded encoding still produces what it always did, so a caller migrating can
2618    /// recompute a Working Draft 01 digest to compare against one they stored.
2619    #[test]
2620    #[allow(deprecated)]
2621    fn the_superseded_hex_digest_is_unchanged() {
2622        let vmc = DTGCredential::new_vmc(
2623            "did:example:community".to_string(),
2624            "did:example:member".to_string(),
2625            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2626                .unwrap()
2627                .with_timezone(&Utc),
2628            None,
2629            false,
2630        )
2631        .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
2632
2633        assert_eq!(
2634            vmc.digest().unwrap(),
2635            "sha256:49c9d5135ab4b5659a343bc79d351e37d64f05add58408cae6eef022828495c2"
2636        );
2637    }
2638
2639    /// A Working Draft 01 digest reaching a Working Draft 02 verifier is *reported*, not
2640    /// silently treated as a mismatch. The two say different things: one is a credential
2641    /// that disagrees, the other a credential that cannot be read at all.
2642    #[test]
2643    fn a_superseded_digest_value_is_rejected_as_malformed() {
2644        let err = decode_digest_multibase(
2645            "sha256:49c9d5135ab4b5659a343bc79d351e37d64f05add58408cae6eef022828495c2",
2646        )
2647        .unwrap_err();
2648
2649        assert!(
2650            matches!(err, DTGCredentialError::InvalidDigest(_)),
2651            "expected InvalidDigest, got {err:?}"
2652        );
2653    }
2654
2655    /// Digests are compared as decoded bytes, never as strings — the specification requires
2656    /// it, because one digest has more than one spelling.
2657    #[test]
2658    fn digests_are_compared_by_bytes_not_by_string() {
2659        // The same sha2-256 multihash, encoded base58btc and base16. Identical bytes,
2660        // different strings.
2661        let multihash = {
2662            let mut v = vec![0x12u8, 0x20];
2663            v.extend_from_slice(&Sha256::digest(b"an edge credential"));
2664            v
2665        };
2666        let b58 = multibase::encode(Base::Base58Btc, &multihash);
2667        let b16 = multibase::encode(Base::Base16Lower, &multihash);
2668
2669        assert_ne!(b58, b16, "the two spellings differ as strings");
2670        assert!(
2671            digests_match(&b58, &b16).unwrap(),
2672            "but name the same digest"
2673        );
2674    }
2675
2676    /// An algorithm the library does not implement is *rejected*, not reported as a
2677    /// mismatch. A verifier that conflated the two would silently downgrade a governing
2678    /// party's choice of a stronger hash into a failed comparison.
2679    #[test]
2680    fn an_unaccepted_hash_algorithm_is_rejected_rather_than_mismatched() {
2681        // 0x13 is sha2-512 in the multicodec table.
2682        let mut multihash = vec![0x13u8, 0x40];
2683        multihash.extend_from_slice(&[0u8; 64]);
2684        let encoded = multibase::encode(Base::Base58Btc, &multihash);
2685
2686        assert!(matches!(
2687            decode_digest_multibase(&encoded),
2688            Err(DTGCredentialError::UnsupportedDigestAlgorithm(0x13))
2689        ));
2690    }
2691
2692    /// The digest binds to what a credential says, not to a signature over it, so a
2693    /// re-proofed credential still satisfies a reference made against the earlier one. This
2694    /// is what lets a member's acknowledgement survive the community re-signing its grant.
2695    #[cfg(feature = "affinidi-signing")]
2696    #[tokio::test]
2697    async fn test_digest_is_unchanged_by_signing() {
2698        use affinidi_secrets_resolver::secrets::Secret;
2699
2700        let secret = Secret::generate_ed25519(None, None);
2701
2702        let mut vmc = DTGCredential::new_vmc(
2703            "did:example:community".to_string(),
2704            "did:example:member".to_string(),
2705            Utc::now(),
2706            None,
2707            false,
2708        );
2709
2710        let before = vmc.digest_multibase().unwrap();
2711        vmc.sign(&secret, None).await.expect("signs");
2712        assert!(vmc.signed());
2713        assert_eq!(before, vmc.digest_multibase().unwrap());
2714    }
2715
2716    /// A grant in the wire form a member actually receives.
2717    fn wire(c: &DTGCredential) -> Value {
2718        serde_json::to_value(c.credential()).expect("credential serialises")
2719    }
2720
2721    /// The whole point of the pair: a grant and the acknowledgement built from it form a
2722    /// complete membership edge, and the parties are mirrored across the two halves.
2723    #[test]
2724    fn test_member_vmc_acknowledges_its_grant() {
2725        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2726            .unwrap()
2727            .with_timezone(&Utc);
2728
2729        let grant = DTGCredential::new_vmc(
2730            "did:example:community".to_string(),
2731            "did:example:member".to_string(),
2732            valid_from,
2733            None,
2734            false,
2735        );
2736
2737        let ack = DTGCredential::new_member_vmc_for(
2738            &wire(&grant),
2739            "did:example:member",
2740            valid_from,
2741            None,
2742        )
2743        .expect("builds");
2744
2745        // Roles reversed.
2746        assert_eq!(ack.issuer(), "did:example:member");
2747        assert_eq!(ack.subject(), "did:example:community");
2748
2749        // The grant MUST omit the digest; the acknowledgement MUST carry it.
2750        assert_eq!(grant.subject_digest(), None);
2751        assert_eq!(
2752            ack.subject_digest(),
2753            Some(grant.digest_multibase().unwrap().as_str())
2754        );
2755
2756        assert!(ack.acknowledges(&grant).unwrap());
2757    }
2758
2759    /// An acknowledgement completes the edge it names and no other. Each case below verifies
2760    /// as a credential in its own right; what fails is the binding.
2761    #[test]
2762    fn test_acknowledges_rejects_a_mismatched_pair() {
2763        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2764            .unwrap()
2765            .with_timezone(&Utc);
2766
2767        let grant = DTGCredential::new_vmc(
2768            "did:example:community".to_string(),
2769            "did:example:member".to_string(),
2770            valid_from,
2771            None,
2772            false,
2773        );
2774        let ack = DTGCredential::new_member_vmc_for(
2775            &wire(&grant),
2776            "did:example:member",
2777            valid_from,
2778            None,
2779        )
2780        .expect("builds");
2781
2782        // A grant to a different member: right community, wrong edge.
2783        let other_member = DTGCredential::new_vmc(
2784            "did:example:community".to_string(),
2785            "did:example:someone-else".to_string(),
2786            valid_from,
2787            None,
2788            false,
2789        );
2790        assert!(!ack.acknowledges(&other_member).unwrap());
2791
2792        // A grant from a different community.
2793        let other_community = DTGCredential::new_vmc(
2794            "did:example:other-community".to_string(),
2795            "did:example:member".to_string(),
2796            valid_from,
2797            None,
2798            false,
2799        );
2800        assert!(!ack.acknowledges(&other_community).unwrap());
2801
2802        // A re-issued grant to the same member — different claims, so a different digest.
2803        // This is what forces re-acknowledgement on renewal rather than letting a stale
2804        // consent carry over to a membership the member never agreed to.
2805        let renewed = DTGCredential::new_vmc(
2806            "did:example:community".to_string(),
2807            "did:example:member".to_string(),
2808            valid_from + chrono::Duration::days(365),
2809            None,
2810            false,
2811        );
2812        assert!(!ack.acknowledges(&renewed).unwrap());
2813
2814        // The acknowledgement is not itself a grant: acknowledging one forms no edge.
2815        let ack_of_ack = DTGCredential::new_member_vmc_for(
2816            &wire(&grant),
2817            "did:example:member",
2818            valid_from,
2819            None,
2820        )
2821        .expect("builds");
2822        assert!(!ack_of_ack.acknowledges(&ack).unwrap());
2823
2824        // A grant on its own does not complete anything — it carries no digest to check.
2825        assert!(!grant.acknowledges(&grant).unwrap());
2826    }
2827
2828    /// A VDC's `credentialStatus` is CONDITIONAL, not required: a delegation whose validity
2829    /// exceeds the freshness window the governing VTC or VTN defines MUST carry one, and
2830    /// one short enough to be bounded by expiry alone MAY omit it. This library does not
2831    /// know that window, so the entry is attached rather than demanded — and once attached,
2832    /// it must reach the wire.
2833    #[test]
2834    fn a_vdc_carries_the_credential_status_it_is_given() {
2835        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2836            .unwrap()
2837            .with_timezone(&Utc);
2838        let valid_until = DateTime::parse_from_rfc3339("2026-12-11T00:00:00Z")
2839            .unwrap()
2840            .with_timezone(&Utc);
2841
2842        let status = serde_json::json!({
2843            "id": "https://delegator.example/status#12",
2844            "type": "BitstringStatusListEntry",
2845            "statusPurpose": "revocation",
2846            "statusListIndex": "12"
2847        });
2848
2849        let vdc = DTGCredential::new_vdc(
2850            "did:example:delegator".to_string(),
2851            "did:example:delegate".to_string(),
2852            valid_from,
2853            valid_until,
2854            vec!["sign:invoices".to_string()],
2855            None,
2856        )
2857        .expect("a bounded grant is well formed");
2858
2859        // Omitting it is legitimate, so the constructor must not invent one.
2860        assert!(
2861            vdc.credential().credential_status.is_none(),
2862            "a VDC MAY omit `credentialStatus`, so the constructor must not supply one"
2863        );
2864
2865        let vdc = vdc.with_credential_status(status.clone());
2866        assert_eq!(vdc.credential().credential_status.as_ref(), Some(&status));
2867        assert_eq!(wire(&vdc).get("credentialStatus"), Some(&status));
2868
2869        // And it must survive the trip back, or a verifier reading the wire form loses the
2870        // only thing that lets it check revocation.
2871        let parsed: DTGCredential = serde_json::from_value(wire(&vdc)).expect("parses");
2872        assert_eq!(
2873            parsed.credential().credential_status.as_ref(),
2874            Some(&status)
2875        );
2876    }
2877
2878    /// The non-consuming form sets the same field.
2879    #[test]
2880    fn set_credential_status_matches_the_builder() {
2881        let status = serde_json::json!({ "type": "BitstringStatusListEntry" });
2882
2883        let mut vmc = DTGCredential::new_vmc(
2884            "did:example:community".to_string(),
2885            "did:example:member".to_string(),
2886            Utc::now(),
2887            None,
2888            false,
2889        );
2890        vmc.set_credential_status(status.clone());
2891
2892        assert_eq!(vmc.credential().credential_status.as_ref(), Some(&status));
2893    }
2894
2895    /// `DTGCredentialType` derives `PartialEq` so a consumer can assert by equality rather
2896    /// than by pattern, and get the actual variant reported on failure.
2897    #[test]
2898    fn credential_types_compare_by_equality() {
2899        let vdc = DTGCredential::new_vdc(
2900            "did:example:delegator".to_string(),
2901            "did:example:delegate".to_string(),
2902            Utc::now(),
2903            Utc::now() + chrono::Duration::days(1),
2904            vec!["sign:invoices".to_string()],
2905            None,
2906        )
2907        .expect("a bounded grant is well formed");
2908
2909        assert_eq!(vdc.type_(), DTGCredentialType::Delegation);
2910        assert_ne!(vdc.type_(), DTGCredentialType::Membership);
2911    }
2912
2913    /// `credentialStatus` used to be dropped by a parse-then-re-serialise round trip, which
2914    /// silently changed a credential's digest. [`DTGCommon::credential_status`] models it,
2915    /// and this pins that it survives.
2916    #[test]
2917    fn credential_status_survives_a_round_trip() {
2918        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2919            .unwrap()
2920            .with_timezone(&Utc);
2921
2922        let mut grant = wire(&DTGCredential::new_vmc(
2923            "did:example:community".to_string(),
2924            "did:example:member".to_string(),
2925            valid_from,
2926            None,
2927            false,
2928        ));
2929        let status = serde_json::json!({
2930            "id": "https://community.example/status#7",
2931            "type": "BitstringStatusListEntry",
2932            "statusPurpose": "revocation",
2933            "statusListIndex": "7"
2934        });
2935        grant["credentialStatus"] = status.clone();
2936
2937        let parsed: DTGCredential = serde_json::from_value(grant.clone()).expect("parses");
2938        assert_eq!(
2939            parsed.credential().credential_status.as_ref(),
2940            Some(&status)
2941        );
2942        assert_eq!(wire(&parsed).get("credentialStatus"), Some(&status));
2943        assert_eq!(
2944            parsed.digest_multibase().unwrap(),
2945            digest_multibase_json(&grant).unwrap(),
2946            "the digest must not change under a round trip that preserves every member"
2947        );
2948    }
2949
2950    /// Top-level members this library does not model at all are preserved too, by
2951    /// [`DTGCommon::extra`]. `credentialSchema` stands in for the open set of them.
2952    #[test]
2953    fn unmodelled_top_level_members_survive_a_round_trip() {
2954        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
2955            .unwrap()
2956            .with_timezone(&Utc);
2957
2958        let mut grant = wire(&DTGCredential::new_vmc(
2959            "did:example:community".to_string(),
2960            "did:example:member".to_string(),
2961            valid_from,
2962            None,
2963            false,
2964        ));
2965        let schema = serde_json::json!({
2966            "id": "https://community.example/schemas/vmc",
2967            "type": "JsonSchema"
2968        });
2969        grant["credentialSchema"] = schema.clone();
2970
2971        let parsed: DTGCredential = serde_json::from_value(grant.clone()).expect("parses");
2972        assert_eq!(
2973            parsed.credential().extra.get("credentialSchema"),
2974            Some(&schema)
2975        );
2976        assert_eq!(
2977            parsed.digest_multibase().unwrap(),
2978            digest_multibase_json(&grant).unwrap()
2979        );
2980    }
2981
2982    /// # Why the wire form is still what gets digested
2983    ///
2984    /// [`DTGCommon::extra`] closed the dropped-member hazard, but not the whole of it. A
2985    /// timestamp is *normalized* on the way out — `2025-12-11T00:00:00.000+00:00` and
2986    /// `2025-12-11T00:00:00Z` are the same instant and parse to the same
2987    /// [`chrono::DateTime`], and this library re-serializes both as the latter. The
2988    /// document that comes back out is therefore equivalent to the one that went in, and
2989    /// hashes differently.
2990    ///
2991    /// An acknowledgement built by digesting the *parsed* grant would carry a digest over a
2992    /// document the community never issued, and the community would rightly refuse it.
2993    /// Silently: both credentials verify, and only the digest comparison fails, with
2994    /// nothing to say why.
2995    ///
2996    /// So `new_member_vmc` takes the wire form, and this pins that it digests what it was
2997    /// handed rather than what it could parse.
2998    #[test]
2999    fn the_acknowledgement_digests_the_grant_as_it_arrived() {
3000        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
3001            .unwrap()
3002            .with_timezone(&Utc);
3003
3004        let mut grant = wire(&DTGCredential::new_vmc(
3005            "did:example:community".to_string(),
3006            "did:example:member".to_string(),
3007            valid_from,
3008            None,
3009            false,
3010        ));
3011        // The same instant, spelled the way another implementation might.
3012        grant["validFrom"] = Value::String("2025-12-11T00:00:00.000+00:00".to_string());
3013
3014        // The parse normalizes it — this is the hazard, asserted rather than assumed.
3015        let parsed: DTGCredential = serde_json::from_value(grant.clone()).expect("parses");
3016        assert_ne!(
3017            wire(&parsed).get("validFrom"),
3018            grant.get("validFrom"),
3019            "the model is expected to normalize the timestamp; if it now round-trips \
3020             verbatim, this test has stopped guarding anything"
3021        );
3022
3023        let ack = DTGCredential::new_member_vmc_for(&grant, "did:example:member", valid_from, None)
3024            .expect("builds");
3025
3026        assert_eq!(
3027            ack.subject_digest(),
3028            Some(digest_multibase_json(&grant).unwrap().as_str()),
3029            "the acknowledgement must digest the grant as received"
3030        );
3031        assert_ne!(
3032            ack.subject_digest(),
3033            Some(parsed.digest_multibase().unwrap().as_str()),
3034            "digesting the parsed model would produce a digest the community cannot match"
3035        );
3036    }
3037
3038    #[test]
3039    fn digest_multibase_json_agrees_with_digest_where_the_model_is_complete() {
3040        let vmc = DTGCredential::new_vmc(
3041            "did:example:community".to_string(),
3042            "did:example:member".to_string(),
3043            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
3044                .unwrap()
3045                .with_timezone(&Utc),
3046            None,
3047            false,
3048        )
3049        .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
3050
3051        assert_eq!(
3052            vmc.digest_multibase().unwrap(),
3053            digest_multibase_json(&wire(&vmc)).unwrap()
3054        );
3055    }
3056
3057    /// `acknowledges` answers only about VMC pairs. A VRC edge is completed by its own
3058    /// reciprocal, not by this.
3059    #[test]
3060    fn test_acknowledges_is_membership_only() {
3061        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
3062            .unwrap()
3063            .with_timezone(&Utc);
3064
3065        let grant = DTGCredential::new_vmc(
3066            "did:example:community".to_string(),
3067            "did:example:member".to_string(),
3068            valid_from,
3069            None,
3070            false,
3071        );
3072        let ack = DTGCredential::new_member_vmc_for(
3073            &wire(&grant),
3074            "did:example:member",
3075            valid_from,
3076            None,
3077        )
3078        .expect("builds");
3079
3080        let vrc = DTGCredential::new_vrc(
3081            "did:example:member".to_string(),
3082            "did:example:community".to_string(),
3083            valid_from,
3084            None,
3085        );
3086        assert!(!ack.acknowledges(&vrc).unwrap());
3087
3088        // And a VWC bound to the grant is a witness attestation, not a member's consent.
3089        let vwc = DTGCredential::new_vwc(
3090            "did:example:witness".to_string(),
3091            "did:example:community".to_string(),
3092            valid_from,
3093            None,
3094            "thread-abc-123".to_string(),
3095            Some(grant.digest_multibase().unwrap()),
3096            None,
3097        );
3098        assert!(vwc.verify_digest(&grant).unwrap(), "the digest does match");
3099        assert!(
3100            !vwc.acknowledges(&grant).unwrap(),
3101            "but a VWC is not the member's acknowledgement"
3102        );
3103    }
3104
3105    /// A grant built against something that cannot be one is refused at construction, where
3106    /// the caller can still do something about it — rather than producing an acknowledgement
3107    /// that verifies as a credential and completes no edge.
3108    #[test]
3109    fn test_new_member_vmc_refuses_a_non_grant() {
3110        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
3111            .unwrap()
3112            .with_timezone(&Utc);
3113
3114        let vrc = DTGCredential::new_vrc(
3115            "did:example:a".to_string(),
3116            "did:example:b".to_string(),
3117            valid_from,
3118            None,
3119        );
3120        assert!(matches!(
3121            DTGCredential::new_member_vmc_for(&wire(&vrc), "did:example:b", valid_from, None),
3122            Err(DTGCredentialError::NotAMembershipGrant(_))
3123        ));
3124
3125        let grant = DTGCredential::new_vmc(
3126            "did:example:community".to_string(),
3127            "did:example:member".to_string(),
3128            valid_from,
3129            None,
3130            false,
3131        );
3132        let ack = DTGCredential::new_member_vmc_for(
3133            &wire(&grant),
3134            "did:example:member",
3135            valid_from,
3136            None,
3137        )
3138        .expect("builds");
3139        assert!(matches!(
3140            DTGCredential::new_member_vmc_for(
3141                &wire(&ack),
3142                "did:example:community",
3143                valid_from,
3144                None
3145            ),
3146            Err(DTGCredentialError::NotAMembershipGrant(_))
3147        ));
3148    }
3149
3150    /// `{ id, digest }` is shape-identical to a VWC subject, and the untagged enum matches
3151    /// `Witness` first. On a MembershipCredential the credential's `type` is the only thing
3152    /// that says otherwise, so the normalization in `TryFrom<DTGCommon>` is what makes this
3153    /// deserialize as the member-issued half rather than as a witness attestation.
3154    #[test]
3155    fn test_member_issued_vmc_deserializes_as_membership_not_witness() {
3156        let vmc: DTGCredential = serde_json::from_str(
3157            r#"{
3158                "@context": ["https://www.w3.org/ns/credentials/v2"],
3159                "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
3160                "issuer": "did:example:member",
3161                "validFrom": "2024-06-18T10:00:00Z",
3162                "credentialSubject": {
3163                    "id": "did:example:community",
3164                    "digestMultibase": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
3165                }
3166            }"#,
3167        )
3168        .expect("deserializes");
3169
3170        assert!(matches!(vmc.type_, DTGCredentialType::Membership));
3171        assert!(matches!(
3172            vmc.credential().credential_subject,
3173            CredentialSubject::Membership(_)
3174        ));
3175        assert_eq!(
3176            vmc.subject_digest(),
3177            Some("sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
3178        );
3179        assert_eq!(vmc.subject(), "did:example:community");
3180    }
3181
3182    /// `witnessContext` belongs to a VWC. A VMC carrying one is malformed rather than
3183    /// merely surprising, and is refused instead of being silently read as a grant.
3184    #[test]
3185    fn test_membership_credential_rejects_a_witness_context() {
3186        let result: Result<DTGCredential, _> = serde_json::from_str(
3187            r#"{
3188                "@context": ["https://www.w3.org/ns/credentials/v2"],
3189                "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
3190                "issuer": "did:example:member",
3191                "validFrom": "2024-06-18T10:00:00Z",
3192                "credentialSubject": {
3193                    "id": "did:example:community",
3194                    "digestMultibase": "sha256:e3b0c4",
3195                    "witnessContext": { "event": "not a membership property" }
3196                }
3197            }"#,
3198        );
3199        assert!(result.is_err());
3200    }
3201
3202    /// The two halves must be distinguishable on the wire by `digestMultibase` alone — that is the
3203    /// only discriminator where both endpoints are C-DIDs, as in VTN membership.
3204    #[test]
3205    fn test_the_two_halves_round_trip_over_the_wire() {
3206        let valid_from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
3207            .unwrap()
3208            .with_timezone(&Utc);
3209
3210        let grant = DTGCredential::new_vmc(
3211            "did:example:community".to_string(),
3212            "did:example:member".to_string(),
3213            valid_from,
3214            None,
3215            false,
3216        );
3217        let ack = DTGCredential::new_member_vmc_for(
3218            &wire(&grant),
3219            "did:example:member",
3220            valid_from,
3221            None,
3222        )
3223        .expect("builds");
3224
3225        let grant_json = serde_json::to_value(&grant).unwrap();
3226        assert!(
3227            grant_json["credentialSubject"]
3228                .get("digestMultibase")
3229                .is_none(),
3230            "the grant MUST omit `digestMultibase`: {grant_json}"
3231        );
3232
3233        let ack_json = serde_json::to_value(&ack).unwrap();
3234        assert_eq!(
3235            ack_json["credentialSubject"]["digestMultibase"],
3236            Value::String(grant.digest_multibase().unwrap()),
3237        );
3238
3239        // And the pair still binds after a round trip through JSON, which is how each side
3240        // actually receives the other's half.
3241        let grant: DTGCredential = serde_json::from_value(grant_json).expect("grant round trips");
3242        let ack: DTGCredential = serde_json::from_value(ack_json).expect("ack round trips");
3243        assert!(ack.acknowledges(&grant).unwrap());
3244    }
3245
3246    #[test]
3247    fn test_iso8601_format_option() {
3248        let now: DateTime<Utc> = DateTime::parse_from_rfc3339(
3249            &Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
3250        )
3251        .unwrap()
3252        .to_utc();
3253        let cred = DTGCommon {
3254            valid_until: Some(now),
3255            ..Default::default()
3256        };
3257
3258        let value = serde_json::to_value(&cred).unwrap();
3259        let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
3260        assert_eq!(cred2.valid_until, Some(now));
3261
3262        let cred = DTGCommon::default();
3263        let value = serde_json::to_value(&cred).unwrap();
3264        let cred2: DTGCommon = serde_json::from_value(value.clone()).unwrap();
3265        assert_eq!(cred2.valid_until, None);
3266    }
3267
3268    #[cfg(feature = "affinidi-signing")]
3269    #[tokio::test]
3270    async fn test_signing() {
3271        use affinidi_secrets_resolver::secrets::Secret;
3272
3273        let secret = Secret::generate_ed25519(None, None);
3274
3275        let mut cred = DTGCredential::new_vrc(
3276            "did:example:issuer".to_string(),
3277            "did:example:subject".to_string(),
3278            Utc::now(),
3279            None,
3280        );
3281
3282        assert!(cred.sign(&secret, Some(Utc::now())).await.is_ok());
3283
3284        assert!(
3285            cred.verify_proof_with_public_key(secret.get_public_bytes())
3286                .is_ok()
3287        );
3288
3289        let secret2 = Secret::generate_ed25519(None, None);
3290        assert!(
3291            cred.verify_proof_with_public_key(secret2.get_public_bytes())
3292                .is_err()
3293        );
3294    }
3295
3296    /// The proof covers `id`, so it must be set *before* signing.
3297    ///
3298    /// This is the property that makes [DTGCredential::with_id]'s "set it before signing"
3299    /// caveat load-bearing rather than advisory: a credential signed without an identifier
3300    /// cannot be given one afterwards to satisfy a verifier that requires it, because the
3301    /// document that was signed did not contain it. Tampering with `id` after the fact is
3302    /// the same operation, and must fail the same way.
3303    #[cfg(feature = "affinidi-signing")]
3304    #[tokio::test]
3305    async fn test_id_is_covered_by_the_proof() {
3306        use affinidi_secrets_resolver::secrets::Secret;
3307
3308        let secret = Secret::generate_ed25519(None, None);
3309
3310        let mut cred = DTGCredential::new_vrc(
3311            "did:example:issuer".to_string(),
3312            "did:example:subject".to_string(),
3313            Utc::now(),
3314            None,
3315        )
3316        .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
3317
3318        cred.sign(&secret, Some(Utc::now()))
3319            .await
3320            .expect("signing a credential that carries an id");
3321        assert!(
3322            cred.verify_proof_with_public_key(secret.get_public_bytes())
3323                .is_ok(),
3324            "an id set before signing verifies"
3325        );
3326
3327        // Changing the id after signing — which is what "splice an id into the JSON on the
3328        // way out" amounts to — invalidates the proof.
3329        cred.set_id("urn:uuid:00000000-0000-0000-0000-000000000000");
3330        assert!(
3331            cred.verify_proof_with_public_key(secret.get_public_bytes())
3332                .is_err(),
3333            "an id changed after signing must break the proof"
3334        );
3335    }
3336
3337    #[cfg(feature = "affinidi-signing")]
3338    #[tokio::test]
3339    async fn test_signing_error() {
3340        use affinidi_secrets_resolver::secrets::Secret;
3341
3342        let secret = Secret::generate_x25519(None, None).unwrap();
3343
3344        let mut cred = DTGCredential::new_vrc(
3345            "did:example:issuer".to_string(),
3346            "did:example:subject".to_string(),
3347            Utc::now(),
3348            None,
3349        );
3350
3351        assert!(cred.sign(&secret, Some(Utc::now())).await.is_err());
3352    }
3353
3354    #[cfg(feature = "affinidi-signing")]
3355    #[test]
3356    fn test_signing_no_proof() {
3357        use crate::DTGCredentialError;
3358        use affinidi_secrets_resolver::secrets::Secret;
3359
3360        let cred = DTGCredential::new_vrc(
3361            "did:example:issuer".to_string(),
3362            "did:example:subject".to_string(),
3363            Utc::now(),
3364            None,
3365        );
3366
3367        let secret = Secret::generate_ed25519(None, None);
3368        match cred.verify_proof_with_public_key(secret.get_public_bytes()) {
3369            Err(DTGCredentialError::NotSigned) => {
3370                // Good
3371            }
3372            _ => panic!("Expected NotSigned error!"),
3373        }
3374    }
3375
3376    /// The constructors that return a plain `Self` have no way to refuse a malformed
3377    /// window, so `validate` is where one is caught for them.
3378    #[test]
3379    fn validate_refuses_an_inverted_window() {
3380        let from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
3381            .unwrap()
3382            .with_timezone(&Utc);
3383        let vmc = |valid_from, valid_until| {
3384            DTGCredential::new_vmc(
3385                "did:example:community".to_string(),
3386                "did:example:member".to_string(),
3387                valid_from,
3388                valid_until,
3389                false,
3390            )
3391        };
3392
3393        assert!(matches!(
3394            vmc(from, Some(from - chrono::Duration::hours(1))).validate(),
3395            Err(DTGCredentialError::InvalidValidityWindow { .. })
3396        ));
3397        assert!(matches!(
3398            vmc(from, Some(from)).validate(),
3399            Err(DTGCredentialError::InvalidValidityWindow { .. })
3400        ));
3401
3402        // Open-ended, and backdated, are both well formed.
3403        assert!(vmc(from, None).validate().is_ok());
3404        assert!(
3405            vmc(from - chrono::Duration::days(3650), Some(from))
3406                .validate()
3407                .is_ok()
3408        );
3409    }
3410
3411    #[test]
3412    fn new_member_vmc_refuses_an_inverted_window() {
3413        let from = DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
3414            .unwrap()
3415            .with_timezone(&Utc);
3416        let grant = DTGCredential::new_vmc(
3417            "did:example:community".to_string(),
3418            "did:example:member".to_string(),
3419            from,
3420            None,
3421            false,
3422        );
3423
3424        assert!(matches!(
3425            DTGCredential::new_member_vmc_for(
3426                &wire(&grant),
3427                "did:example:member",
3428                from,
3429                Some(from - chrono::Duration::hours(1))
3430            ),
3431            Err(DTGCredentialError::InvalidValidityWindow { .. })
3432        ));
3433    }
3434
3435    /// `sign` runs `validate` first, so this library never puts a proof on a credential
3436    /// whose window is never open.
3437    #[cfg(feature = "affinidi-signing")]
3438    #[tokio::test]
3439    async fn sign_refuses_an_inverted_window() {
3440        use affinidi_secrets_resolver::secrets::Secret;
3441
3442        let secret = Secret::generate_ed25519(None, None);
3443        let mut vrc = DTGCredential::new_vrc(
3444            "did:example:issuer".to_string(),
3445            "did:example:subject".to_string(),
3446            Utc::now(),
3447            Some(Utc::now() - chrono::Duration::days(1)),
3448        );
3449
3450        assert!(matches!(
3451            vrc.sign(&secret, None).await,
3452            Err(DTGCredentialError::InvalidValidityWindow { .. })
3453        ));
3454        assert!(!vrc.signed(), "a refused credential must not carry a proof");
3455    }
3456}