Files
Trusty/docs/threat-model.md

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

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.

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.