|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | += Provenance Threat Model |
| 4 | +Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 5 | +:toc: left |
| 6 | +:toclevels: 3 |
| 7 | +:icons: font |
| 8 | +:source-highlighter: rouge |
| 9 | + |
| 10 | +This document specifies what verisimiser's provenance hash chain |
| 11 | +actually proves, in what threat model, with what assumptions. It is |
| 12 | +the binding reference for the Step 2 implementation issues |
| 13 | +(V-L2-C1 … V-L2-L2) and is cited from each of them. |
| 14 | + |
| 15 | +The chain is a SHA-256 hash chain over `ProvenanceEntry` rows |
| 16 | +(`src/abi/mod.rs`). It is *not* a blockchain — there is no consensus, |
| 17 | +no proof-of-work, no distributed timestamp authority — but it does |
| 18 | +provide *append-only, tamper-evident* semantics under the conditions |
| 19 | +documented below. |
| 20 | + |
| 21 | +== 1. Adversary models |
| 22 | + |
| 23 | +Each subsequent section is parameterised by which adversary we are |
| 24 | +defending against. The four models are mutually distinct in |
| 25 | +capability; in practice a real attacker may sit between two of them. |
| 26 | + |
| 27 | +[cols="1,3,2"] |
| 28 | +|=== |
| 29 | +| Name | Capability | Typical real-world correspondent |
| 30 | + |
| 31 | +| **Read-only** |
| 32 | +| Can read both target database and sidecar (provenance log, |
| 33 | + temporal versions, lineage). Cannot write either. |
| 34 | +| Compromised read replica; off-host backup reader; auditor with |
| 35 | + least-privilege account; lawful disclosure recipient. |
| 36 | + |
| 37 | +| **Sidecar-append** |
| 38 | +| Can append new rows to `verisimdb_provenance_log` but cannot |
| 39 | + delete or rewrite existing rows. (Realistic in append-only |
| 40 | + configurations: filesystem-level WORM, immutable cron-driven |
| 41 | + ingest, or a `BEFORE UPDATE/DELETE` trigger that refuses.) |
| 42 | +| Misbehaving instrumentation library; faulty sidecar-writer cron; |
| 43 | + malicious extension to the application that can call the writer |
| 44 | + API but cannot bypass the WORM layer. |
| 45 | + |
| 46 | +| **Sidecar-rewrite** |
| 47 | +| Can rewrite arbitrary rows in the provenance log, in any order, |
| 48 | + with any contents. The integrity store (sidecar) is fully under |
| 49 | + the adversary's control. |
| 50 | +| Root on the sidecar host; compromised application running with |
| 51 | + sidecar write privilege; disk swap / device theft. |
| 52 | + |
| 53 | +| **Clock-skew** |
| 54 | +| Orthogonal to the above. Can submit writes (via the legitimate |
| 55 | + writer path) with timestamps drawn from a clock that does not |
| 56 | + agree with monotonic UTC. May be combined with any of the three |
| 57 | + above. |
| 58 | +| Untrusted NTP source; container with overridden clock; mobile |
| 59 | + client; multi-region writer with clock drift. |
| 60 | +|=== |
| 61 | + |
| 62 | +== 2. What the chain proves |
| 63 | + |
| 64 | +In all cases below, the chain's claims are conditional on the |
| 65 | +assumptions in section 4. "Protected" means tampering produces a |
| 66 | +hash that fails `ProvenanceEntry::verify()` (per V-L2-C1, the |
| 67 | +preimage is domain-separated and length-prefixed over all seven |
| 68 | +data fields). |
| 69 | + |
| 70 | +=== 2.1 Read-only adversary |
| 71 | + |
| 72 | +Trivially: the adversary makes no writes, so nothing they observe |
| 73 | +can cause the chain to fail to verify. The chain is intact by |
| 74 | +construction; the adversary can read it. |
| 75 | + |
| 76 | +* **Detectable**: nothing (they did nothing detectable). |
| 77 | +* **Defeated**: by `BEGIN TRANSACTION ... COMMIT` reads against |
| 78 | + the sidecar; no replay attack is even meaningful here. |
| 79 | + |
| 80 | +This model exists primarily to constrain *what we publish*: the |
| 81 | +sidecar is readable, so any field stored in it is exposed. |
| 82 | +`before_snapshot` may contain redacted data; access-control policy |
| 83 | +(`verisimdb_access_policies`) is the boundary against unauthorised |
| 84 | +read. |
| 85 | + |
| 86 | +=== 2.2 Sidecar-append adversary |
| 87 | + |
| 88 | +The adversary can write new rows. They cannot delete or rewrite |
| 89 | +existing rows, so the existing chain is intact. |
| 90 | + |
| 91 | +* **Protected**: every historical entry. Existing |
| 92 | + `previous_hash` pointers cannot be altered, so the chain spine |
| 93 | + from genesis to the most recent legitimate entry is verifiable. |
| 94 | +* **Not protected**: the adversary can append entirely new entries |
| 95 | + with whatever `actor`, `operation`, `before_snapshot`, |
| 96 | + `transformation` they choose, *including chained to the existing |
| 97 | + tip*. Their writes will have the right `previous_hash` and will |
| 98 | + `verify()` true individually. |
| 99 | +* **Defeated**: by out-of-band actor authentication. The chain |
| 100 | + records *what was claimed*; the adversary can claim |
| 101 | + `actor="alice"`. Pairing the chain with a signature on each |
| 102 | + entry (e.g. SSH commit signing equivalent over `entry.hash`) is |
| 103 | + the standard fix. **Not currently implemented.** See open |
| 104 | + question OQ-1. |
| 105 | + |
| 106 | +=== 2.3 Sidecar-rewrite adversary |
| 107 | + |
| 108 | +The adversary can rewrite arbitrary rows. |
| 109 | + |
| 110 | +* **Protected**: nothing locally. Every row can be replaced; the |
| 111 | + hash chain can be entirely reconstructed from genesis with new |
| 112 | + content and will verify just as well. The chain provides zero |
| 113 | + protection against an adversary with write access to the |
| 114 | + sidecar. |
| 115 | +* **Defeated**: only by an *external* anchor. The standard |
| 116 | + anchors are (a) periodic publication of the chain's tip hash |
| 117 | + to a remote append-only log (e.g. transparency log, |
| 118 | + certificate transparency, signed-timestamp service); or |
| 119 | + (b) per-entry signatures backed by a key whose authority is |
| 120 | + not under the adversary's control. **Neither is currently |
| 121 | + implemented.** See OQ-2. |
| 122 | + |
| 123 | +=== 2.4 Clock-skew adversary |
| 124 | + |
| 125 | +The adversary submits writes through the legitimate path but with |
| 126 | +manipulated timestamps. |
| 127 | + |
| 128 | +* **Protected**: ordering is determined by the `previous_hash` |
| 129 | + chain spine, not by `timestamp`. A backdated or future-dated |
| 130 | + entry will still chain correctly *to whatever predecessor it |
| 131 | + claims*; reordering is therefore prevented even under clock |
| 132 | + skew, *given* the predecessor was honestly chosen. |
| 133 | +* **Not protected**: the absolute time recorded in `timestamp`. |
| 134 | + V-L2-C2 hashes the timestamp canonically so the recorded value |
| 135 | + is tamper-evident, but the recorded value can still be a lie |
| 136 | + the writer chose at write time. |
| 137 | +* **Defeated**: trust a single monotonic clock source (NTP from a |
| 138 | + trusted root, hardware clock with attestation) and refuse |
| 139 | + writes whose timestamp is more than ±N seconds from the |
| 140 | + receiving host's clock. **Not currently implemented.** See OQ-3. |
| 141 | + |
| 142 | +== 3. Out of scope |
| 143 | + |
| 144 | +These threats are *not* defended by the provenance chain and any |
| 145 | +claim to the contrary is incorrect: |
| 146 | + |
| 147 | +* **Denial of service.** An adversary who fills the sidecar |
| 148 | + disk, or who deletes the entire sidecar file, defeats the |
| 149 | + chain. The chain produces no signal in that case. |
| 150 | +* **Side channels.** Timing, memory, network metadata, query |
| 151 | + cardinality. The chain records what was written; it does not |
| 152 | + obscure access patterns. |
| 153 | +* **Target-DB tampering not routed through the writer.** |
| 154 | + VeriSimiser intercepts modifications through the configured |
| 155 | + interception path (`pg_notify`, `sqlite3_update_hook`, |
| 156 | + change streams, application middleware — see README). |
| 157 | + Modifications that bypass interception — direct file-system |
| 158 | + writes to the target's storage, restore from backup, schema |
| 159 | + migration outside the controlled path — are invisible to |
| 160 | + the chain. |
| 161 | +* **Retroactive provenance.** Data that existed before |
| 162 | + verisimiser was attached has no provenance row. The chain |
| 163 | + starts at the genesis entry; rows that predate the genesis |
| 164 | + cannot be vouched for. |
| 165 | +* **Identity binding.** The chain records `actor` as a string; |
| 166 | + it does not authenticate that the string corresponds to the |
| 167 | + real-world entity it names. See sidecar-append, above. |
| 168 | +* **Confidentiality.** Every field in the entry is stored in |
| 169 | + plaintext in the sidecar. |
| 170 | + |
| 171 | +== 4. Assumptions |
| 172 | + |
| 173 | +* **Sidecar locality.** The sidecar runs on the same host as |
| 174 | + the target database. Cross-host or remote sidecar deployments |
| 175 | + introduce a network-write surface that the threat model does |
| 176 | + not cover. |
| 177 | +* **Append-only storage (optional).** Sidecar-append's |
| 178 | + protection assumes the sidecar storage refuses |
| 179 | + `UPDATE`/`DELETE` on `verisimdb_provenance_log` at a layer |
| 180 | + the application cannot bypass. Without this, sidecar-append |
| 181 | + collapses into sidecar-rewrite. |
| 182 | +* **Hash algorithm.** SHA-256 is preimage-resistant and |
| 183 | + collision-resistant for the foreseeable future. A migration |
| 184 | + to a different algorithm requires bumping the domain tag |
| 185 | + (currently `b"verisim-prov-v1\0"`); see V-L2-C1. |
| 186 | +* **Clock source.** `chrono::Utc::now()` reads the system |
| 187 | + monotonic clock. The clock-skew adversary section assumes |
| 188 | + no defence against a lying clock; "trusted clock" is an |
| 189 | + out-of-band assumption a deployment may add. |
| 190 | +* **Single writer.** No multi-writer reconciliation. If two |
| 191 | + concurrent writers both chain to the current tip, only the |
| 192 | + first commit survives; the second errors at insert time |
| 193 | + (FK / chain-position contention). |
| 194 | + |
| 195 | +== 5. Open questions |
| 196 | + |
| 197 | +Each is a deliberate non-decision; an ADR resolves the choice. |
| 198 | + |
| 199 | +* **OQ-1: Per-entry signatures?** Do we want a detached |
| 200 | + signature column (e.g. `signature TEXT`) over `entry.hash` |
| 201 | + using a per-actor key, defending sidecar-append against |
| 202 | + forged `actor`? Trade-off: PKI overhead vs. forensic |
| 203 | + integrity. Suggested follow-up: ADR-0004. |
| 204 | +* **OQ-2: External anchoring?** Periodic publication of the |
| 205 | + chain tip to a remote append-only log (TUF, Sigsum, |
| 206 | + certificate transparency). Trade-off: anchor latency and |
| 207 | + external dependency vs. defence against sidecar-rewrite. |
| 208 | + Suggested follow-up: ADR-0005. |
| 209 | +* **OQ-3: Trusted clock policy?** Refuse writes whose |
| 210 | + timestamp drifts more than ±N seconds from the local |
| 211 | + monotonic clock. Trade-off: replicated / mobile writers |
| 212 | + break vs. clock-skew defence. Suggested follow-up: |
| 213 | + ADR-0006. |
| 214 | +* **OQ-4: Snapshot redaction.** `before_snapshot` may carry |
| 215 | + PII. Should the writer hash a *digest* of the snapshot |
| 216 | + rather than the snapshot itself, with the plaintext kept |
| 217 | + in a separately access-controlled store? Trade-off: storage |
| 218 | + vs. lawful-disclosure granularity. Suggested follow-up: |
| 219 | + ADR-0007. |
| 220 | + |
| 221 | +== Cross-references |
| 222 | + |
| 223 | +This document is cited by V-L2-C1 through V-L2-C4 (which |
| 224 | +implement the field-coverage and canonical-timestamp choices), |
| 225 | +V-L2-L1 / V-L2-L2 (which constrain chain forks at the storage |
| 226 | +layer), and V-L1-C1 / V-L1-C2 (which build the actual write |
| 227 | +pipeline that produces the entries this model talks about). |
| 228 | + |
| 229 | +The implementation lives in `src/abi/mod.rs::ProvenanceEntry`; |
| 230 | +the storage schema in `src/codegen/overlay.rs::generate_provenance_table`; |
| 231 | +and the integration tests in |
| 232 | +`tests/integration_test.rs::test_provenance_chain_integrity_multi_step`. |
0 commit comments