297 lines
14 KiB
Markdown
297 lines
14 KiB
Markdown
# 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.
|