feat(security): add tamper-evidence for transcript, session, and durable journal history#6453
Conversation
8b5675a to
0fc4098
Compare
|
Rebased onto origin/main (now at #6452, "durable rotate-key") to resolve the branch staleness flagged during review — no code changes beyond conflict resolution. Two genuine conflicts, both in Re-verified post-rebase: fmt clean, clippy clean (exact CI matrix), nextest 14486/14486 passed, rustdoc gate clean, doctests clean, integrity CI gate self-test passing, full-source gitleaks scan clean. |
0fc4098 to
01cda7e
Compare
|
Fixed a real CI failure caught by the per-bundle checks (`Bundle Check (chat)`, `Bundle Check (bench)`, `MSRV check (1.97, bench)`) after the rebase push: `Cli::resume_allow_unverified` was write-only, never read anywhere in the codebase, triggering a dead-code error under `build.warnings = deny`. Root cause: once `--allow-unverified` was made `requires = "print"` at the clap level (closing the earlier silent-no-op finding on the interactive resume path), the `print: false` match arm's captured `allow_unverified` is now provably always `false` — clap guarantees the flag can't reach that arm as `true`. The field existed as scaffolding for a not-yet-implemented interactive override and became fully dead once the clap-level guard landed. My local clippy run earlier didn't catch it because it used `--all-targets` under a wide combined feature set where a different code path happened to mask the warning; the narrower single-bundle `cargo check --features chat`/`--features bench` builds CI actually runs exposed it directly. Removed the field and its assignment; the match arm now uses `..` to ignore the (always-false) `allow_unverified` field it destructures, with a comment explaining why. Re-verified clean under both `cargo check --features chat` and `cargo check --features bench --all-targets` locally (matching the exact failing CI commands), plus the full fmt/clippy/nextest suite (1028/1028 for the `zeph` binary crate specifically, no regressions). |
…ble journal history Persisted transcript/session/journal files were read back and replayed (including unattended zeph-durable crash-resume) with no protection against at-rest tampering, distinct from the existing live prompt-injection defenses. Adds a keyed-BLAKE3 hash-chain over zeph-subagent transcripts and zeph-session event logs (fail-closed on any detected break, auto-trust-once for genuinely pre-feature legacy files, epoch-aware key rotation), and an authenticated high-water-mark plus default-on row-HMAC for zeph-durable's journal, compaction-aware across checkpoint_fold. Session resume gains a deliberate --allow-unverified override for the attended path; durable's unattended resume stays hard-abort with no override. Known scope limit: a full-file chain-field strip still downgrades to trusted-legacy (no cryptographic anchor outside attacker write-reach yet); tracked as a P1 follow-up in #6449. A loud once-per-file warning now flags this case when a key is actively configured. Closes #6360
01cda7e to
7940abc
Compare
…ate-key (#6460) * feat(durable): add HMAC and HWM rotation windows for zeph durable rotate-key Control-entry HMACs on a shared DB were derived from ZEPH_DURABLE_KEY with no rotation window, so rotating the key force-aborted every in-flight execution; rotate-key only guarded this with an operator-trust --ack-shared-db-drain flag. LocalBackend gains with_previous_hmac_key, mirroring the AEAD cipher's existing with_previous window: verification tries the current key then the previous key, and a new drop-scan (count_control_entries_under_previous_hmac) covers the payload-less crash-orphan EffectIntent case the AEAD blob-scan alone would miss. --ack-shared-db-drain, its refusal gate, and its CLI surface are removed now that a real window exists. The TUI durable panel gains a passive key_id / rotation-window-open indicator, reusing the existing 5s poll task. Rebasing onto the concurrently-merged transcript/durable tamper-evidence work (#6453) surfaced a related defect: its HWM integrity key added a with_previous_hwm_key primitive but froze the rotation epoch at a constant, leaving the window unreachable and the scheduler-daemon read path entirely unattached, so any rotate-key followed by a restart force-aborted every pre-rotation execution's high-water-mark check on all deployments, not just shared DBs. HWM rotation now rides the same key_id/previous_key_id lifecycle as the AEAD and control-HMAC windows, with a third drop-scan for executions whose checkpoint was folded after rotation (checkpoint_fold reseals under the new key without re-signing the HWM row, so neither existing scan would catch them). Closes #6451 Closes #6450 Closes #6454 * ci(durable): audit scheduler_daemon.rs for the transcript-integrity gate New test-only open_execution calls in src/commands/scheduler_daemon.rs (added for the HMAC/HWM rotation-window regression tests) tripped the #6360 CI completeness gate, which requires every trusted-history reader to appear in .github/integrity_audited.txt with a verdict.
Summary
Persisted transcript/session/journal files are read back and replayed (including unattended
zeph-durablecrash-resume) with no protection against at-rest tampering — a distinct threat model from the existing live prompt-injection defenses (IPI filter, causal analyzer, shadow-sentinel isolation), which only cover content arriving during the current turn.zeph-common::hash_chain) reusing the existing keyed-BLAKE3/derive_keypattern already used byzeph-durable's row-HMAC.zeph-subagenttranscripts andzeph-sessionevent logs gain an inline, additivechainfield: fail-closed on any detected break (never warn-and-skip), auto-trust-once for genuinely pre-feature legacy files, and fail-closed (not legacy-downgrade) for any chained file with an unresolvable key epoch.zeph-durablegains a default-on row-HMAC (closing the previous opt-in gap) plus an authenticated per-execution high-water-mark that stays correct acrosscheckpoint_foldcompaction.zeph sessions resumegains a deliberate--allow-unverifiedoverride for the attended path (hard-rejected by clap outside--printfor this PR); durable's unattended crash-resume path stays hard-abort with no override.ZEPH_HISTORY_KEY, domain-separated per subsystem), a CI completeness gate (.github/scripts/check_integrity_audit.sh) enumerating every trusted-history reader in the codebase, and a spec atspecs/081-transcript-integrity/spec.md.Known scope limitation
A full-file chain-field strip (deleting every
chainfield so a tampered file looks like genuine pre-feature legacy) still downgrades to trusted-legacy — there is no cryptographic anchor outside attacker write-reach yet. This is the primary open gap relative to the adaptive-injection threat model that motivated the issue. In-place edits, reordering, partial strips, and epoch/key tampering are all still detected and fail-closed. A loud once-per-file warning now flags a legacy classification while a key is actively configured, converting silent trust into an auditable signal without the full cryptographic fix.Tracked as a P1 follow-up: #6449 will implement the vault-anchor mechanism for both the JSONL and durable sides.
Also filed as a follow-up (found during implementation, not part of this PR's scope): #6447 wires durable's HWM key-rotation into
--init/a CLI command — currently structurally supported but not operator-facing.Review history
This went through 3 rounds of architect/critic design review (2 critical findings on the first pass: a downgrade-to-legacy defeat under the stated threat model, and a fundamental conflict between an early chain design and
zeph-durable'scheckpoint_foldcompaction — both resolved), parallel tester/perf/security/impl-critic validation (impl-critic caught an unrelated, previously-untested bug: durable prune broke on keyed backends due to a missing FK-cascade delete, now fixed with a regression test), and a full code-review pass that caught the branch being unrebased against 5 since-mergedorigin/maincommits, including a migration-number collision with #6439 (resolved: renumbered to migration 113).Test plan
cargo nextest run --config-file .github/nextest.toml --workspace --features "desktop,ide,server,chat,pdf,scheduler" --lib --bins— 14459/14459 passedcargo +nightly fmt --checkcargo clippy --profile ci --workspace --all-targets --features "desktop,ide,server,chat,pdf,scheduler,registry,testing,deep-link" -- -D warningsRUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace --features "desktop,ide,server,chat,pdf,scheduler"cargo test --doc --workspace --features "desktop,ide,server,chat,pdf,scheduler"bash .github/scripts/check_integrity_audit.sh(CI completeness gate, self-test verified to catch pattern-rot)gitleaks protect --staged --no-banner --redact— no leaks.local/testing/playbooks/transcript-integrity.md).local/testing/coverage-status.mdCloses #6360