Skip to main content

qualia_client_core/
disclosure_trace.rs

1//! **Disclosure traceability** — so a *betrayal is knowable and attributable*.
2//!
3//! The high-stakes case (Timothy, 2026-07-06): a person seeking protection from serious crime "**cc**"s a
4//! **transparency credential** to an oversight authority — their local member of parliament, a minister — to
5//! get help / put them on notice. But the perpetrator may be a **PEP** (politically-exposed person) or a
6//! political **donor** with influence over that very authority. If the authority — **or their staff** — leaks
7//! the disclosure to the perpetrator, threats follow. The system must make that **knowable**: *who* accessed
8//! or was told *what*, *when*, under *which* credential, and — crucially — by *whom* (including a **delegate**
9//! such as an MP's staffer acting under the MP's credential), with a per-recipient **fingerprint** so a
10//! leaked copy (or leaked knowledge) traces back to its source.
11//!
12//! This is the anti-corruption / anti-retaliation substrate for the **knowledge economy** around protection —
13//! and it is *particularly* load-bearing for **UN / World-Bank development-funding** and **human-rights
14//! support** use-cases, where beneficiaries and whistle-blowers face capture and reprisal, and where "who
15//! knew, and who told" must survive powerful actors' attempts to hide it.
16//!
17//! It composes with [`crate::consent_credential`]: disclosures are of the durable, un-deletable
18//! `EncryptedCommonsPayload` (so the *trace itself cannot be erased* by a betrayer), each carrying the
19//! payload's commitment. The real per-recipient watermark / traitor-tracing scheme and the tamper-evident
20//! store (signed WAL + commons) are the crypto/storage composition (coordinate); this is the domain model +
21//! the invariants: **the trace makes the leak knowable, and attributable to a specific actor.**
22
23use serde::{Deserialize, Serialize};
24
25use crate::consent_credential::PayloadCommitment;
26
27/// A per-recipient **tracing fingerprint** — a unique tag bound to *one disclosure to one party*, so a
28/// leaked copy (or knowledge recovered from a leak) can be traced to whose disclosure it came from. The real
29/// mechanism is per-recipient watermarking / traitor-tracing; here it is the tag the trace keys on.
30pub type DisclosureFingerprint = [u8; 16];
31
32/// What kind of disclosure an event records.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum DisclosureKind {
36    /// The recipient (or their delegate) accessed the payload directly under their credential.
37    DirectAccess,
38    /// The recipient (or their delegate) **shared it onward** to another party — recorded, so the chain of
39    /// who-told-whom is traced (this is how an onward leak becomes visible).
40    OnwardShare { to_did: String },
41}
42
43/// One traced access / disclosure event — **durable and tamper-evident** (of the commons payload; a person
44/// revoking access, or a betrayer, cannot erase it). Says *who* was given/took access to *what*, *when*,
45/// under *which* credential, and *by whom* (including a delegate).
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct DisclosureEvent {
48    pub id: String,
49    /// The commons payload disclosed (by commitment).
50    pub payload_commitment: PayloadCommitment,
51    /// Under which consent / transparency credential the disclosure occurred.
52    pub credential_id: String,
53    /// The party the disclosure was **to** / who holds the credential (e.g. the MP / minister).
54    pub recipient_did: String,
55    /// If a **delegate** actually acted under the credential (e.g. the MP's staffer), their identifier.
56    /// `None` = the recipient themselves acted. **This is what makes a staff leak attributable.**
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub acting_delegate_did: Option<String>,
59    pub time_unix: u64,
60    /// The per-recipient tracing fingerprint bound to this disclosure.
61    pub fingerprint: DisclosureFingerprint,
62    pub kind: DisclosureKind,
63}
64
65impl DisclosureEvent {
66    /// The **actor accountable** for this disclosure: the acting delegate (a staffer) if one acted, else the
67    /// recipient (the authority) themselves. A leak traced to this event is attributable to this actor.
68    pub fn accountable_actor(&self) -> &str {
69        self.acting_delegate_did
70            .as_deref()
71            .unwrap_or(&self.recipient_did)
72    }
73}
74
75/// A "**cc**" / transparency credential note — the record that the person **informed an oversight authority**
76/// (MP / minister) for transparency / protection. The record is itself protective and **durable**: "I
77/// informed them on date X for purpose Y" is provable, so if the authority betrays or fails to act, that is
78/// knowable *against this record*.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct TransparencyCc {
81    /// The credential that carries the disclosure (links to the access + the trace).
82    pub credential_id: String,
83    /// The authority informed (the MP / minister).
84    pub informed_authority_did: String,
85    /// Why they were informed (e.g. "protection from serious crime").
86    pub purpose: String,
87    pub informed_unix: u64,
88}
89
90/// Every disclosure of a given payload, in order — the **audit chain**: who could have leaked this, and by
91/// which route (direct access vs onward share). Tamper-evident; survives revocation.
92pub fn disclosure_chain<'a>(
93    events: &'a [DisclosureEvent],
94    commitment: &PayloadCommitment,
95) -> Vec<&'a DisclosureEvent> {
96    events
97        .iter()
98        .filter(|e| &e.payload_commitment == commitment)
99        .collect()
100}
101
102/// The distinct actors who had access to a payload (recipients + any acting delegates) — the set the leak
103/// **must** be within. If the perpetrator demonstrably knows something disclosed only here, the leak is one
104/// of these.
105pub fn actors_with_access<'a>(
106    events: &'a [DisclosureEvent],
107    commitment: &PayloadCommitment,
108) -> Vec<&'a str> {
109    let mut out: Vec<&str> = Vec::new();
110    for e in events
111        .iter()
112        .filter(|e| &e.payload_commitment == commitment)
113    {
114        for actor in [
115            Some(e.recipient_did.as_str()),
116            e.acting_delegate_did.as_deref(),
117        ]
118        .into_iter()
119        .flatten()
120        {
121            if !out.contains(&actor) {
122                out.push(actor);
123            }
124        }
125    }
126    out
127}
128
129/// Trace a leak by its fingerprint — the disclosure it came from — making the betrayal **knowable**. A
130/// leaked copy (or knowledge recovered from a leak) carrying `leaked` is matched to the exact disclosure, and
131/// thence to the [`accountable_actor`](DisclosureEvent::accountable_actor) (the authority, or their staffer).
132pub fn trace_leak<'a>(
133    events: &'a [DisclosureEvent],
134    leaked: &DisclosureFingerprint,
135) -> Option<&'a DisclosureEvent> {
136    events.iter().find(|e| &e.fingerprint == leaked)
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    const C: PayloadCommitment = [9u8; 32];
144    const FP_MP: DisclosureFingerprint = [1u8; 16];
145    const FP_STAFF: DisclosureFingerprint = [2u8; 16];
146    const FP_MINISTER: DisclosureFingerprint = [3u8; 16];
147
148    fn ev(
149        id: &str,
150        recipient: &str,
151        delegate: Option<&str>,
152        fp: DisclosureFingerprint,
153        kind: DisclosureKind,
154    ) -> DisclosureEvent {
155        DisclosureEvent {
156            id: id.into(),
157            payload_commitment: C,
158            credential_id: "cc-transparency".into(),
159            recipient_did: recipient.into(),
160            acting_delegate_did: delegate.map(|d| d.into()),
161            time_unix: 1_000,
162            fingerprint: fp,
163            kind,
164        }
165    }
166
167    #[test]
168    fn a_transparency_cc_records_that_the_authority_was_informed() {
169        let cc = TransparencyCc {
170            credential_id: "cc-transparency".into(),
171            informed_authority_did: "did:wf:mp-smith".into(),
172            purpose: "protection from serious crime".into(),
173            informed_unix: 1_000,
174        };
175        // "I informed them on date X for purpose Y" is provable — the protective record.
176        assert_eq!(cc.informed_authority_did, "did:wf:mp-smith");
177        assert_eq!(cc.purpose, "protection from serious crime");
178    }
179
180    #[test]
181    fn a_leak_is_traceable_to_its_source_and_attributable_to_the_actor() {
182        // The person cc's the MP; the MP's disclosure and the minister's each carry a distinct fingerprint.
183        let events = vec![
184            ev(
185                "d1",
186                "did:wf:mp-smith",
187                None,
188                FP_MP,
189                DisclosureKind::DirectAccess,
190            ),
191            ev(
192                "d2",
193                "did:wf:minister",
194                None,
195                FP_MINISTER,
196                DisclosureKind::DirectAccess,
197            ),
198        ];
199        // A leaked copy carrying the MP's fingerprint surfaces near the perpetrator → traced to the MP.
200        let src = trace_leak(&events, &FP_MP).expect("leak traced");
201        assert_eq!(src.recipient_did, "did:wf:mp-smith");
202        assert_eq!(
203            src.accountable_actor(),
204            "did:wf:mp-smith",
205            "attributable to the MP"
206        );
207    }
208
209    #[test]
210    fn a_staff_leak_is_attributed_to_the_staffer_not_only_the_authority() {
211        // The MP's STAFFER accessed under the MP's credential and leaked. The trace attributes it to the
212        // staffer (the accountable actor) — the "or their staff" case.
213        let events = vec![ev(
214            "d1",
215            "did:wf:mp-smith",
216            Some("did:wf:mp-staffer-jones"),
217            FP_STAFF,
218            DisclosureKind::DirectAccess,
219        )];
220        let src = trace_leak(&events, &FP_STAFF).unwrap();
221        assert_eq!(
222            src.recipient_did, "did:wf:mp-smith",
223            "under the MP's credential"
224        );
225        assert_eq!(
226            src.accountable_actor(),
227            "did:wf:mp-staffer-jones",
228            "attributable to the specific staffer who acted"
229        );
230    }
231
232    #[test]
233    fn onward_sharing_is_recorded_so_the_chain_is_traced() {
234        // The staffer shares onward to the perpetrator — recorded, so the route is visible.
235        let events = vec![
236            ev(
237                "d1",
238                "did:wf:mp-smith",
239                Some("did:wf:mp-staffer-jones"),
240                FP_STAFF,
241                DisclosureKind::DirectAccess,
242            ),
243            ev(
244                "d2",
245                "did:wf:mp-smith",
246                Some("did:wf:mp-staffer-jones"),
247                [4u8; 16],
248                DisclosureKind::OnwardShare {
249                    to_did: "did:wf:perpetrator-pep".into(),
250                },
251            ),
252        ];
253        let chain = disclosure_chain(&events, &C);
254        assert_eq!(chain.len(), 2);
255        // The onward share to the perpetrator is on the record, attributed to the staffer.
256        let onward = chain
257            .iter()
258            .find(|e| matches!(&e.kind, DisclosureKind::OnwardShare { to_did } if to_did == "did:wf:perpetrator-pep"))
259            .expect("onward share recorded");
260        assert_eq!(onward.accountable_actor(), "did:wf:mp-staffer-jones");
261    }
262
263    #[test]
264    fn the_leak_set_is_bounded_to_who_had_access() {
265        // If the perpetrator knows something disclosed only to these actors, the leak is one of them.
266        let events = vec![
267            ev(
268                "d1",
269                "did:wf:mp-smith",
270                Some("did:wf:mp-staffer-jones"),
271                FP_STAFF,
272                DisclosureKind::DirectAccess,
273            ),
274            ev(
275                "d2",
276                "did:wf:minister",
277                None,
278                FP_MINISTER,
279                DisclosureKind::DirectAccess,
280            ),
281        ];
282        let actors = actors_with_access(&events, &C);
283        assert!(actors.contains(&"did:wf:mp-smith"));
284        assert!(actors.contains(&"did:wf:mp-staffer-jones"));
285        assert!(actors.contains(&"did:wf:minister"));
286        assert_eq!(actors.len(), 3, "the bounded set the leak must be within");
287    }
288
289    #[test]
290    fn serde_round_trips() {
291        let e = ev(
292            "d1",
293            "did:wf:mp",
294            Some("did:wf:staff"),
295            FP_STAFF,
296            DisclosureKind::DirectAccess,
297        );
298        let back: DisclosureEvent =
299            serde_json::from_str(&serde_json::to_string(&e).unwrap()).unwrap();
300        assert_eq!(e, back);
301    }
302}