Skip to content

feat(security): add vault-anchor downgrade resistance for transcript integrity#6461

Merged
bug-ops merged 1 commit into
mainfrom
feat/issue-6449/vault-anchor-downgrade-resist
Jul 18, 2026
Merged

feat(security): add vault-anchor downgrade resistance for transcript integrity#6461
bug-ops merged 1 commit into
mainfrom
feat/issue-6449/vault-anchor-downgrade-resist

Conversation

@bug-ops

@bug-ops bug-ops commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #6449

The base transcript-integrity feature (#6360, PR #6453) shipped hash-chain
tamper-evidence for subagent transcripts, session logs, and the durable
journal, but explicitly deferred defense against a fully-consistent
whole-file/whole-execution downgrade
: an attacker who deletes every chain
field from a JSONL file (or the durable integrity row entirely) makes
tampered content indistinguishable from genuine pre-feature legacy content,
which is auto-trusted by design.

This PR closes that gap:

  • JSONL (subagent transcript, session log): a per-file vault anchor
    {epoch, count, head, written_at} is written to the age vault on finalize
    and checked on read. Since the vault cannot be selectively edited without
    the age key, a whole-file strip now produces a detectable mismatch. A
    bounded reconcile-and-cap sweep reaps orphaned anchors and caps vault
    growth via LRU eviction keyed on the anchor's own embedded write
    timestamp (not filesystem mtime, which is attacker-writable).
  • Durable: replaces the "missing integrity row is legacy" assumption
    with an operator-gated vault seal (zeph durable seal-integrity). Once
    sealed, an absent integrity row on a keyed, drained execution is
    unconditional tamper. The seal boundary is entirely vault-resolved — no
    attacker-writable database column participates in the tamper decision.
  • New [integrity] config section, zeph sessions verify CLI, doctor/
    --init/--migrate-config/TUI integration.
  • Documented residuals rather than silent gaps: session-prefix rollback
    (unanchored tail truncation), grandfather opt-out (permanent forgeable
    slot by design), reconcile-window recreation (bounded, requires a
    destructive precursor).

Design process

Went through 2 architecture revisions and 2 rounds of adversarial critique
before implementation:

  • Rev1 proposed a durable created_at-based floor; critic found it was
    defeated by the same DB-write threat model it targeted (the comparison
    column was unauthenticated and attacker-writable).
  • Rev2 replaced it with a pure vault-presence seal marker plus a
    drain-before-seal precondition — critic confirmed this closed the gap,
    but found the session-anchor LRU eviction picked victims by attacker-
    writable filesystem mtime (same flaw class). Fixed to evict by an
    AEAD-protected embedded timestamp instead.

Implementation was independently validated by parallel test/perf/security/
critic passes, which surfaced and fixed: an insta snapshot regression, a
non-functional read-timeout (the tokio::time::timeout wrapped an
already-resolved future), a vault write-lock held across synchronous
filesystem stats, an inaccurately-documented reconcile-reap residual, and a
dropped NFR-005 audit-event requirement. Final review required two more
rebases as origin/main advanced during the cycle (#6455, #6457, #6460),
including a careful reconciliation in zeph-durable's local.rs to confirm
a concurrently-merged HMAC-rotation feature did not reintroduce a
database-derived value onto the tamper-decision boundary.

Test plan

  • Full CI-matching suite green: fmt --check, clippy --profile ci
    (full feature set), rustdoc gate, cargo test --doc
  • cargo nextest run — 14607 passed, 0 failed, 35 skipped
  • gitleaks protect --staged — clean
  • Whole-strip of an anchored transcript/session → TAMPER; pre-anchor
    chained file (no anchor) → still opens (no migration break)
  • Durable: forged created_at + deleted integrity row on a post-seal
    keyed execution → still TAMPER (proves no DB column on the boundary)
  • Session-anchor eviction order proven to follow the embedded
    written_at, not filesystem mtime, under deliberately conflicting
    test setup
  • Read-timeout regression tests using genuine background-thread lock
    contention (not mocked) for both the async and sync anchor-lookup
    paths
  • Testing playbook and coverage-status updated (main repo
    .local/testing/)

…integrity

Closes #6449

A file-write-only attacker could strip every chain field from a
subagent transcript or session log JSONL file, or delete a durable
execution's integrity row entirely, making tampered content
indistinguishable from genuine pre-feature legacy content that is
auto-trusted by design (#6360's stated residual).

Adds a per-file vault anchor for the JSONL surfaces: a small
{epoch, count, head, written_at} attestation mirrored to the age
vault on finalize and checked on read. Since the vault cannot be
selectively edited without the age key, a whole-file strip now
leaves a detectable mismatch between the file and its anchor. A
bounded reconcile-and-cap sweep reaps orphaned anchors and caps
vault growth via LRU eviction keyed on the anchor's own embedded
write timestamp, not filesystem mtime, so eviction order cannot be
manipulated by a file-write attacker.

For durable executions, replaces the "missing integrity row is
legacy" trust assumption with an operator-gated vault seal: once
sealed, an absent integrity row on a keyed, drained execution is
unconditional tamper. The seal boundary is entirely vault-resolved
so no attacker-writable database column participates in the tamper
decision.

Config gains `[integrity]` (anchor mode, session-anchor cap); new
CLI `durable seal-integrity` and `sessions verify`; wired into
doctor, --init, --migrate-config, and the TUI command palette.
Accepted residuals (session-prefix rollback, grandfather opt-out,
reconcile-window recreation) are documented rather than silently
left implicit.
@github-actions github-actions Bot added enhancement New feature or request documentation Improvements or additions to documentation rust Rust code changes core zeph-core crate size/XL Extra large PR (500+ lines) and removed enhancement New feature or request labels Jul 18, 2026
@bug-ops
bug-ops enabled auto-merge (squash) July 18, 2026 17:33
@bug-ops
bug-ops merged commit 37e1620 into main Jul 18, 2026
47 checks passed
@bug-ops
bug-ops deleted the feat/issue-6449/vault-anchor-downgrade-resist branch July 18, 2026 17:41
bug-ops added a commit that referenced this pull request Jul 18, 2026
* docs(specs): sync specs/ with commits merged since v0.22.1

Covers the durable-execution key-rotation surface (#6447, #6451, #6460),
subagent live transcript forwarding (#6359/#6455), and web_search
follow-ups (#6457):

- specs/064-durable-execution: document the previously-unspecified
  `zeph durable rotate-key`/`--drop-previous` CLI, the unified AEAD/
  control-HMAC/HWM key_id/previous_key_id rotation lifecycle, the three
  drop-previous safety scans, and the durable_execution_integrity HWM
  table/folded_count column.
- specs/081-transcript-integrity: add addendum documenting the HWM
  key-rotation window fix (issue #6460) and correct stale README/MOC
  summaries that still described the whole-file downgrade gap as open
  after #6461 closed it.
- specs/044-subagent-lifecycle: add new section documenting opt-in live
  transcript forwarding (config, mechanism, invariants); cross-reference
  from specs/026-tui-subagent-management and specs/047-cli-modes.
- specs/006-tools/006-1-web-search: document resolved-address-set client
  caching and real backend-status propagation into egress telemetry.
- specs/README.md, specs/MOC-specs.md: refresh index summaries for all
  of the above.

* release: prepare v0.22.2

Bump version from 0.22.1 to 0.22.2, finalize the changelog entry, and
sync generated documentation (mdBook pages, crate READMEs, root
README) with the features and fixes merged since v0.22.1.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core zeph-core crate documentation Improvements or additions to documentation rust Rust code changes size/XL Extra large PR (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

transcript-integrity chain has no downgrade resistance against a full-strip attack (vault anchor missing)

1 participant