These workplans exist only in the retired local hub. Their random pre-ADR-007 identifiers are refused by C-06 as stale references, so they cannot be registered. Deriving from the canonical record id takes no identity from anything: central does not hold them and the old ids die with the cache. Records central already holds were deliberately left untouched. Refs CUST-WP-0068-T06 Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
209 lines
7.3 KiB
Markdown
209 lines
7.3 KiB
Markdown
---
|
|
id: AUDIT-WP-0007
|
|
type: workplan
|
|
title: "Integrity verification for operational custody"
|
|
domain: infotech
|
|
repo: audit-core
|
|
status: finished
|
|
owner: grok
|
|
topic_slug: railiance
|
|
created: "2026-08-16"
|
|
updated: "2026-08-16"
|
|
depends_on:
|
|
- AUDIT-WP-0006
|
|
state_hub_workstream_id: "5398f698-4f86-5dd8-962a-e5dbaf93c9f4"
|
|
---
|
|
|
|
# AUDIT-WP-0007 — Integrity verification for operational custody
|
|
|
|
## Goal
|
|
|
|
Make the `operations.audit` integrity claim checkable.
|
|
|
|
AUDIT-WP-0006 made custody honest: the live receiver reports
|
|
`operational`, not `archive`, and cites the 30-day platform backup.
|
|
`tamper_evidence` is still `False`. The append-only trigger rejects
|
|
UPDATE/DELETE for the runtime role, and each row stores `payload_hash`,
|
|
but a database owner can drop the trigger and rewrite rows. Nothing
|
|
would *prove* they had. ITC-CAP hook `integrity_verification` is
|
|
therefore `unknown`.
|
|
|
|
This workplan adds a hash chain on accept, a verify surface that can
|
|
fail, and a chain-head attestation stored outside `platform-pg`. Only
|
|
then may `RetentionPolicy.tamper_evidence` become true, and only for
|
|
what those artefacts actually detect.
|
|
|
|
It does not raise the provision to D5. D5 also needs reliability
|
|
measured and controlled (one replica, no drill cadence). Leave maturity
|
|
at D4 unless those land elsewhere.
|
|
|
|
## Why this is the next improvement
|
|
|
|
`data.archive` still needs a founder decision and a resource-control
|
|
procurement. A second sender expands the product. Integrity is the
|
|
remaining honesty gap on the live provision and is owned here.
|
|
|
|
## Boundaries
|
|
|
|
**This workplan owns**
|
|
|
|
- Per-event hash chain in the Postgres store.
|
|
- A verify CLI and a `may_read` HTTP surface.
|
|
- A chain-head attestation outside `audit_core` on `platform-pg`.
|
|
- Updating `tamper_evidence` and the ITC-CAP `integrity_verification`
|
|
hook only when verify can detect a rewritten payload.
|
|
|
|
**This workplan does not own**
|
|
|
|
- A `data.archive` / WORM object-store sink.
|
|
- Booked cost, `rapp.yaml`, public ingest, Barman/S3.
|
|
- A public transparency log.
|
|
- Raising provision maturity to D5.
|
|
- Replacing the append-only trigger (it stays).
|
|
|
|
## T01 — Specify the chain and the proof bound
|
|
|
|
```task
|
|
id: AUDIT-WP-0007-T01
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "ea526d34-57e7-5273-b80b-6cd7bfe3bc1f"
|
|
```
|
|
|
|
Write a short contract in `docs/audit-backend-contract.md` (or a sibling
|
|
`docs/integrity.md`) that states:
|
|
|
|
1. Each accepted event carries `payload_hash` (already stored) and a
|
|
new `chain_hash` = H(previous `chain_hash` || `payload_hash` ||
|
|
`event_id`). First event uses a documented genesis value.
|
|
2. The chain is per schema / database, not per tenant. Tenant splits
|
|
would hide a cross-tenant rewrite.
|
|
3. `verify` walks accepted_at/chain order, recomputes, and reports the
|
|
first break. A break is a custody defect, not a sender error.
|
|
4. Proof bound: a chain inside the same database detects a rewritten
|
|
payload *if the attacker does not also recompute the suffix*. A
|
|
database owner can. Tamper evidence against that class of attacker
|
|
requires a chain-head attestation stored outside `platform-pg`.
|
|
5. `tamper_evidence=True` is allowed only when both verify and an
|
|
external head exist. It still does not mean WORM or `data.archive`.
|
|
|
|
Done when the contract is written and the tests to be added are named.
|
|
|
|
Done 2026-08-16: `docs/integrity.md` names the chain, genesis, proof bound,
|
|
and the six tests.
|
|
|
|
## T02 — Persist the chain on accept
|
|
|
|
```task
|
|
id: AUDIT-WP-0007-T02
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "649a60b1-e8a7-59f5-9164-24c57290e163"
|
|
```
|
|
|
|
Add an additive migration (0006) for `chain_hash` (and `chain_prev` if
|
|
the walk needs it). Backfill existing rows in accept-order so a live
|
|
database with the AUDIT-WP-0005/0006 events verifies after migrate.
|
|
|
|
`accept` must write the next link in the same transaction as the
|
|
idempotent insert. A duplicate accept must not mint a second link.
|
|
A conflict must not advance the head.
|
|
|
|
SQLite (dev) should implement the same chain so conformance stays one
|
|
suite. Mock file backend stays `tamper_evidence=False`.
|
|
|
|
Done when conformance tests show: first accept sets genesis; second
|
|
links; duplicate does not fork; verify is clean on a fresh store.
|
|
|
|
Done 2026-08-16: migration 0006 plus backfill; SQLite and Postgres accept
|
|
write the next link under a lock. Conformance + `tests/test_integrity.py`.
|
|
|
|
## T03 — Verify surface
|
|
|
|
```task
|
|
id: AUDIT-WP-0007-T03
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "71b7faeb-1f70-5162-8ce1-3c3bbe849db1"
|
|
```
|
|
|
|
- CLI: `python -m audit_core verify-chain` (or equivalent) exits 0
|
|
only if the walk is intact. Print first-break event id on failure.
|
|
- HTTP: a `may_read` route (suggested `GET /v1/integrity`) returns
|
|
`{intact, events, head, first_break}` without event payloads.
|
|
|
|
A test must break a payload_hash (or chain_hash) using a superuser
|
|
connection and show verify fails. That is the evidence the trigger
|
|
alone never gave us.
|
|
|
|
Done when CLI and HTTP agree, and the broken-row test is red-then-green
|
|
as a detector, not as a repair.
|
|
|
|
Done 2026-08-16: `verify-chain` CLI, `GET /v1/integrity`, SQLite rewrite
|
|
test and Postgres superuser rewrite test both fail verify.
|
|
|
|
## T04 — Attest the chain head outside platform-pg
|
|
|
|
```task
|
|
id: AUDIT-WP-0007-T04
|
|
status: done
|
|
priority: medium
|
|
state_hub_task_id: "3b4d6b4e-1f72-5bc7-bf24-ca344caceaf6"
|
|
```
|
|
|
|
Publish `{chain_hash, event_id, accepted_at, event_count, observed_at}`
|
|
to a location that is not a table in `audit_core` on `platform-pg`.
|
|
First implementation: an operator command that writes a dated evidence
|
|
file, plus a documented second copy (for example the existing
|
|
logical-offsite / Nextcloud path used by RESOURCE-WP-0002-T06 — cite
|
|
it, do not invent a new bucket).
|
|
|
|
Do not write the head into the Barman prefix. That copy dies with the
|
|
same restore as the table.
|
|
|
|
Verify must be able to compare the live head to a cited attestation
|
|
and report mismatch as a break.
|
|
|
|
Done when one production walk produces an attestation file in
|
|
`docs/evidence/` and verify-against-attestation is tested.
|
|
|
|
Done 2026-08-16: live walk 30 events, intact, head
|
|
`03fd1773…`. Evidence
|
|
`docs/evidence/chain-head-20260816.json`. Verify-against matched.
|
|
Do not copy this file into the Barman prefix; a second copy may follow
|
|
RESOURCE-WP-0002-T06 logical-offsite.
|
|
|
|
## T05 — Declare tamper_evidence only as far as the proof
|
|
|
|
```task
|
|
id: AUDIT-WP-0007-T05
|
|
status: done
|
|
priority: medium
|
|
state_hub_task_id: "f98fe8c8-637e-5e7d-bc3a-b2bedfc7525b"
|
|
```
|
|
|
|
After T03 and T04:
|
|
|
|
- Postgres `RetentionPolicy.tamper_evidence` becomes `True`.
|
|
- `/readyz` may report it. Do not change `custody_class` back to
|
|
`archive`.
|
|
- Update `data/capability/audit-core-operational.json`:
|
|
`integrity_verification` basis `measured`, pointing at the broken-row
|
|
test and the attestation walk. Leave maturity at D4. Keep
|
|
`data.archive` unmet.
|
|
- Refresh the reuse-surface card and operator runbook (how to verify,
|
|
what a break means, that reboot unreadiness is unrelated).
|
|
|
|
Done when capability-review still `ok` and the hook is no longer
|
|
`unknown`.
|
|
|
|
Done 2026-08-16: Postgres `tamper_evidence=True`; live `/readyz` reports
|
|
it; capability-review `ok`; `integrity_verification` is `measured`.
|
|
Maturity stays D4. `data.archive` stays unmet.
|
|
|
|
## Acceptance
|
|
|
|
- A rewritten stored payload makes `verify` fail.
|
|
- A chain-head attestation exists outside `platform-pg`.
|
|
- `tamper_evidence` is true only after those two exist.
|
|
- No `data.archive` sink, no D5 inflation, no new cost stream.
|