Trusty: provenance library workspace (initial import)

This commit is contained in:
2026-09-09 18:31:00 -04:00
commit 693f3bef1e
73 changed files with 26295 additions and 0 deletions

296
docs/threat-model.md Normal file
View File

@@ -0,0 +1,296 @@
# Trusty threat model
What trusty detects, what it does not detect, what the trust boundaries
are, and how the system behaves under key compromise. Read alongside
`docs/payload-v2.md` for the on-wire format details this doc references.
This is a living document. If you add a primitive (new condition, new
signature algorithm, new break reason) that changes any property here, this
file gets updated in the same session.
---
## 1. Assets under protection
| Asset | What it represents |
|-----------------------|------------------------------------------------------------------|
| Chain integrity | The append-only link from genesis through every row's `prev_hash`. |
| Row authenticity (v2) | The claim that a specific signer endorsed a specific row payload. |
| Event history | That every mutation -- benign or adversarial -- was recorded. |
| External anchors | SHA-256 `anchor_hash` values suitable for publication / notary. |
Trusty is an **integrity and provenance** layer. It is not a confidentiality
layer, not an availability layer, and not an access-control layer.
---
## 2. Adversary model
We consider an attacker who has one or more of:
- **Read access to the SQLite file** (or the underlying bytes).
- **Write access to the SQLite file** (can modify rows, re-order, delete,
insert fabricated rows).
- **Control of the upstream source** feeding `data` into `append()`.
- **Control of `detected_at` / `received_at` values** when submitting.
- **Possession of one or both cryptographic keys** -- the 16-byte SipHash
`SecretKey` used for `row_hash`, and/or the Ed25519 signing key used for
v2 signatures.
We do not consider:
- Side-channel attacks on SipHash / Ed25519 implementations. The underlying
crates (`siphasher`, `ed25519-dalek`) are treated as correct.
- Compromise of the host OS kernel or memory bus -- below the trust
boundary.
- Quantum-era attacks on Ed25519. Out of scope; algorithm agility is
reserved for a future version bump.
---
## 3. The chain-break-as-event primitive
Trusty's central design choice differs from most provenance systems: when a
break condition fires, trusty **records the break as a row** (`event_type =
"BREAK"`, `break_reason` set) and **starts a new chain segment** from
genesis for the next append. It does not refuse the write and does not
silently repair.
Implications for the threat model:
- Tampering cannot erase its own trace. Even an attacker with write access
who corrupts a row triggers a BREAK at the next honest append, which is
itself an auditable event.
- Verification after the fact surfaces the break: `verify_links` returns
`ok = true` for a chain with BREAK markers (they are valid structural
separators), but the presence of BREAK rows is itself the signal.
Consumers of verify reports MUST inspect BREAK markers and their
`break_reason`, not treat a passing verify as clean history.
- An attacker who controls writes AND holds both keys can forge an entire
new segment that looks benign. BREAK markers detect tampering only when
a subsequent honest append occurs. See Section 6 for the defense.
---
## 4. Detection surfaces
### 4.1 Chain structural checks (`verify_links`)
| Surface | Detects | Miss condition |
|-----------------------|----------------------------------------------------------|----------------------------------------------------------------|
| `PrevHashMismatch` | Row reordering, deletion mid-chain, fabricated row with wrong link. | Attacker re-stitches prev_hash and also forges row_hash (requires SecretKey). |
| `RowHashMismatch` | In-place payload mutation without re-hashing. | Attacker recomputes row_hash (requires SecretKey). |
| `AnchorHashMismatch` | Tampering with stored anchor when row_hash/entity/chain is intact. | Attacker recomputes anchor (requires access to entity_id + chain_id only, no key). |
| `MissingRowHash` / `MissingRawPayload` | Rows with structural fields dropped. | None at this layer. |
### 4.2 Break conditions (at append time, `crates/trusty-core/src/conditions.rs`)
| Condition | Fires on | `BreakReason` |
|---------------------|--------------------------------------------------------------------------|--------------------------------|
| `HashIntegrity` | Stored tip hash != computed chain-tip hash (pre-append). | `TAMPER_DETECTED` |
| `SequenceContinuity`| `sequence` is not `last_sequence + 1`. | `SEQUENCE_GAP` |
| `SourceDomain` (opt-in) | `source_url` not in configured trusted-domains list, `hard = true`. | `SOURCE_CHANGE` |
First failing condition wins; remaining conditions do not run. Custom
conditions compose via the `Condition` trait.
### 4.3 v2 signature checks (`verify_row_signatures`)
Only applied when `payload_version >= 2` and `signatures` is non-empty.
| Surface | Detects |
|-------------------------|-------------------------------------------------------------------|
| `SignatureInvalid` | Signature does not verify against the reconstructed signing view. |
| `SignatureMalformed` | Signature entry not a JSON object; pk/sig hex decode failure. |
| `UnknownSignatureAlg` | `alg` field is not `ed25519` (only algorithm supported today). |
### 4.4 Cross-check KV store (`trusty-sqlite`)
`trusty-sqlite` mirrors `(anchor_hash -> row_id)` into a `hash_index` table
separate from the main `provenance_envelope` table. A modified anchor on
the primary row is caught when verification walks the cross-check.
Byte-layout matches the Python reference `cross_check_kv_store`.
---
## 5. Ed25519 trust boundary (v2)
The v2 signing-view algorithm establishes a specific trust relationship:
- **Signer claims**: "at the time of signing, this `{entity_id, chain_id,
sequence, prev_hash, data, detected_at, received_at, source_url,
extraction_method, hash_alg, payload_version}` tuple was true." The
signature covers the **signing view** (canonical bytes with
`signatures=[]`).
- **Signer does not claim** row_hash correctness. row_hash is produced by
the infrastructure holding the SipHash `SecretKey`, which may be a
different party.
- **Infrastructure claims** (via `SecretKey`): "I received these exact bytes
and they chain from the previous tip." Infrastructure cannot forge the
Ed25519 signature without the signing key.
This split is deliberate: it lets data-producing entities sign with
hardware-bound Ed25519 keys while the trusty storage layer runs with a
commodity SipHash key. Compromising one key is not enough to silently
rewrite history. See Section 6.
### 5.1 Row-hash-commits-to-signature property
`row_hash = SipHash(SecretKey, canonical_bytes_with_signature_spliced_in)`.
Therefore row_hash is a function of the signature bytes themselves.
Tampering strategies and their failure modes:
| Attacker action | What fires |
|-----------------------------------------------|---------------------------------------------|
| Change sig bytes, keep row_hash unchanged. | `RowHashMismatch`. |
| Change sig bytes, recompute row_hash. | `SignatureInvalid` (sig no longer verifies over signing view). |
| Change data, recompute sig (no Ed25519 key). | Cannot produce sig -> `SignatureInvalid`. |
| Change data + sig + row_hash (has both keys). | **Passes.** See Section 6. |
| Strip signatures array to empty. | `RowHashMismatch` (canonical bytes changed). Downstream also loses authenticity claim. |
| Downgrade `payload_version` to 1. | `RowHashMismatch`. v1 re-emit is a different byte sequence. |
---
## 6. Key compromise scenarios
Trusty has two key materials with different blast radii:
### 6.1 SipHash `SecretKey` compromise (one key, symmetric, per-installation)
Attacker can:
- Recompute `row_hash` for any fabricated payload.
- Walk the chain forward: craft a new row with correct `prev_hash` and
correct `row_hash`, pass `verify_links`.
- Generate valid `anchor_hash` values (anchor_hash requires no key -- it is
public SHA-256; attacker can always do this step).
Attacker cannot (in a v2 chain):
- Forge an Ed25519 signature. The `SignatureInvalid` error will fire on any
row they fabricate claiming a signer they do not control.
- **Therefore: v2 signed chains survive SipHash key compromise.** The break
surfaces as `SignatureInvalid`, not structural mismatch, but it is still
detected.
Mitigation: rotate SipHash key on suspected exposure. Old rows remain
verifiable with the old key; new rows use the new key. Document rotation
boundary in operations.
### 6.2 Ed25519 signing key compromise (per-signer, asymmetric)
Attacker can:
- Sign arbitrary data under the compromised public key. Any verifier who
trusts that `pk` will accept the forged signatures.
Attacker cannot:
- Compute `row_hash` without the SipHash key.
- **Therefore: signed chains survive Ed25519 key compromise.** The break
surfaces as `RowHashMismatch` when the storage-layer key is different
from the attacker's compromised key.
Mitigation: revoke compromised `pk`; verifiers maintain a revocation list
and reject signatures from revoked keys after the revocation timestamp.
Trusty does not ship a revocation mechanism today -- this is a downstream
consumer concern.
### 6.3 Dual compromise (both keys)
Attacker can forge a self-consistent chain end-to-end. This is the only
scenario where trusty's local primitives fail silently.
Defense-in-depth:
- **Publish `anchor_hash` externally.** Any row whose anchor was published
(blockchain, transparency log, notary, etc.) before the compromise
window cannot be retroactively rewritten. Post-publication tampering is
caught by comparing local anchor to the external record.
- **Distribute keys across trust boundaries.** SipHash key belongs to the
storage operator; Ed25519 key belongs to the data-producing entity. An
attacker who wants both must compromise both parties.
- **Cross-check with siblings.** Where a chain is mirrored (threadkeeper,
Distant Star), discrepancies between the local chain and the mirror are
evidence of tampering on one side, independent of local key material.
### 6.4 What dual compromise still cannot do
Even with both keys, an attacker cannot:
- Forge a row whose `anchor_hash` has already been published externally and
whose copy is archived outside the trusty DB. Anchor publication is the
ultimate integrity witness.
- Hide a historical BREAK marker if any consumer has previously read and
cached the chain (independent copies surface the diff).
---
## 7. Out-of-scope threats
Trusty does **not** protect against these and callers must layer additional
controls where they matter:
- **Backdating.** `detected_at` and `received_at` are caller-provided
strings. They are canonical bytes inside the payload and covered by
row_hash + signature, but they are unverifiable claims about when
something happened. Use a trusted time source (TSA, blockchain timestamp)
externally if the timing claim is load-bearing.
- **Data authenticity upstream of ingest.** `data` is whatever the caller
hands `append()`. Trusty certifies that the byte sequence was received
and chained; it does not certify that the byte sequence reflects reality.
IoT sensor integrity, API-to-DB pipelines, etc. are upstream concerns.
- **Availability / DoS.** Trusty makes no liveness guarantees. An attacker
with write access can flood the DB or corrupt the file; trusty detects
tampering on verify but does not prevent service degradation.
- **Confidentiality.** Payloads are stored in cleartext canonical JSON.
Encrypt the DB file (or the `data` field via caller-side envelope
encryption) if confidentiality is required. `row_hash` is computed over
the cleartext.
- **Access control.** Any process with file-system access can call
`append()`. Gate ingestion at a higher layer (API, auth middleware).
- **Replay.** A signed row can be inserted into a different chain if the
attacker also holds the SipHash key for that target chain. Chain-id and
entity-id are in the signing view, so cross-chain replay requires
fabricating anchor_hash with matching entity + chain strings -- detected
by `AnchorHashMismatch` only if the original row has a different anchor.
Defense is at the insertion gate.
---
## 8. Operational guidance for integrators
Short checklist for consumers (threadkeeper, Distant Star, direct users):
1. **Verify reports are not pass/fail.** Always inspect `errors` and walk
BREAK markers. A chain with five BREAK events and `ok = true` is an
integrity event, not a clean bill.
2. **Publish anchor_hash early and often.** Anchor publication is the
single strongest defense against dual-key compromise.
3. **Separate key custody.** SipHash key lives with the storage operator;
Ed25519 keys live with signing entities. Do not collocate them in the
same process or HSM slot unless you have a specific reason.
4. **Maintain signer-key registries.** Consumers decide which `pk` values
they trust. Trusty does not; it only verifies signatures against the
pk field in the payload.
5. **Treat `detected_at` / `received_at` as claims, not facts.** If timing
is adversarial, pair with an external TSA.
6. **Do not compare row_hash across installations.** The 16-byte SipHash
key differs per installation, so the same logical payload has different
row_hash values across deployments. Compare `anchor_hash` for
cross-installation equivalence, since that is SHA-256 and keyless.
---
## 9. Related locked surfaces
Changes to these surfaces require a threat-model review and must leave the
listed invariant tests green:
- Dual-hash primitive (SipHash-2-4 + SHA-256). Tag
`s1_invariant_payload_v1_canonical`.
- v1 canonical byte shape. Tag `s5_invariant_v1_byte_shape_preserved`.
- row_hash BE-hex encoding. Tag `s2_decision_be_encoding_canonical`.
- Signing-view algorithm (row_hash commits to signature). Tag
`s5_decision_v2_signatures_inline_row_hash_commits`.
- Ed25519 determinism (no randomized-sig variant). Tag
`s4_invariant_ed25519_determinism`.
- Three-table SQLite schema + `hash_index` cross-check.
Anything that weakens a detection surface is a regression.