Skip to content

feat(security): add tamper-evidence for transcript, session, and durable journal history#6453

Merged
bug-ops merged 1 commit into
mainfrom
feat/issue-6360/transcript-integrity
Jul 18, 2026
Merged

feat(security): add tamper-evidence for transcript, session, and durable journal history#6453
bug-ops merged 1 commit into
mainfrom
feat/issue-6360/transcript-integrity

Conversation

@bug-ops

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

Copy link
Copy Markdown
Owner

Summary

Persisted transcript/session/journal files are read back and replayed (including unattended zeph-durable crash-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.

  • Adds a shared keyed-BLAKE3 hash-chain primitive (zeph-common::hash_chain) reusing the existing keyed-BLAKE3/derive_key pattern already used by zeph-durable's row-HMAC.
  • zeph-subagent transcripts and zeph-session event logs gain an inline, additive chain field: 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-durable gains a default-on row-HMAC (closing the previous opt-in gap) plus an authenticated per-execution high-water-mark that stays correct across checkpoint_fold compaction.
  • zeph sessions resume gains a deliberate --allow-unverified override for the attended path (hard-rejected by clap outside --print for this PR); durable's unattended crash-resume path stays hard-abort with no override.
  • Vault-backed key material (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 at specs/081-transcript-integrity/spec.md.

Known scope limitation

A full-file chain-field strip (deleting every chain field 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's checkpoint_fold compaction — 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-merged origin/main commits, 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 passed
  • cargo +nightly fmt --check
  • cargo clippy --profile ci --workspace --all-targets --features "desktop,ide,server,chat,pdf,scheduler,registry,testing,deep-link" -- -D warnings
  • RUSTFLAGS="-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
  • Fail-closed chain-break, legacy auto-trust-once, partial-strip detection, epoch-downgrade rejection, concurrent-append safety, fork history-laundering rejection, keyed-backend durable prune — all covered by new regression tests (see .local/testing/playbooks/transcript-integrity.md)
  • Live multi-turn session / multi-agent transcript testing not performed as part of this PR — automated coverage only; flagged for the next live-testing cycle per .local/testing/coverage-status.md

Closes #6360

@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 ci CI/CD configuration 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 13:03
@github-actions github-actions Bot added the enhancement New feature or request label Jul 18, 2026
@bug-ops
bug-ops force-pushed the feat/issue-6360/transcript-integrity branch from 8b5675a to 0fc4098 Compare July 18, 2026 13:22
@bug-ops

bug-ops commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

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 zeph-config's migration-step registry: #6452 and this PR each independently claimed migration step 97 (MigrateDurableKeyRotation vs MigrateDurableHwmAdvisory). Resolved by keeping #6452's step 97 as-is and renumbering this PR's advisory migration to step 98 — both now coexist in sequence. CHANGELOG merge was purely additive (two independent bullets under the same ### Added header). No conflicts in source logic; zeph-durable's AEAD key-rotation (#6452) and this PR's HWM/row-HMAC integrity mechanism are unrelated features sharing the same crate.

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.

@bug-ops
bug-ops force-pushed the feat/issue-6360/transcript-integrity branch from 0fc4098 to 01cda7e Compare July 18, 2026 13:33
@bug-ops

bug-ops commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

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
@bug-ops
bug-ops force-pushed the feat/issue-6360/transcript-integrity branch from 01cda7e to 7940abc Compare July 18, 2026 13:38
@bug-ops
bug-ops merged commit 1bc76b2 into main Jul 18, 2026
47 checks passed
@bug-ops
bug-ops deleted the feat/issue-6360/transcript-integrity branch July 18, 2026 13:46
bug-ops added a commit that referenced this pull request Jul 18, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI/CD configuration core zeph-core crate documentation Improvements or additions to documentation enhancement New feature or request rust Rust code changes size/XL Extra large PR (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

research(security): persisted transcript/history integrity — no tamper detection before durable replay

1 participant