14 KiB
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
dataintoappend(). - Control of
detected_at/received_atvalues when submitting. - Possession of one or both cryptographic keys -- the 16-byte SipHash
SecretKeyused forrow_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_linksreturnsok = truefor 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 theirbreak_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 withsignatures=[]). - 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_hashfor any fabricated payload. - Walk the chain forward: craft a new row with correct
prev_hashand correctrow_hash, passverify_links. - Generate valid
anchor_hashvalues (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
SignatureInvaliderror 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
pkwill accept the forged signatures.
Attacker cannot:
- Compute
row_hashwithout the SipHash key. - Therefore: signed chains survive Ed25519 key compromise. The break
surfaces as
RowHashMismatchwhen 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_hashexternally. 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_hashhas 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_atandreceived_atare 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.
datais whatever the caller handsappend(). 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
datafield via caller-side envelope encryption) if confidentiality is required.row_hashis 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
AnchorHashMismatchonly 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):
- Verify reports are not pass/fail. Always inspect
errorsand walk BREAK markers. A chain with five BREAK events andok = trueis an integrity event, not a clean bill. - Publish anchor_hash early and often. Anchor publication is the single strongest defense against dual-key compromise.
- 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.
- Maintain signer-key registries. Consumers decide which
pkvalues they trust. Trusty does not; it only verifies signatures against the pk field in the payload. - Treat
detected_at/received_atas claims, not facts. If timing is adversarial, pair with an external TSA. - 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_hashfor 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_indexcross-check.
Anything that weakens a detection surface is a regression.