Skip to main content

dtg_credentials/
create.rs

1/*!
2*   Builder methods for creating new entities.
3*/
4
5#[allow(deprecated)]
6use crate::{
7    AuthorityGrant, CredentialSubject, CredentialSubjectAuthority, CredentialSubjectBasic,
8    CredentialSubjectDelegation, CredentialSubjectEndorsement, CredentialSubjectMembership,
9    CredentialSubjectRCard, CredentialSubjectWitness, DTGCommon, DTGCredential, DTGCredentialError,
10    DTGCredentialType, DelegationGrant, WitnessContext,
11};
12use chrono::{DateTime, SubsecRound, Utc};
13use serde_json::Value;
14
15/// Refuses a validity window that closes before, or at the instant, it opens.
16///
17/// Only the ordering is checked. A `valid_from` in the past is legitimate — backdating is
18/// how a re-issued credential keeps the date the original took effect — and whether a
19/// window is current is a question about an instant the verifier chooses.
20///
21/// Both ends are compared at whole seconds, because that is all the wire form carries: a
22/// window a few hundred milliseconds wide in memory serializes as an empty one.
23pub(crate) fn check_window(
24    valid_from: DateTime<Utc>,
25    valid_until: Option<DateTime<Utc>>,
26) -> Result<(), DTGCredentialError> {
27    match valid_until {
28        Some(valid_until) if valid_until.trunc_subsecs(0) <= valid_from.trunc_subsecs(0) => {
29            Err(DTGCredentialError::InvalidValidityWindow {
30                valid_from,
31                valid_until,
32            })
33        }
34        _ => Ok(()),
35    }
36}
37
38/// The `type` prefix every version of `witness/session` shares.
39const WITNESS_SESSION_TYPE_PREFIX: &str = "https://trusttasks.org/spec/witness/session/";
40
41/// Refuses anything but the `witness/session` document that opened a witness session.
42///
43/// Two checks, both about naming the right exchange. The `type` must be
44/// `witness/session/<major>.<minor>` exactly — `witness/session/submit/0.1` shares the
45/// prefix and is the likeliest wrong document to hold, and a `#response` fragment is the
46/// witness's answer, not the opening document. And `threadId` must equal `id`, which
47/// `witness/session` Conformance, item 1, requires of the opening document.
48pub(crate) fn check_witness_session(session: &Value) -> Result<(), DTGCredentialError> {
49    let object = session
50        .as_object()
51        .ok_or_else(|| DTGCredentialError::MalformedTaskDocument("not a JSON object".into()))?;
52    let id = object
53        .get("id")
54        .and_then(Value::as_str)
55        .ok_or_else(|| DTGCredentialError::MalformedTaskDocument("no string `id`".into()))?;
56
57    let type_ = object
58        .get("type")
59        .and_then(Value::as_str)
60        .unwrap_or_default();
61    let is_version = |v: &str| {
62        v.split_once('.').is_some_and(|(major, minor)| {
63            !major.is_empty()
64                && !minor.is_empty()
65                && major.bytes().all(|b| b.is_ascii_digit())
66                && minor.bytes().all(|b| b.is_ascii_digit())
67        })
68    };
69    if !type_
70        .strip_prefix(WITNESS_SESSION_TYPE_PREFIX)
71        .is_some_and(is_version)
72    {
73        return Err(DTGCredentialError::NotAWitnessSession(format!(
74            "`type` is `{type_}`"
75        )));
76    }
77
78    match object.get("threadId").and_then(Value::as_str) {
79        Some(thread_id) if thread_id == id => Ok(()),
80        Some(thread_id) => Err(DTGCredentialError::NotAWitnessSession(format!(
81            "`threadId` `{thread_id}` is not the document's own `id` `{id}`"
82        ))),
83        None => Err(DTGCredentialError::NotAWitnessSession(
84            "no `threadId`; the opening document names its own thread".into(),
85        )),
86    }
87}
88
89/// The issuer of a credential in its wire form: a string, or an object carrying an `id`, per
90/// the W3C data model.
91pub(crate) fn issuer_of(credential: &serde_json::Map<String, Value>) -> Option<String> {
92    credential.get("issuer").and_then(|issuer| {
93        issuer
94            .as_str()
95            .or_else(|| issuer.get("id").and_then(Value::as_str))
96            .map(str::to_string)
97    })
98}
99
100/// Reads an RFC 3339 timestamp off a credential in its wire form, under its W3C VC 2.0 name
101/// or its 1.1 alias.
102///
103/// `Ok(None)` when neither is present. A value that is present but unreadable is an error
104/// rather than an absence: an expiry that cannot be read must not be treated as no expiry.
105pub(crate) fn read_timestamp(
106    credential: &serde_json::Map<String, Value>,
107    name: &str,
108    alias: &str,
109) -> Result<Option<DateTime<Utc>>, String> {
110    let Some(value) = credential.get(name).or_else(|| credential.get(alias)) else {
111        return Ok(None);
112    };
113    value
114        .as_str()
115        .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
116        .map(|t| Some(t.with_timezone(&Utc)))
117        .ok_or_else(|| format!("`{name}` is not an RFC 3339 timestamp"))
118}
119
120/// The `validUntil` of a grant in its wire form, if it has one.
121fn grant_valid_until(grant: &Value) -> Result<Option<DateTime<Utc>>, String> {
122    match grant.as_object() {
123        Some(object) => read_timestamp(object, "validUntil", "expirationDate"),
124        None => Ok(None),
125    }
126}
127
128/// Refuses an answer to a grant — an acknowledgement or an acceptance — that would remain
129/// valid after the grant it answers has expired.
130///
131/// Open-ended counts as outliving a grant that expires. Compared at whole seconds, the
132/// precision the wire form carries.
133fn check_within_grant(
134    valid_until: Option<DateTime<Utc>>,
135    grant_valid_until: Option<DateTime<Utc>>,
136) -> Result<(), DTGCredentialError> {
137    let Some(grant_valid_until) = grant_valid_until else {
138        return Ok(());
139    };
140    match valid_until {
141        Some(until) if until.trunc_subsecs(0) <= grant_valid_until.trunc_subsecs(0) => Ok(()),
142        _ => Err(DTGCredentialError::OutlivesGrant {
143            valid_until,
144            grant_valid_until,
145        }),
146    }
147}
148
149impl DTGCredential {
150    /// Creates a new community-issued Verifiable Membership Credential (VMC) — the
151    /// membership **grant**, the community → member half of a membership edge.
152    ///
153    /// A membership edge is a *pair* of VMCs, and this is only one of them. The member
154    /// answers with [DTGCredential::new_member_vmc_for], and the edge is not complete until
155    /// they have: a community can always issue a credential naming somebody as a member,
156    /// but it cannot produce the acknowledgement without that party's signature. The pair
157    /// is what makes an unconsented membership claim unprovable.
158    ///
159    /// The grant MUST NOT carry a `digestMultibase` — that property is what marks the
160    /// other direction — and this constructor does not set one.
161    ///
162    /// issuer: The identifier of the VTC or VTN granting membership
163    /// subject: The member's identifier, or the member VTC's own for VTN membership
164    /// valid_from: The datetime from which this credential is valid
165    /// valid_until: Optional: The datetime this credential is valid until
166    /// personhood: Whether this VMC can be used as a form of Personhood Credential
167    ///             - Adds PersonhoodCredential to the type array if true
168    ///
169    /// # Give it an `id`
170    ///
171    /// Chain [DTGCredential::with_id] on: the member stores the grant under its `id`, and
172    /// re-issuing is only recognisable as a renewal rather than a duplicate if there is one.
173    pub fn new_vmc(
174        issuer: String,
175        subject: String,
176        valid_from: DateTime<Utc>,
177        valid_until: Option<DateTime<Utc>>,
178        personhood: bool,
179    ) -> Self {
180        let mut vmc = DTGCommon {
181            issuer,
182            valid_from,
183            valid_until,
184            credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
185                id: subject,
186                digest_multibase: None,
187            }),
188            ..Default::default()
189        };
190
191        vmc.type_.push(DTGCredentialType::Membership.to_string());
192
193        if personhood {
194            vmc.type_.push("PersonhoodCredential".to_string());
195        }
196
197        DTGCredential {
198            credential: vmc,
199            type_: DTGCredentialType::Membership,
200            version: crate::W3CVCVersion::V2_0,
201        }
202    }
203
204    /// Creates a new member-issued Verifiable Membership Credential (VMC) — the membership
205    /// **acknowledgement**, the member → community half of a membership edge.
206    ///
207    /// The roles of [DTGCredential::new_vmc] are reversed (the member issues, the community
208    /// is the subject) and the subject carries a `digestMultibase` of the grant being
209    /// acknowledged.
210    /// That digest is what binds the two halves into one edge: an acknowledgement whose
211    /// digest matches no valid grant does not complete anything, and the binding forces an
212    /// order — the grant must exist before this can reference it.
213    ///
214    /// This is the member's consent artifact. Because the member is its issuer, withdrawing
215    /// consent needs no cooperation from the community.
216    ///
217    /// # Takes the grant in its wire form, deliberately
218    ///
219    /// `grant` is the JSON the community sent, not a parsed [DTGCredential]. The digest has
220    /// to cover the document the community will recompute it over, and this library does
221    /// not model every member a credential may carry — `credentialStatus`, which every VMC
222    /// issued against a status list carries, is dropped by a parse-then-re-serialise round
223    /// trip. Building the acknowledgement from a parsed grant would produce a digest that
224    /// verifies nowhere, and would do it silently.
225    ///
226    /// So: keep the bytes you were given, and pass them here.
227    ///
228    /// member: The member acknowledging — the party whose key will sign this. Refused unless
229    ///         the grant names exactly this identifier as its subject.
230    /// valid_from: The datetime from which this credential is valid
231    /// valid_until: Optional: The datetime this credential is valid until. Must not be later
232    ///              than the grant's own `validUntil`, and must be set if the grant's is.
233    ///
234    /// # Errors
235    ///
236    /// [DTGCredentialError::NotAMembershipGrant] if `grant` is not a JSON object, does not
237    /// carry `MembershipCredential` in its `type`, has no `issuer` or
238    /// `credentialSubject.id`, or already carries a `digest` — that last is an
239    /// acknowledgement, and acknowledging one does not form an edge.
240    ///
241    /// It is also [DTGCredentialError::NotAMembershipGrant] if the grant carries a
242    /// `validUntil` that is not an RFC 3339 timestamp.
243    ///
244    /// [DTGCredentialError::NotTheGrantSubject] if the grant's `credentialSubject.id` is not
245    /// `member`.
246    ///
247    /// [DTGCredentialError::OutlivesGrant] if the grant expires and `valid_until` is later
248    /// than it, or absent.
249    ///
250    /// [DTGCredentialError::InvalidValidityWindow] if `valid_until` is not after
251    /// `valid_from`, and [DTGCredentialError::JsonTooDeep] if the grant is nested more deeply
252    /// than [crate::MAX_JSON_DEPTH].
253    ///
254    /// # Give it an `id`
255    ///
256    /// Chain [DTGCredential::with_id] on before signing. A community keys a member's VMC by
257    /// `id` to tell a re-send from a renewal.
258    ///
259    /// # Security
260    ///
261    /// The result is binding evidence, not membership. This constructor does not verify the
262    /// grant's proof, so it builds an acknowledgement of a grant nobody signed as readily as
263    /// of one the community did. Verify the grant first — with
264    /// `verify_grant_with_public_key` under the `affinidi-signing` feature, or against your
265    /// own resolver — and treat the edge as complete only once both proofs and both windows
266    /// have verified.
267    ///
268    /// Pass as `member` the identity whose key will sign the acknowledgement, established
269    /// independently of the grant. An identifier read out of the grant would make the check
270    /// compare the grant with itself.
271    pub fn new_member_vmc_for(
272        grant: &Value,
273        member: &str,
274        valid_from: DateTime<Utc>,
275        valid_until: Option<DateTime<Utc>>,
276    ) -> Result<Self, DTGCredentialError> {
277        check_window(valid_from, valid_until)?;
278
279        let (found, community) = Self::read_membership_grant(grant)?;
280        if found != member {
281            return Err(DTGCredentialError::NotTheGrantSubject {
282                expected: member.to_string(),
283                found,
284            });
285        }
286        let grant_valid_until =
287            grant_valid_until(grant).map_err(DTGCredentialError::NotAMembershipGrant)?;
288        check_within_grant(valid_until, grant_valid_until)?;
289
290        Self::assemble_member_vmc(grant, found, community, valid_from, valid_until)
291    }
292
293    /// Creates a member-issued VMC without checking who the grant names or when it expires.
294    ///
295    /// Identical to [DTGCredential::new_member_vmc_for] except that the member is taken from
296    /// the grant with nothing to compare it against, and the grant's `validUntil` is not
297    /// consulted.
298    #[deprecated(
299        since = "0.10.0",
300        note = "Takes the member from the grant without comparing it to anything, and lets \
301                the acknowledgement outlive the grant. Use DTGCredential::new_member_vmc_for, \
302                which takes the member you expect and refuses a grant naming anyone else. \
303                This constructor will be removed in a future release."
304    )]
305    pub fn new_member_vmc(
306        grant: &Value,
307        valid_from: DateTime<Utc>,
308        valid_until: Option<DateTime<Utc>>,
309    ) -> Result<Self, DTGCredentialError> {
310        check_window(valid_from, valid_until)?;
311
312        let (member, community) = Self::read_membership_grant(grant)?;
313        Self::assemble_member_vmc(grant, member, community, valid_from, valid_until)
314    }
315
316    /// Reads the member and the community off a membership grant in its wire form, refusing
317    /// anything that is not a community-issued grant.
318    fn read_membership_grant(grant: &Value) -> Result<(String, String), DTGCredentialError> {
319        let object = grant
320            .as_object()
321            .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("not a JSON object".into()))?;
322
323        let is_membership = object
324            .get("type")
325            .and_then(Value::as_array)
326            .is_some_and(|types| {
327                types
328                    .iter()
329                    .filter_map(Value::as_str)
330                    .any(|t| t == "MembershipCredential")
331            });
332        if !is_membership {
333            return Err(DTGCredentialError::NotAMembershipGrant(
334                "`type` does not include `MembershipCredential`".into(),
335            ));
336        }
337
338        let subject = object
339            .get("credentialSubject")
340            .and_then(Value::as_object)
341            .ok_or_else(|| {
342                DTGCredentialError::NotAMembershipGrant("no `credentialSubject`".into())
343            })?;
344
345        // Both spellings: `digestMultibase` is the Working Draft 02 name, `digest` the
346        // Working Draft 01 one this library also accepts on the wire. Probing only the
347        // current name would let an acknowledgement issued against the older draft be
348        // acknowledged in turn, which forms no edge.
349        if subject.contains_key("digestMultibase") || subject.contains_key("digest") {
350            return Err(DTGCredentialError::NotAMembershipGrant(
351                "the credential carries a digest of another credential, so it is itself a \
352                 member-issued acknowledgement rather than a community-issued grant"
353                    .into(),
354            ));
355        }
356
357        // The member is the grant's subject and the community its issuer: reading both off
358        // the grant is what keeps the two halves naming the same pair. Taking them as
359        // parameters would let a caller acknowledge one grant while naming the parties of
360        // another, which verifies as a digest match and means nothing.
361        let member = subject
362            .get("id")
363            .and_then(Value::as_str)
364            .ok_or_else(|| {
365                DTGCredentialError::NotAMembershipGrant("no `credentialSubject.id`".into())
366            })?
367            .to_string();
368
369        let community = issuer_of(object)
370            .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("no `issuer`".into()))?;
371
372        Ok((member, community))
373    }
374
375    /// Assembles the acknowledgement once the grant has been read and every check has passed.
376    fn assemble_member_vmc(
377        grant: &Value,
378        member: String,
379        community: String,
380        valid_from: DateTime<Utc>,
381        valid_until: Option<DateTime<Utc>>,
382    ) -> Result<Self, DTGCredentialError> {
383        let mut vmc = DTGCommon {
384            issuer: member,
385            valid_from,
386            valid_until,
387            credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
388                id: community,
389                digest_multibase: Some(crate::digest_multibase_json(grant)?),
390            }),
391            ..Default::default()
392        };
393
394        vmc.type_.push(DTGCredentialType::Membership.to_string());
395
396        Ok(DTGCredential {
397            credential: vmc,
398            type_: DTGCredentialType::Membership,
399            version: crate::W3CVCVersion::V2_0,
400        })
401    }
402
403    /// Creates a new Verified Relationship Credential (VRC)
404    /// issuer: The issuer DID of the credential
405    /// subject: The DID of the subject of this credential
406    /// valid_from: The datetime from which this credential is valid
407    /// valid_until: Optional: The datetime this credential is valid until
408    pub fn new_vrc(
409        issuer: String,
410        subject: String,
411        valid_from: DateTime<Utc>,
412        valid_until: Option<DateTime<Utc>>,
413    ) -> Self {
414        let mut vrc = DTGCommon {
415            issuer,
416            valid_from,
417            valid_until,
418            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
419            ..Default::default()
420        };
421
422        vrc.type_.push(DTGCredentialType::Relationship.to_string());
423
424        DTGCredential {
425            credential: vrc,
426            type_: DTGCredentialType::Relationship,
427            version: crate::W3CVCVersion::V2_0,
428        }
429    }
430
431    /// Creates a new Verified Invitation Credential (VIC)
432    /// issuer: The issuer DID of the credential
433    /// subject: The DID of the subject of this credential
434    /// valid_from: The datetime from which this credential is valid
435    /// valid_until: Optional: The datetime this credential is valid until
436    pub fn new_vic(
437        issuer: String,
438        subject: String,
439        valid_from: DateTime<Utc>,
440        valid_until: Option<DateTime<Utc>>,
441    ) -> Self {
442        let mut vic = DTGCommon {
443            issuer,
444            valid_from,
445            valid_until,
446            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
447            ..Default::default()
448        };
449
450        vic.type_.push(DTGCredentialType::Invitation.to_string());
451
452        DTGCredential {
453            credential: vic,
454            type_: DTGCredentialType::Invitation,
455            version: crate::W3CVCVersion::V2_0,
456        }
457    }
458
459    /// Creates a new Verifiable Authority Credential (VAC) — a chain root.
460    ///
461    /// The issuer is the party governing `scope`. To derive a narrower VAC from one you
462    /// already hold, use [DTGCredential::attenuate] instead: a chain root is a grant made
463    /// by the governing party, and minting one directly is how a self-issued grant of
464    /// arbitrary authority gets in.
465    ///
466    /// `actions` MUST NOT be empty — an empty list confers nothing rather than everything.
467    ///
468    /// # `valid_until` is required
469    ///
470    /// Not optional, unlike the base structure and unlike every other `new_*` constructor
471    /// here. Nothing about the subject's current standing is consulted when a VAC is
472    /// verified, so authority that does not expire is authority nobody can withdraw by
473    /// waiting.
474    ///
475    /// # Errors
476    ///
477    /// [DTGCredentialError::InvalidValidityWindow] if `valid_until` is not after
478    /// `valid_from`, and [DTGCredentialError::EmptyAuthorityActions] if `actions` is empty.
479    pub fn new_vac(
480        issuer: String,
481        subject: String,
482        scope: String,
483        actions: Vec<String>,
484        valid_from: DateTime<Utc>,
485        valid_until: DateTime<Utc>,
486    ) -> Result<Self, DTGCredentialError> {
487        check_window(valid_from, Some(valid_until))?;
488
489        if actions.is_empty() {
490            return Err(DTGCredentialError::EmptyAuthorityActions);
491        }
492        let mut vac = DTGCommon {
493            issuer,
494            valid_from,
495            valid_until: Some(valid_until),
496            credential_subject: CredentialSubject::Authority(CredentialSubjectAuthority {
497                id: subject,
498                authority: AuthorityGrant {
499                    scope,
500                    actions,
501                    parent: None,
502                },
503            }),
504            ..Default::default()
505        };
506
507        vac.type_.push(DTGCredentialType::Authority.to_string());
508
509        Ok(DTGCredential {
510            credential: vac,
511            type_: DTGCredentialType::Authority,
512            version: crate::W3CVCVersion::V2_0,
513        })
514    }
515
516    /// Derive a narrower VAC from one this holder already holds.
517    ///
518    /// This is what lets a member equip an agent, a device, or a short-lived session with
519    /// only the authority that task needs, rather than lending it their own. The derived
520    /// credential is issued by the *holder*, not by the party governing the scope, and
521    /// carries `parent` — the **digest** of the credential it narrows — so a verifier can
522    /// walk back to a root.
523    ///
524    /// Refuses anything that would widen. The checks here mirror
525    /// [crate::authority::verify_chain] on purpose: a holder should be unable to *build* a
526    /// chain a verifier would reject, so the failure surfaces at issue time rather than at
527    /// use — but the verifier's checks remain authoritative, because nothing stops a
528    /// different implementation constructing the JSON by hand.
529    ///
530    /// - `self` must be a VAC.
531    /// - `actions` must be a subset of what `self` confers.
532    /// - `valid_until` must not exceed `self`'s.
533    ///
534    /// # Binding the derivative to the agent is `subject`, not a separate field
535    ///
536    /// A VAC is not a bearer credential: [crate::authority::verify_chain] requires the
537    /// party presenting the leaf to be its subject. So equipping an agent means naming the
538    /// agent in `subject`, and there is nothing further to bind. An earlier version of this
539    /// method took an `audience` for that job; it was removed with the property.
540    ///
541    /// # Digests the model
542    ///
543    /// The `parent` digest is computed with [DTGCredential::digest_multibase], which hashes
544    /// this in-memory credential. That is right for a VAC this process built and signed.
545    /// For one that **arrived from a counterparty**, use
546    /// [DTGCredential::attenuate_from_json] and give it the bytes you received — the same
547    /// distinction [DTGCredential::new_member_vmc_for] draws, and for the same reason.
548    pub fn attenuate(
549        &self,
550        subject: String,
551        actions: Vec<String>,
552        valid_from: DateTime<Utc>,
553        valid_until: DateTime<Utc>,
554    ) -> Result<Self, DTGCredentialError> {
555        let parent_grant = self
556            .credential()
557            .authority()
558            .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
559
560        Self::attenuate_inner(
561            parent_grant.clone(),
562            self.credential().subject().to_string(),
563            self.credential().valid_until(),
564            self.digest_multibase()?,
565            subject,
566            actions,
567            valid_from,
568            valid_until,
569        )
570    }
571
572    /// Derive a narrower VAC from a parent in its **wire form**.
573    ///
574    /// Identical to [DTGCredential::attenuate] except that the parent is the JSON a
575    /// counterparty sent rather than a parsed credential, so the `parent` digest covers
576    /// the document the verifier will recompute it over. Use this whenever the VAC being
577    /// narrowed came from somewhere else.
578    ///
579    /// # Errors
580    ///
581    /// [DTGCredentialError::NotAnAuthorityCredential] if `parent` is not a JSON object
582    /// carrying `AuthorityCredential` in its `type` and a well-formed
583    /// `credentialSubject.authority`, and the same widening errors as
584    /// [DTGCredential::attenuate].
585    pub fn attenuate_from_json(
586        parent: &Value,
587        subject: String,
588        actions: Vec<String>,
589        valid_from: DateTime<Utc>,
590        valid_until: DateTime<Utc>,
591    ) -> Result<Self, DTGCredentialError> {
592        // Before any member is read out: reading clones one, and cloning recurses.
593        crate::check_json_depth(parent)?;
594
595        let object = parent
596            .as_object()
597            .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
598
599        let is_authority = object
600            .get("type")
601            .and_then(Value::as_array)
602            .is_some_and(|types| {
603                types
604                    .iter()
605                    .filter_map(Value::as_str)
606                    .any(|t| t == "AuthorityCredential")
607            });
608        if !is_authority {
609            return Err(DTGCredentialError::NotAnAuthorityCredential);
610        }
611
612        let parent_subject = object
613            .get("credentialSubject")
614            .and_then(Value::as_object)
615            .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
616
617        // The holder attenuating is the parent's subject; reading it off the parent is what
618        // keeps a derived VAC from citing a chain its issuer never held.
619        let holder = parent_subject
620            .get("id")
621            .and_then(Value::as_str)
622            .ok_or(DTGCredentialError::NotAnAuthorityCredential)?
623            .to_string();
624
625        let parent_grant: AuthorityGrant = parent_subject
626            .get("authority")
627            .ok_or(DTGCredentialError::NotAnAuthorityCredential)
628            .and_then(|a| {
629                serde_json::from_value(a.clone())
630                    .map_err(|_| DTGCredentialError::NotAnAuthorityCredential)
631            })?;
632
633        let parent_until = object
634            .get("validUntil")
635            .or_else(|| object.get("expirationDate"))
636            .and_then(Value::as_str)
637            .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
638            .map(|t| t.with_timezone(&Utc));
639
640        Self::attenuate_inner(
641            parent_grant,
642            holder,
643            parent_until,
644            crate::digest_multibase_json(parent)?,
645            subject,
646            actions,
647            valid_from,
648            valid_until,
649        )
650    }
651
652    /// The narrowing checks and the assembly, shared by both attenuation entry points.
653    #[allow(clippy::too_many_arguments)]
654    fn attenuate_inner(
655        parent_grant: AuthorityGrant,
656        holder: String,
657        parent_until: Option<DateTime<Utc>>,
658        parent_digest: String,
659        subject: String,
660        actions: Vec<String>,
661        valid_from: DateTime<Utc>,
662        valid_until: DateTime<Utc>,
663    ) -> Result<Self, DTGCredentialError> {
664        check_window(valid_from, Some(valid_until))?;
665
666        if actions.is_empty() {
667            return Err(DTGCredentialError::EmptyAuthorityActions);
668        }
669        for action in &actions {
670            if !parent_grant.actions.contains(action) {
671                return Err(DTGCredentialError::AttenuationWidens(format!(
672                    "action `{action}` is not conferred by the parent"
673                )));
674            }
675        }
676        if let Some(parent_until) = parent_until
677            && valid_until > parent_until
678        {
679            return Err(DTGCredentialError::AttenuationWidens(format!(
680                "validUntil {valid_until} is beyond the parent's {parent_until}"
681            )));
682        }
683
684        let mut vac = DTGCommon {
685            // The holder issues: they are the subject of the parent grant.
686            issuer: holder,
687            valid_from,
688            valid_until: Some(valid_until),
689            credential_subject: CredentialSubject::Authority(CredentialSubjectAuthority {
690                id: subject,
691                authority: AuthorityGrant {
692                    // Scope never changes down a chain.
693                    scope: parent_grant.scope.clone(),
694                    actions,
695                    parent: Some(parent_digest),
696                },
697            }),
698            ..Default::default()
699        };
700
701        vac.type_.push(DTGCredentialType::Authority.to_string());
702
703        Ok(DTGCredential {
704            credential: vac,
705            type_: DTGCredentialType::Authority,
706            version: crate::W3CVCVersion::V2_0,
707        })
708    }
709
710    /// Creates a new Verifiable Delegation Credential (VDC) — the delegation **grant**,
711    /// the delegator → delegate half of a delegation edge.
712    ///
713    /// Establishes that `subject` may act **in the issuer's name**, for the acts named in
714    /// `scope`, until `valid_until`. Within that scope what the delegate does is
715    /// attributable to the delegator.
716    ///
717    /// # This is not authority
718    ///
719    /// A VDC never supplies permission the delegator did not itself hold. A verifier
720    /// substitutes the delegator for the delegate and then asks the permission question it
721    /// would have asked of the delegator directly — so withdrawing the delegator's own
722    /// permission ends the delegate's ability to act immediately, without revoking
723    /// anything. See [DTGCredential::new_vac] for the credential that answers that
724    /// question.
725    ///
726    /// # The edge is not complete without the acceptance
727    ///
728    /// This is one half. The delegate answers with [DTGCredential::new_delegate_vdc_for], and
729    /// a verifier MUST obtain and verify that half before accepting any party as acting
730    /// under the delegation: a grant alone establishes what the delegator appointed, not
731    /// what the delegate agreed to. Same consent rule as a membership edge, and for the
732    /// same reason — a delegator can always name someone as its delegate, but cannot
733    /// produce the countersignature.
734    ///
735    /// `scope` MUST NOT be empty: a VDC cannot express an unbounded appointment by
736    /// omitting it.
737    ///
738    /// `max_depth` is the number of further re-delegations permitted below this one.
739    /// `None` and `Some(0)` both prohibit re-delegation — the default is a single hop, and
740    /// setting it above zero is the delegator's explicit authorisation, of which there is
741    /// no other kind.
742    ///
743    /// # `valid_until` is required
744    ///
745    /// An appointment with no expiry cannot be reasoned about by a verifier that cannot
746    /// reach the delegator.
747    ///
748    /// # Errors
749    ///
750    /// [DTGCredentialError::MalformedDelegation] if `scope` is empty, and
751    /// [DTGCredentialError::InvalidValidityWindow] if `valid_until` is not after
752    /// `valid_from`.
753    pub fn new_vdc(
754        issuer: String,
755        subject: String,
756        valid_from: DateTime<Utc>,
757        valid_until: DateTime<Utc>,
758        scope: Vec<String>,
759        max_depth: Option<u32>,
760    ) -> Result<Self, DTGCredentialError> {
761        check_window(valid_from, Some(valid_until))?;
762
763        if scope.is_empty() {
764            return Err(DTGCredentialError::MalformedDelegation(
765                "a grant MUST carry at least one `scope` entry — a VDC cannot express an \
766                 unbounded appointment by emptying it"
767                    .into(),
768            ));
769        }
770
771        let mut vdc = DTGCommon {
772            issuer,
773            valid_from,
774            valid_until: Some(valid_until),
775            credential_subject: CredentialSubject::Delegation(CredentialSubjectDelegation {
776                id: subject,
777                delegation: DelegationGrant {
778                    scope: Some(scope),
779                    parent: None,
780                    max_depth,
781                    accepts: None,
782                },
783            }),
784            ..Default::default()
785        };
786
787        vdc.type_.push(DTGCredentialType::Delegation.to_string());
788
789        Ok(DTGCredential {
790            credential: vdc,
791            type_: DTGCredentialType::Delegation,
792            version: crate::W3CVCVersion::V2_0,
793        })
794    }
795
796    /// Derive a further VDC from one this delegate already holds — a **re-delegation**.
797    ///
798    /// Only permitted where the held VDC sets `maxDepth` above zero, and only for a subset
799    /// of the acts it was itself appointed for. The default is a single hop: a delegate
800    /// that needs a further delegate and is not authorised to re-delegate asks the
801    /// principal, who issues a fresh root delegation directly — so that the principal
802    /// always holds the complete register of who may speak in its name.
803    ///
804    /// The derived VDC carries `parent`, the digest of the VDC it derives from, and a
805    /// `maxDepth` one less than its parent's.
806    ///
807    /// Like [DTGCredential::attenuate], this digests the in-memory model; for a grant that
808    /// arrived from a counterparty, use [DTGCredential::redelegate_from_json].
809    ///
810    /// # Errors
811    ///
812    /// [DTGCredentialError::MalformedDelegation] if `self` is not a delegation grant, if
813    /// it does not permit re-delegation, if `scope` is empty or not a subset of the
814    /// parent's, or if `valid_until` is later than the parent's.
815    /// [DTGCredentialError::InvalidValidityWindow] if `valid_until` is not after
816    /// `valid_from`.
817    pub fn redelegate(
818        &self,
819        subject: String,
820        scope: Vec<String>,
821        valid_from: DateTime<Utc>,
822        valid_until: DateTime<Utc>,
823    ) -> Result<Self, DTGCredentialError> {
824        let parent = self.credential().delegation().ok_or_else(|| {
825            DTGCredentialError::MalformedDelegation("not a DelegationCredential".into())
826        })?;
827
828        Self::redelegate_inner(
829            parent.clone(),
830            self.credential().subject().to_string(),
831            self.credential().valid_until(),
832            self.digest_multibase()?,
833            subject,
834            scope,
835            valid_from,
836            valid_until,
837        )
838    }
839
840    /// Derive a further VDC from a parent grant in its **wire form**.
841    ///
842    /// Identical to [DTGCredential::redelegate] except that the parent is the JSON the
843    /// delegator sent, so the `parent` digest covers the document a verifier will
844    /// recompute it over.
845    pub fn redelegate_from_json(
846        parent: &Value,
847        subject: String,
848        scope: Vec<String>,
849        valid_from: DateTime<Utc>,
850        valid_until: DateTime<Utc>,
851    ) -> Result<Self, DTGCredentialError> {
852        let (delegate, grant, parent_until) = Self::read_delegation_json(parent)?;
853
854        Self::redelegate_inner(
855            grant,
856            delegate,
857            parent_until,
858            crate::digest_multibase_json(parent)?,
859            subject,
860            scope,
861            valid_from,
862            valid_until,
863        )
864    }
865
866    /// The narrowing checks and the assembly, shared by both re-delegation entry points.
867    #[allow(clippy::too_many_arguments)]
868    fn redelegate_inner(
869        parent_grant: DelegationGrant,
870        holder: String,
871        parent_until: Option<DateTime<Utc>>,
872        parent_digest: String,
873        subject: String,
874        scope: Vec<String>,
875        valid_from: DateTime<Utc>,
876        valid_until: DateTime<Utc>,
877    ) -> Result<Self, DTGCredentialError> {
878        check_window(valid_from, Some(valid_until))?;
879
880        if parent_grant.accepts.is_some() {
881            return Err(DTGCredentialError::MalformedDelegation(
882                "the parent is an acceptance, not a grant — an acceptance appoints nobody \
883                 and cannot be re-delegated from"
884                    .into(),
885            ));
886        }
887
888        // Absence prohibits re-delegation just as `0` does. This is the opposite default
889        // from a VAC, deliberately: a delegate speaks in the principal's name, so the
890        // principal keeps the register of who may do so.
891        let parent_depth = parent_grant.max_depth.unwrap_or(0);
892        if parent_depth == 0 {
893            return Err(DTGCredentialError::MalformedDelegation(
894                "the parent does not permit re-delegation — `maxDepth` is absent or zero, \
895                 and setting it above zero is the delegator's only way to authorise one"
896                    .into(),
897            ));
898        }
899
900        if scope.is_empty() {
901            return Err(DTGCredentialError::MalformedDelegation(
902                "a grant MUST carry at least one `scope` entry".into(),
903            ));
904        }
905        let parent_scope = parent_grant.scope.as_deref().unwrap_or(&[]);
906        for act in &scope {
907            if !parent_scope.contains(act) {
908                return Err(DTGCredentialError::MalformedDelegation(format!(
909                    "`{act}` is not in the scope this delegation derives from"
910                )));
911            }
912        }
913        if let Some(parent_until) = parent_until
914            && valid_until > parent_until
915        {
916            return Err(DTGCredentialError::MalformedDelegation(format!(
917                "validUntil {valid_until} is beyond the parent's {parent_until}"
918            )));
919        }
920
921        let mut vdc = DTGCommon {
922            issuer: holder,
923            valid_from,
924            valid_until: Some(valid_until),
925            credential_subject: CredentialSubject::Delegation(CredentialSubjectDelegation {
926                id: subject,
927                delegation: DelegationGrant {
928                    scope: Some(scope),
929                    parent: Some(parent_digest),
930                    max_depth: Some(parent_depth - 1),
931                    accepts: None,
932                },
933            }),
934            ..Default::default()
935        };
936
937        vdc.type_.push(DTGCredentialType::Delegation.to_string());
938
939        Ok(DTGCredential {
940            credential: vdc,
941            type_: DTGCredentialType::Delegation,
942            version: crate::W3CVCVersion::V2_0,
943        })
944    }
945
946    /// Creates the delegate-issued half of a delegation edge — the **acceptance**.
947    ///
948    /// The roles of [DTGCredential::new_vdc] are reversed (the delegate issues, the
949    /// delegator is the subject) and the subject carries `accepts`, the digest of the
950    /// grant being taken on. That digest is what binds the two halves into one edge.
951    ///
952    /// An acceptance carries no `scope` of its own. What the delegate consented to is the
953    /// scope of the grant it names, which a verifier holds in any case; restating it would
954    /// require an equality check across the two credentials that cannot be satisfied under
955    /// selective disclosure of either.
956    ///
957    /// This is the delegate's consent artifact, and its accountability for acting in
958    /// another's name. Because a delegator cannot produce it, a party holding only the
959    /// delegate's key cannot manufacture appointments either.
960    ///
961    /// # Takes the grant in its wire form, deliberately
962    ///
963    /// Same reasoning as [DTGCredential::new_member_vmc_for]: the digest has to cover the
964    /// document the delegator will recompute it over. Keep the bytes you were given and
965    /// pass them here.
966    ///
967    /// # Errors
968    ///
969    /// [DTGCredentialError::NotADelegationGrant] if `grant` is not a JSON object carrying
970    /// `DelegationCredential` in its `type`, has no `issuer` or `credentialSubject.id`, or
971    /// already carries `accepts` — that last is itself an acceptance, and accepting one
972    /// forms no edge.
973    ///
974    /// It is also [DTGCredentialError::NotADelegationGrant] if the grant carries a
975    /// `validUntil` that is not an RFC 3339 timestamp.
976    ///
977    /// [DTGCredentialError::NotTheGrantSubject] if the grant's `credentialSubject.id` is not
978    /// `delegate`.
979    ///
980    /// [DTGCredentialError::OutlivesGrant] if `valid_until` is later than the grant's.
981    ///
982    /// [DTGCredentialError::InvalidValidityWindow] if `valid_until` is not after
983    /// `valid_from`, and [DTGCredentialError::JsonTooDeep] if the grant is nested more deeply
984    /// than [crate::MAX_JSON_DEPTH].
985    ///
986    /// # Security
987    ///
988    /// The result is binding evidence, not an appointment. This constructor does not verify
989    /// the grant's proof, so it builds an acceptance of a grant nobody signed as readily as of
990    /// one the delegator did. Verify the grant first — with `verify_grant_with_public_key`
991    /// under the `affinidi-signing` feature, or against your own resolver — and treat the edge
992    /// as complete only once both proofs and both windows have verified.
993    ///
994    /// Pass as `delegate` the identity whose key will sign the acceptance, established
995    /// independently of the grant. An identifier read out of the grant would make the check
996    /// compare the grant with itself.
997    pub fn new_delegate_vdc_for(
998        grant: &Value,
999        delegate: &str,
1000        valid_from: DateTime<Utc>,
1001        valid_until: DateTime<Utc>,
1002    ) -> Result<Self, DTGCredentialError> {
1003        check_window(valid_from, Some(valid_until))?;
1004
1005        let (found, delegator) = Self::read_delegation_grant(grant)?;
1006        if found != delegate {
1007            return Err(DTGCredentialError::NotTheGrantSubject {
1008                expected: delegate.to_string(),
1009                found,
1010            });
1011        }
1012        let grant_valid_until =
1013            grant_valid_until(grant).map_err(DTGCredentialError::NotADelegationGrant)?;
1014        check_within_grant(Some(valid_until), grant_valid_until)?;
1015
1016        Self::assemble_delegate_vdc(grant, found, delegator, valid_from, valid_until)
1017    }
1018
1019    /// Creates a delegate-issued acceptance without checking who the grant appoints or when it
1020    /// expires.
1021    ///
1022    /// Identical to [DTGCredential::new_delegate_vdc_for] except that the delegate is taken
1023    /// from the grant with nothing to compare it against, and the grant's `validUntil` is not
1024    /// consulted.
1025    #[deprecated(
1026        since = "0.10.0",
1027        note = "Takes the delegate from the grant without comparing it to anything, and lets \
1028                the acceptance outlive the grant. Use DTGCredential::new_delegate_vdc_for, \
1029                which takes the delegate you expect and refuses a grant appointing anyone \
1030                else. This constructor will be removed in a future release."
1031    )]
1032    pub fn new_delegate_vdc(
1033        grant: &Value,
1034        valid_from: DateTime<Utc>,
1035        valid_until: DateTime<Utc>,
1036    ) -> Result<Self, DTGCredentialError> {
1037        check_window(valid_from, Some(valid_until))?;
1038
1039        let (delegate, delegator) = Self::read_delegation_grant(grant)?;
1040        Self::assemble_delegate_vdc(grant, delegate, delegator, valid_from, valid_until)
1041    }
1042
1043    /// Reads the delegate and the delegator off a delegation grant in its wire form, refusing
1044    /// anything that is not a grant.
1045    fn read_delegation_grant(grant: &Value) -> Result<(String, String), DTGCredentialError> {
1046        let object = grant
1047            .as_object()
1048            .ok_or_else(|| DTGCredentialError::NotADelegationGrant("not a JSON object".into()))?;
1049
1050        let is_delegation = object
1051            .get("type")
1052            .and_then(Value::as_array)
1053            .is_some_and(|types| {
1054                types
1055                    .iter()
1056                    .filter_map(Value::as_str)
1057                    .any(|t| t == "DelegationCredential")
1058            });
1059        if !is_delegation {
1060            return Err(DTGCredentialError::NotADelegationGrant(
1061                "`type` does not include `DelegationCredential`".into(),
1062            ));
1063        }
1064
1065        let subject = object
1066            .get("credentialSubject")
1067            .and_then(Value::as_object)
1068            .ok_or_else(|| {
1069                DTGCredentialError::NotADelegationGrant("no `credentialSubject`".into())
1070            })?;
1071
1072        let delegation = subject
1073            .get("delegation")
1074            .and_then(Value::as_object)
1075            .ok_or_else(|| {
1076                DTGCredentialError::NotADelegationGrant("no `credentialSubject.delegation`".into())
1077            })?;
1078
1079        if delegation.contains_key("accepts") {
1080            return Err(DTGCredentialError::NotADelegationGrant(
1081                "the credential carries `accepts`, so it is itself an acceptance rather \
1082                 than a grant"
1083                    .into(),
1084            ));
1085        }
1086        if !delegation.contains_key("scope") {
1087            return Err(DTGCredentialError::NotADelegationGrant(
1088                "the grant carries no `scope`, so there is no appointment to accept".into(),
1089            ));
1090        }
1091
1092        // The delegate is the grant's subject and the delegator its issuer. Reading both
1093        // off the grant is what keeps the two halves naming the same pair — taking them as
1094        // parameters would let a caller accept one grant while naming the parties of
1095        // another, which verifies as a digest match and means nothing.
1096        let delegate = subject
1097            .get("id")
1098            .and_then(Value::as_str)
1099            .ok_or_else(|| {
1100                DTGCredentialError::NotADelegationGrant("no `credentialSubject.id`".into())
1101            })?
1102            .to_string();
1103
1104        let delegator = issuer_of(object)
1105            .ok_or_else(|| DTGCredentialError::NotADelegationGrant("no `issuer`".into()))?;
1106
1107        Ok((delegate, delegator))
1108    }
1109
1110    /// Assembles the acceptance once the grant has been read and every check has passed.
1111    fn assemble_delegate_vdc(
1112        grant: &Value,
1113        delegate: String,
1114        delegator: String,
1115        valid_from: DateTime<Utc>,
1116        valid_until: DateTime<Utc>,
1117    ) -> Result<Self, DTGCredentialError> {
1118        let mut vdc = DTGCommon {
1119            issuer: delegate,
1120            valid_from,
1121            valid_until: Some(valid_until),
1122            credential_subject: CredentialSubject::Delegation(CredentialSubjectDelegation {
1123                id: delegator,
1124                delegation: DelegationGrant {
1125                    scope: None,
1126                    parent: None,
1127                    max_depth: None,
1128                    accepts: Some(crate::digest_multibase_json(grant)?),
1129                },
1130            }),
1131            ..Default::default()
1132        };
1133
1134        vdc.type_.push(DTGCredentialType::Delegation.to_string());
1135
1136        Ok(DTGCredential {
1137            credential: vdc,
1138            type_: DTGCredentialType::Delegation,
1139            version: crate::W3CVCVersion::V2_0,
1140        })
1141    }
1142
1143    /// Reads the delegate, the grant, and the parent's expiry off a VDC in its wire form.
1144    fn read_delegation_json(
1145        doc: &Value,
1146    ) -> Result<(String, DelegationGrant, Option<DateTime<Utc>>), DTGCredentialError> {
1147        // Before any member is read out: reading clones one, and cloning recurses.
1148        crate::check_json_depth(doc)?;
1149
1150        let object = doc
1151            .as_object()
1152            .ok_or_else(|| DTGCredentialError::MalformedDelegation("not a JSON object".into()))?;
1153
1154        let is_delegation = object
1155            .get("type")
1156            .and_then(Value::as_array)
1157            .is_some_and(|types| {
1158                types
1159                    .iter()
1160                    .filter_map(Value::as_str)
1161                    .any(|t| t == "DelegationCredential")
1162            });
1163        if !is_delegation {
1164            return Err(DTGCredentialError::MalformedDelegation(
1165                "`type` does not include `DelegationCredential`".into(),
1166            ));
1167        }
1168
1169        let subject = object
1170            .get("credentialSubject")
1171            .and_then(Value::as_object)
1172            .ok_or_else(|| {
1173                DTGCredentialError::MalformedDelegation("no `credentialSubject`".into())
1174            })?;
1175
1176        let delegate = subject
1177            .get("id")
1178            .and_then(Value::as_str)
1179            .ok_or_else(|| {
1180                DTGCredentialError::MalformedDelegation("no `credentialSubject.id`".into())
1181            })?
1182            .to_string();
1183
1184        let grant: DelegationGrant = subject
1185            .get("delegation")
1186            .ok_or_else(|| {
1187                DTGCredentialError::MalformedDelegation("no `credentialSubject.delegation`".into())
1188            })
1189            .and_then(|d| {
1190                serde_json::from_value(d.clone()).map_err(|e| {
1191                    DTGCredentialError::MalformedDelegation(format!("malformed `delegation`: {e}"))
1192                })
1193            })?;
1194
1195        let until = object
1196            .get("validUntil")
1197            .or_else(|| object.get("expirationDate"))
1198            .and_then(Value::as_str)
1199            .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
1200            .map(|t| t.with_timezone(&Utc));
1201
1202        Ok((delegate, grant, until))
1203    }
1204
1205    /// Creates a new Verified Persona Credential (VPC)
1206    /// issuer: The issuer DID of the credential
1207    /// subject: The DID of the subject of this credential
1208    /// valid_from: The datetime from which this credential is valid
1209    /// valid_until: Optional: The datetime this credential is valid until
1210    pub fn new_vpc(
1211        issuer: String,
1212        subject: String,
1213        valid_from: DateTime<Utc>,
1214        valid_until: Option<DateTime<Utc>>,
1215    ) -> Self {
1216        let mut vpc = DTGCommon {
1217            issuer,
1218            valid_from,
1219            valid_until,
1220            credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
1221            ..Default::default()
1222        };
1223
1224        vpc.type_.push(DTGCredentialType::Persona.to_string());
1225
1226        DTGCredential {
1227            credential: vpc,
1228            type_: DTGCredentialType::Persona,
1229            version: crate::W3CVCVersion::V2_0,
1230        }
1231    }
1232
1233    /// Creates a new Verified Endorsement Credential (VEC)
1234    /// issuer: The issuer DID of the credential
1235    /// subject: The DID of the subject of this credential
1236    /// valid_from: The datetime from which this credential is valid
1237    /// valid_until: Optional: The datetime this credential is valid until
1238    /// endorsement: The endorsement details for this credential
1239    ///
1240    /// # Security
1241    ///
1242    /// `endorsement` is embedded verbatim. The specification does not define its content,
1243    /// so its shape is the issuer's to choose and this library checks nothing about it but
1244    /// its depth. A VEC says only that its issuer said this: a consumer must verify the
1245    /// proof, *and* establish that the issuer is one whose endorsements it accepts for this
1246    /// purpose, before relying on any member of it.
1247    ///
1248    /// Build it from input you control. Nesting past [crate::MAX_JSON_DEPTH] is refused
1249    /// when the credential is validated, digested or signed rather than here, because this
1250    /// constructor cannot return an error.
1251    pub fn new_vec(
1252        issuer: String,
1253        subject: String,
1254        valid_from: DateTime<Utc>,
1255        valid_until: Option<DateTime<Utc>>,
1256        endorsement: Value,
1257    ) -> Self {
1258        let mut vec = DTGCommon {
1259            issuer,
1260            valid_from,
1261            valid_until,
1262            credential_subject: CredentialSubject::Endorsement(CredentialSubjectEndorsement {
1263                id: subject,
1264                endorsement,
1265            }),
1266            ..Default::default()
1267        };
1268
1269        vec.type_.push(DTGCredentialType::Endorsement.to_string());
1270
1271        DTGCredential {
1272            credential: vec,
1273            type_: DTGCredentialType::Endorsement,
1274            version: crate::W3CVCVersion::V2_0,
1275        }
1276    }
1277
1278    /// Creates a Verifiable Witness Credential (VWC) for a `witness/session`, citing the
1279    /// session by `taskContext` **and** `taskDigestMultibase`.
1280    ///
1281    /// This is the constructor for the only VWC-issuing flow the specifications define:
1282    /// the witness, answering `witness/session/submit` on the party's session, delivers the
1283    /// VWC in the response. That specification's Conformance, item 1, requires the VWC's
1284    /// `taskContext` to be the `id` of the `witness/session` document that opened the
1285    /// session and its `taskDigestMultibase` to be that document's task digest. Both are
1286    /// read from `session` here, so the pair cannot disagree.
1287    ///
1288    /// - `issuer`: the witness's DID — a member's identifier, or the DID of a VTA acting
1289    ///   according to VTC policy.
1290    /// - `subject`: the DID of the party observed **issuing** the edge credential that
1291    ///   `digest` names, i.e. that credential's `issuer`. One VWC per direction.
1292    /// - `session`: the `witness/session` document, as received. Its top-level `proof`, if
1293    ///   any, is excluded from the digest, so a signed and an unsigned copy give one value.
1294    /// - `digest`: the witnessed edge credential's digest, from
1295    ///   [DTGCredential::digest_multibase] or [crate::digest_multibase_json]. REQUIRED
1296    ///   here, unlike [DTGCredential::new_vwc]: a VWC without it does not say which edge
1297    ///   was witnessed.
1298    ///
1299    /// # Errors
1300    ///
1301    /// - [DTGCredentialError::MalformedTaskDocument] if `session` is not an object with a
1302    ///   string `id`.
1303    /// - [DTGCredentialError::NotAWitnessSession] if its `type` is not a `witness/session`
1304    ///   version, or its `threadId` is not its own `id`. `witness/session` requires the
1305    ///   opening document to name its own thread, so a document whose `threadId` differs
1306    ///   is a later document of some exchange, not the one that opened this one. Both
1307    ///   checks exist to catch citing the wrong exchange: the `submit` document, or the
1308    ///   relationship exchange the session is nested in.
1309    /// - [DTGCredentialError::InvalidValidityWindow] for a window that closes before it
1310    ///   opens, and [DTGCredentialError::JsonTooDeep] for a `session` nested past
1311    ///   [crate::MAX_JSON_DEPTH].
1312    ///
1313    /// # Security
1314    ///
1315    /// Pass the session document the witness itself received and answered, not one the
1316    /// party supplies alongside its submission. The digest is load-bearing because the
1317    /// witness signs it: it is how a verifier later tells this session from a counterfeit
1318    /// reusing its `id`.
1319    pub fn new_vwc_for_session(
1320        issuer: String,
1321        subject: String,
1322        valid_from: DateTime<Utc>,
1323        valid_until: Option<DateTime<Utc>>,
1324        session: &Value,
1325        digest: String,
1326        witness_context: Option<WitnessContext>,
1327    ) -> Result<Self, DTGCredentialError> {
1328        check_window(valid_from, valid_until)?;
1329        check_witness_session(session)?;
1330
1331        #[allow(deprecated)]
1332        let vwc = Self::new_vwc(
1333            issuer,
1334            subject,
1335            valid_from,
1336            valid_until,
1337            String::new(),
1338            Some(digest),
1339            witness_context,
1340        );
1341        vwc.with_task_citation(session)
1342    }
1343
1344    /// Creates a new Verified Witness Credential (VWC)
1345    ///
1346    /// Carries no `taskDigestMultibase`, so the VWC names its session by `id` alone. Use
1347    /// [DTGCredential::new_vwc_for_session], which reads both halves of the citation from
1348    /// the session document.
1349    ///
1350    /// issuer: The issuer DID of the credential - a member's identifier, or the DID of a
1351    ///         VTA acting according to VTC policy
1352    /// subject: The DID of the observed party. For a witnessed bi-directional exchange this
1353    ///          MUST be the issuer of the VRC that this VWC attests (the VRC referenced by
1354    ///          `digestMultibase`), so that the two VWCs of an exchange are unambiguously bound to
1355    ///          their respective directions. The witness should issue one VWC per direction.
1356    /// valid_from: The datetime from which this credential is valid
1357    /// valid_until: Optional: The datetime this credential is valid until
1358    /// task_context: Required `threadId` of the trust task exchange the witnessing occurred in
1359    /// digest: Cryptographic hash of the witnessed edge credential, binding this VWC to the
1360    ///         specific edge. Produce it with [DTGCredential::digest_multibase] on that
1361    ///         credential, or [crate::digest_multibase_json] on the bytes you received.
1362    ///         REQUIRED by the specification; `Option` here because a VWC that predates the
1363    ///         requirement still has to deserialize. A VWC without one identifies the
1364    ///         observed party and the exchange, but not which edge was witnessed.
1365    /// witness_context: Optional Semantic context for the witness
1366    #[deprecated(
1367        since = "0.11.0",
1368        note = "A VWC issued through witness/session MUST carry taskDigestMultibase, the \
1369                task digest of the session document, beside taskContext \
1370                (witness/session/submit Conformance, item 1), and this constructor cannot \
1371                set it. Use DTGCredential::new_vwc_for_session."
1372    )]
1373    pub fn new_vwc(
1374        issuer: String,
1375        subject: String,
1376        valid_from: DateTime<Utc>,
1377        valid_until: Option<DateTime<Utc>>,
1378        task_context: String,
1379        digest: Option<String>,
1380        witness_context: Option<WitnessContext>,
1381    ) -> Self {
1382        let mut vwc = DTGCommon {
1383            issuer,
1384            valid_from,
1385            valid_until,
1386            task_context: Some(task_context),
1387            credential_subject: CredentialSubject::Witness(CredentialSubjectWitness {
1388                id: subject,
1389                digest_multibase: digest,
1390                witness_context,
1391            }),
1392            ..Default::default()
1393        };
1394
1395        vwc.type_.push(DTGCredentialType::Witness.to_string());
1396
1397        DTGCredential {
1398            credential: vwc,
1399            type_: DTGCredentialType::Witness,
1400            version: crate::W3CVCVersion::V2_0,
1401        }
1402    }
1403
1404    /// Creates a new Verified RCard Credential (VWC)
1405    /// issuer: The issuer DID of the credential
1406    /// subject: The DID of the subject of this credential
1407    /// valid_from: The datetime from which this credential is valid
1408    /// valid_until: Optional: The datetime this credential is valid until
1409    /// card: JSON Value representing a Jcard (RFC 7095) format
1410    #[deprecated(
1411        since = "0.2.0",
1412        note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1413                It was removed from the DTG Core Credentials specification in Working Draft 01 \
1414                and will be defined by the planned DTG Verifiable Data Structures specification. \
1415                This constructor will be removed in a future release."
1416    )]
1417    #[allow(deprecated)]
1418    pub fn new_rcard(
1419        issuer: String,
1420        subject: String,
1421        valid_from: DateTime<Utc>,
1422        valid_until: Option<DateTime<Utc>>,
1423        card: Value,
1424    ) -> Self {
1425        let mut rcard = DTGCommon {
1426            issuer,
1427            valid_from,
1428            valid_until,
1429            credential_subject: CredentialSubject::RCard(CredentialSubjectRCard {
1430                id: subject,
1431                card,
1432            }),
1433            ..Default::default()
1434        };
1435
1436        rcard.type_.push(DTGCredentialType::RCard.to_string());
1437
1438        DTGCredential {
1439            credential: rcard,
1440            type_: DTGCredentialType::RCard,
1441            version: crate::W3CVCVersion::V2_0,
1442        }
1443    }
1444
1445    /// Sets this credential's own identifier, consuming and returning it so it chains onto
1446    /// any of the `new_*` constructors above.
1447    ///
1448    /// `id` MUST be a single URL per the W3C VC Data Model; `urn:uuid:<uuid>` is the usual
1449    /// choice for a credential with no dereferenceable home. This crate does not validate it.
1450    ///
1451    /// ```
1452    /// # use chrono::Utc;
1453    /// # use dtg_credentials::DTGCredential;
1454    /// let vmc = DTGCredential::new_vmc(
1455    ///     "did:example:member".to_string(),
1456    ///     "did:example:community".to_string(),
1457    ///     Utc::now(),
1458    ///     None,
1459    ///     false,
1460    /// )
1461    /// .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
1462    /// assert_eq!(vmc.id(), Some("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52"));
1463    /// ```
1464    ///
1465    /// # Set it before signing
1466    ///
1467    /// A Data Integrity proof covers the credential minus its `proof`, so `id` is part of what
1468    /// is signed. Chain this onto the constructor, before [DTGCredential::sign] — adding an id
1469    /// to an already-signed credential leaves a document whose proof no longer verifies.
1470    pub fn with_id(mut self, id: impl Into<String>) -> Self {
1471        self.credential.id = Some(id.into());
1472        self
1473    }
1474
1475    /// Sets this credential's own identifier in place.
1476    ///
1477    /// The non-consuming form of [DTGCredential::with_id]; the same "before signing" caveat
1478    /// applies.
1479    pub fn set_id(&mut self, id: impl Into<String>) {
1480        self.credential.id = Some(id.into());
1481    }
1482
1483    /// Cites a Trust Task document: sets `taskContext` to its `id` and
1484    /// `taskDigestMultibase` to its task digest, together.
1485    ///
1486    /// Use it for any credential whose meaning depends on an exchange — a VWC, or a
1487    /// statement a `vetting/session` produces. Name the **innermost** exchange that attests
1488    /// what the credential states, by the document that initiated it (Trust Tasks §4.9.1).
1489    /// Setting both halves from one document is the point: the `id` locates the exchange
1490    /// and the digest binds the credential to it, and a pair taken from two places binds
1491    /// nothing. See [crate::task_digest_multibase_json] for how the digest is computed.
1492    ///
1493    /// Replaces a `taskContext` and `taskDigestMultibase` already set.
1494    ///
1495    /// # Set it before signing
1496    ///
1497    /// Both members are covered by the credential's proof, as for [DTGCredential::with_id].
1498    ///
1499    /// # Errors
1500    ///
1501    /// [DTGCredentialError::MalformedTaskDocument] if `document` is not an object with a
1502    /// string `id`; [DTGCredentialError::JsonTooDeep] if it is nested past
1503    /// [crate::MAX_JSON_DEPTH].
1504    pub fn with_task_citation(mut self, document: &Value) -> Result<Self, DTGCredentialError> {
1505        self.set_task_citation(document)?;
1506        Ok(self)
1507    }
1508
1509    /// Cites a Trust Task document in place.
1510    ///
1511    /// The non-consuming form of [DTGCredential::with_task_citation]; the same caveats
1512    /// apply. On error, the credential is left unchanged.
1513    pub fn set_task_citation(&mut self, document: &Value) -> Result<(), DTGCredentialError> {
1514        let id = document
1515            .as_object()
1516            .ok_or_else(|| DTGCredentialError::MalformedTaskDocument("not a JSON object".into()))?
1517            .get("id")
1518            .and_then(Value::as_str)
1519            .ok_or_else(|| DTGCredentialError::MalformedTaskDocument("no string `id`".into()))?
1520            .to_string();
1521        let digest = crate::task_digest_multibase_json(document)?;
1522
1523        self.credential.task_context = Some(id);
1524        self.credential.task_digest_multibase = Some(digest);
1525        Ok(())
1526    }
1527
1528    /// Attaches the status mechanism through which a verifier determines whether this
1529    /// credential has been revoked.
1530    ///
1531    /// The entry is opaque to this library: the mechanism is chosen by the governing VTC
1532    /// or VTN, and nothing here selects one or resolves it. `BitstringStatusListEntry` is
1533    /// the common choice.
1534    ///
1535    /// ```
1536    /// # use chrono::{Duration, Utc};
1537    /// # use dtg_credentials::DTGCredential;
1538    /// # use serde_json::json;
1539    /// let vdc = DTGCredential::new_vdc(
1540    ///     "did:example:delegator".to_string(),
1541    ///     "did:example:delegate".to_string(),
1542    ///     Utc::now(),
1543    ///     Utc::now() + Duration::days(90),
1544    ///     vec!["sign:invoices".to_string()],
1545    ///     None,
1546    /// )
1547    /// .unwrap()
1548    /// .with_credential_status(json!({
1549    ///     "id": "https://example.com/status/3#94567",
1550    ///     "type": "BitstringStatusListEntry",
1551    ///     "statusPurpose": "revocation",
1552    ///     "statusListIndex": "94567",
1553    ///     "statusListCredential": "https://example.com/status/3"
1554    /// }));
1555    /// assert!(vdc.credential().credential_status.is_some());
1556    /// ```
1557    ///
1558    /// # When a VDC needs one
1559    ///
1560    /// CONDITIONAL, not required. A verifier MUST be able to establish that an appointment
1561    /// is in force without contacting the delegator, and either of two things satisfies
1562    /// that: a `validUntil` short enough that expiry alone bounds the exposure, or a status
1563    /// entry the verifier can check. A VDC MUST carry one where its validity period exceeds
1564    /// the freshness window the governing VTC or VTN defines for delegations, and MAY omit
1565    /// it otherwise.
1566    ///
1567    /// That window is governance this library does not know, so it cannot decide for a
1568    /// caller which side of the condition a given VDC falls on — hence a setter rather than
1569    /// a constructor parameter. Prefer short validity and re-issuance wherever the
1570    /// delegator is reachable: a status check is a live lookup that reveals the
1571    /// verification event to whoever hosts the status list. A long-lived appointment made
1572    /// in advance of a delegator's unavailability is the case this exists for.
1573    ///
1574    /// # Set it before signing
1575    ///
1576    /// Same caveat as [DTGCredential::with_id] — a Data Integrity proof covers the
1577    /// credential minus its `proof`, so attaching a status entry to an already-signed
1578    /// credential leaves a document whose proof no longer verifies.
1579    ///
1580    /// # This library does not check it
1581    ///
1582    /// Neither [`crate::delegation::verify_chain`] nor [`crate::authority::verify_chain`]
1583    /// resolves a status entry; both verify structure, scope and validity only. Revocation
1584    /// is a live lookup the caller performs.
1585    ///
1586    /// # Security
1587    ///
1588    /// The entry is embedded verbatim and nothing about it is checked when it is attached.
1589    /// It tells a verifier where to look, so a verifier must check the credential's proof
1590    /// before following it, and should apply its own policy to where it leads. Nesting past
1591    /// [crate::MAX_JSON_DEPTH] is refused when the credential is validated, digested or
1592    /// signed, because this setter cannot return an error.
1593    pub fn with_credential_status(mut self, status: Value) -> Self {
1594        self.credential.credential_status = Some(status);
1595        self
1596    }
1597
1598    /// Attaches a revocation status mechanism in place.
1599    ///
1600    /// The non-consuming form of [DTGCredential::with_credential_status]; the same "before
1601    /// signing" caveat, the same CONDITIONAL rule and the same security notes apply.
1602    pub fn set_credential_status(&mut self, status: Value) {
1603        self.credential.credential_status = Some(status);
1604    }
1605}
1606
1607#[cfg(test)]
1608#[allow(deprecated)]
1609mod tests {
1610    use crate::{DTGCredential, WitnessContext};
1611    use chrono::{DateTime, Utc};
1612    use serde_json::json;
1613
1614    #[test]
1615    fn test_vmc_serialization() {
1616        let vmc = DTGCredential::new_vmc(
1617            "did:example:issuer".to_string(),
1618            "did:example:subject".to_string(),
1619            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1620                .unwrap()
1621                .with_timezone(&Utc),
1622            None,
1623            false,
1624        );
1625
1626        let txt = serde_json::to_string_pretty(&vmc).unwrap();
1627        let sample = r#"{
1628  "@context": [
1629    "https://www.w3.org/ns/credentials/v2",
1630    "https://firstperson.network/credentials/dtg/v1"
1631  ],
1632  "type": [
1633    "VerifiableCredential",
1634    "DTGCredential",
1635    "MembershipCredential"
1636  ],
1637  "issuer": "did:example:issuer",
1638  "validFrom": "2025-12-11T00:00:00Z",
1639  "credentialSubject": {
1640    "id": "did:example:subject"
1641  }
1642}"#;
1643
1644        assert_eq!(txt, sample);
1645    }
1646
1647    #[test]
1648    fn test_vmc_phc_serialization() {
1649        let vmc = DTGCredential::new_vmc(
1650            "did:example:issuer".to_string(),
1651            "did:example:subject".to_string(),
1652            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1653                .unwrap()
1654                .with_timezone(&Utc),
1655            None,
1656            true,
1657        );
1658
1659        let txt = serde_json::to_string_pretty(&vmc).unwrap();
1660        let sample = r#"{
1661  "@context": [
1662    "https://www.w3.org/ns/credentials/v2",
1663    "https://firstperson.network/credentials/dtg/v1"
1664  ],
1665  "type": [
1666    "VerifiableCredential",
1667    "DTGCredential",
1668    "MembershipCredential",
1669    "PersonhoodCredential"
1670  ],
1671  "issuer": "did:example:issuer",
1672  "validFrom": "2025-12-11T00:00:00Z",
1673  "credentialSubject": {
1674    "id": "did:example:subject"
1675  }
1676}"#;
1677
1678        assert_eq!(txt, sample);
1679    }
1680    /// `id` is OPTIONAL, and a credential that was never given one must keep serializing the
1681    /// shape it always did — no `"id": null`, no empty string.
1682    #[test]
1683    fn test_vmc_without_id_omits_the_property() {
1684        let vmc = DTGCredential::new_vmc(
1685            "did:example:issuer".to_string(),
1686            "did:example:subject".to_string(),
1687            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1688                .unwrap()
1689                .with_timezone(&Utc),
1690            None,
1691            false,
1692        );
1693
1694        assert_eq!(vmc.id(), None);
1695        let value: serde_json::Value = serde_json::to_value(&vmc).unwrap();
1696        assert!(
1697            value.get("id").is_none(),
1698            "an unset id must not appear on the wire at all: {value}"
1699        );
1700    }
1701
1702    /// `with_id` puts the identifier at the top level of the credential — a sibling of
1703    /// `issuer`, not something nested under `credentialSubject` (which carries the *subject's*
1704    /// id, a different thing entirely).
1705    #[test]
1706    fn test_vmc_with_id_serialization() {
1707        let vmc = DTGCredential::new_vmc(
1708            "did:example:issuer".to_string(),
1709            "did:example:subject".to_string(),
1710            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1711                .unwrap()
1712                .with_timezone(&Utc),
1713            None,
1714            false,
1715        )
1716        .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
1717
1718        let txt = serde_json::to_string_pretty(&vmc).unwrap();
1719        let sample = r#"{
1720  "@context": [
1721    "https://www.w3.org/ns/credentials/v2",
1722    "https://firstperson.network/credentials/dtg/v1"
1723  ],
1724  "type": [
1725    "VerifiableCredential",
1726    "DTGCredential",
1727    "MembershipCredential"
1728  ],
1729  "id": "urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff",
1730  "issuer": "did:example:issuer",
1731  "validFrom": "2025-12-11T00:00:00Z",
1732  "credentialSubject": {
1733    "id": "did:example:subject"
1734  }
1735}"#;
1736
1737        assert_eq!(txt, sample);
1738    }
1739
1740    /// The identifier has to survive a round trip. It arrives on the wire and is read back
1741    /// through `TryFrom<DTGCommon>`, which is where `taskContext` was previously being dropped
1742    /// — a field that deserializes into nothing breaks signing and verification silently.
1743    #[test]
1744    fn test_id_round_trips_through_deserialization() {
1745        let vmc = DTGCredential::new_vmc(
1746            "did:example:issuer".to_string(),
1747            "did:example:subject".to_string(),
1748            Utc::now(),
1749            None,
1750            false,
1751        )
1752        .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
1753
1754        let txt = serde_json::to_string(&vmc).unwrap();
1755        let parsed: DTGCredential = serde_json::from_str(&txt).unwrap();
1756        assert_eq!(
1757            parsed.id(),
1758            Some("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff")
1759        );
1760    }
1761
1762    /// A credential with no `id` still deserializes — the property is OPTIONAL, and every
1763    /// credential issued before this field existed has none.
1764    #[test]
1765    fn test_missing_id_deserializes_as_none() {
1766        let parsed: DTGCredential = serde_json::from_str(
1767            r#"{
1768              "@context": ["https://www.w3.org/ns/credentials/v2"],
1769              "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1770              "issuer": "did:example:issuer",
1771              "validFrom": "2025-12-11T00:00:00Z",
1772              "credentialSubject": { "id": "did:example:subject" }
1773            }"#,
1774        )
1775        .unwrap();
1776        assert_eq!(parsed.id(), None);
1777    }
1778
1779    /// `set_id` is the in-place form of `with_id`; both write the same property.
1780    #[test]
1781    fn test_set_id_matches_with_id() {
1782        let build = || {
1783            DTGCredential::new_vrc(
1784                "did:example:issuer".to_string(),
1785                "did:example:subject".to_string(),
1786                DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1787                    .unwrap()
1788                    .with_timezone(&Utc),
1789                None,
1790            )
1791        };
1792        let mut in_place = build();
1793        in_place.set_id("urn:uuid:abc");
1794        assert_eq!(
1795            serde_json::to_value(&in_place).unwrap(),
1796            serde_json::to_value(build().with_id("urn:uuid:abc")).unwrap()
1797        );
1798    }
1799
1800    #[test]
1801    fn test_vrc_serialization() {
1802        let vrc = DTGCredential::new_vrc(
1803            "did:example:issuer".to_string(),
1804            "did:example:subject".to_string(),
1805            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1806                .unwrap()
1807                .with_timezone(&Utc),
1808            None,
1809        );
1810
1811        let txt = serde_json::to_string_pretty(&vrc).unwrap();
1812        let sample = r#"{
1813  "@context": [
1814    "https://www.w3.org/ns/credentials/v2",
1815    "https://firstperson.network/credentials/dtg/v1"
1816  ],
1817  "type": [
1818    "VerifiableCredential",
1819    "DTGCredential",
1820    "RelationshipCredential"
1821  ],
1822  "issuer": "did:example:issuer",
1823  "validFrom": "2025-12-11T00:00:00Z",
1824  "credentialSubject": {
1825    "id": "did:example:subject"
1826  }
1827}"#;
1828
1829        assert_eq!(txt, sample);
1830    }
1831
1832    #[test]
1833    fn test_vic_serialization() {
1834        let vic = DTGCredential::new_vic(
1835            "did:example:issuer".to_string(),
1836            "did:example:subject".to_string(),
1837            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1838                .unwrap()
1839                .with_timezone(&Utc),
1840            None,
1841        );
1842
1843        let txt = serde_json::to_string_pretty(&vic).unwrap();
1844        let sample = r#"{
1845  "@context": [
1846    "https://www.w3.org/ns/credentials/v2",
1847    "https://firstperson.network/credentials/dtg/v1"
1848  ],
1849  "type": [
1850    "VerifiableCredential",
1851    "DTGCredential",
1852    "InvitationCredential"
1853  ],
1854  "issuer": "did:example:issuer",
1855  "validFrom": "2025-12-11T00:00:00Z",
1856  "credentialSubject": {
1857    "id": "did:example:subject"
1858  }
1859}"#;
1860
1861        assert_eq!(txt, sample);
1862    }
1863
1864    #[test]
1865    fn test_vpc_serialization() {
1866        let vpc = DTGCredential::new_vpc(
1867            "did:example:issuer".to_string(),
1868            "did:example:subject".to_string(),
1869            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1870                .unwrap()
1871                .with_timezone(&Utc),
1872            None,
1873        );
1874
1875        let txt = serde_json::to_string_pretty(&vpc).unwrap();
1876        let sample = r#"{
1877  "@context": [
1878    "https://www.w3.org/ns/credentials/v2",
1879    "https://firstperson.network/credentials/dtg/v1"
1880  ],
1881  "type": [
1882    "VerifiableCredential",
1883    "DTGCredential",
1884    "PersonaCredential"
1885  ],
1886  "issuer": "did:example:issuer",
1887  "validFrom": "2025-12-11T00:00:00Z",
1888  "credentialSubject": {
1889    "id": "did:example:subject"
1890  }
1891}"#;
1892
1893        assert_eq!(txt, sample);
1894    }
1895
1896    #[test]
1897    fn test_vec_serialization() {
1898        let vec = DTGCredential::new_vec(
1899            "did:example:issuer".to_string(),
1900            "did:example:subject".to_string(),
1901            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1902                .unwrap()
1903                .with_timezone(&Utc),
1904            None,
1905            json!({
1906              "type": "SkillEndorsement",
1907              "name": "Software Development",
1908              "competencyLevel": "expert"
1909            }),
1910        );
1911
1912        let txt = serde_json::to_string_pretty(&vec).unwrap();
1913        let sample = r#"{
1914  "@context": [
1915    "https://www.w3.org/ns/credentials/v2",
1916    "https://firstperson.network/credentials/dtg/v1"
1917  ],
1918  "type": [
1919    "VerifiableCredential",
1920    "DTGCredential",
1921    "EndorsementCredential"
1922  ],
1923  "issuer": "did:example:issuer",
1924  "validFrom": "2025-12-11T00:00:00Z",
1925  "credentialSubject": {
1926    "id": "did:example:subject",
1927    "endorsement": {
1928      "competencyLevel": "expert",
1929      "name": "Software Development",
1930      "type": "SkillEndorsement"
1931    }
1932  }
1933}"#;
1934
1935        assert_eq!(txt, sample);
1936    }
1937
1938    #[test]
1939    fn test_vwc_serialization() {
1940        let vwc = DTGCredential::new_vwc(
1941            "did:example:issuer".to_string(),
1942            "did:example:subject".to_string(),
1943            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1944                .unwrap()
1945                .with_timezone(&Utc),
1946            None,
1947            "thread-abc-123".to_string(),
1948            Some("zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw".to_string()),
1949            Some(WitnessContext {
1950                event: Some("EthDenver 2024".to_string()),
1951                session_id: Some("session-8822-nonce".to_string()),
1952                method: Some("in-person-proximity".to_string()),
1953            }),
1954        );
1955
1956        let txt = serde_json::to_string_pretty(&vwc).unwrap();
1957
1958        let sample = r#"{
1959  "@context": [
1960    "https://www.w3.org/ns/credentials/v2",
1961    "https://firstperson.network/credentials/dtg/v1"
1962  ],
1963  "type": [
1964    "VerifiableCredential",
1965    "DTGCredential",
1966    "WitnessCredential"
1967  ],
1968  "issuer": "did:example:issuer",
1969  "validFrom": "2025-12-11T00:00:00Z",
1970  "taskContext": "thread-abc-123",
1971  "credentialSubject": {
1972    "id": "did:example:subject",
1973    "digestMultibase": "zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw",
1974    "witnessContext": {
1975      "event": "EthDenver 2024",
1976      "sessionId": "session-8822-nonce",
1977      "method": "in-person-proximity"
1978    }
1979  }
1980}"#;
1981
1982        assert_eq!(txt, sample);
1983    }
1984
1985    #[test]
1986    fn test_rcard_serialization() {
1987        let rcard = DTGCredential::new_rcard(
1988            "did:example:issuer".to_string(),
1989            "did:example:subject".to_string(),
1990            DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1991                .unwrap()
1992                .with_timezone(&Utc),
1993            None,
1994            json!([
1995                "vcard",
1996                [
1997                    ["fn", {}, "text", "Alice Smith"],
1998                    ["email", {}, "text", "alice@example.com"]
1999                ]
2000            ]),
2001        );
2002
2003        let txt = serde_json::to_string_pretty(&rcard).unwrap();
2004
2005        let sample = r#"{
2006  "@context": [
2007    "https://www.w3.org/ns/credentials/v2",
2008    "https://firstperson.network/credentials/dtg/v1"
2009  ],
2010  "type": [
2011    "VerifiableCredential",
2012    "DTGCredential",
2013    "RCardCredential"
2014  ],
2015  "issuer": "did:example:issuer",
2016  "validFrom": "2025-12-11T00:00:00Z",
2017  "credentialSubject": {
2018    "id": "did:example:subject",
2019    "card": [
2020      "vcard",
2021      [
2022        [
2023          "fn",
2024          {},
2025          "text",
2026          "Alice Smith"
2027        ],
2028        [
2029          "email",
2030          {},
2031          "text",
2032          "alice@example.com"
2033        ]
2034      ]
2035    ]
2036  }
2037}"#;
2038
2039        assert_eq!(txt, sample);
2040    }
2041}