# 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.