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}