Skip to content

fix(buzz-audit): truncate created_at to microseconds before hashing#2678

Closed
Christian-Sidak wants to merge 1 commit into
block:mainfrom
Christian-Sidak:fix/issue-2637
Closed

fix(buzz-audit): truncate created_at to microseconds before hashing#2678
Christian-Sidak wants to merge 1 commit into
block:mainfrom
Christian-Sidak:fix/issue-2637

Conversation

@Christian-Sidak

Copy link
Copy Markdown

Summary

  • Utc::now() returns nanosecond precision but Postgres TIMESTAMPTZ stores only microseconds; to_rfc3339() emits 9 vs 6 fractional digits, producing a different byte string and thus a different SHA-256 digest
  • verify_chain recomputes the hash from the stored (truncated) row and compares it to the written hash -- they never matched, so every chain failed verification on untampered data
  • This made AuditError::HashMismatch the steady state, rendering tamper detection useless

Fix: call .trunc_subsecs(6) in log_inner before computing the hash so the value hashed at write time is identical to what Postgres returns on read. One-line change at the single choke point (NewAuditEntry carries no timestamp; all callers go through log_inner).

Adds three Postgres-free regression tests to hash.rs:

  • nanosecond_and_microsecond_timestamps_hash_differently -- documents the root cause
  • hash_survives_microsecond_precision_round_trip -- proves the fix works
  • untruncated_nanosecond_timestamp_fails_round_trip -- regression guard

Test plan

  • cargo fmt -p buzz-audit passes (no formatting changes)
  • Three new regression tests in hash.rs cover the invariant, the trap, and the round-trip
  • All six #[ignore = "requires Postgres"] chain tests in service.rs are expected to pass once run against a real database (per the issue, they fail on unpatched main and should pass with this fix)

Fixes #2637

@Christian-Sidak
Christian-Sidak requested a review from a team as a code owner July 24, 2026 04:09
`Utc::now()` returns nanosecond precision but Postgres TIMESTAMPTZ stores
only microseconds. `verify_chain` re-reads the truncated value and
recomputes the digest, so the hashes never matched -- every chain failed
verification on untampered data, making tamper detection useless.

Fix: call `.trunc_subsecs(6)` in `log_inner` so the value hashed at write
time is identical to what Postgres returns on read.

Adds three Postgres-free regression tests to `hash.rs` documenting the
invariant, the root cause, and the round-trip behaviour.

Fixes block#2637

Signed-off-by: Christian-Sidak <61099993+Christian-Sidak@users.noreply.github.com>
@wpfleger96

wpfleger96 commented Jul 24, 2026

Copy link
Copy Markdown
Member

🤖 hey — thanks for tracking this down, the diagnosis is exactly right (ns-precision Utc::now() at write time vs µs after the Postgres round-trip, so the verify-time preimage differs). We ended up going with #2638 for the same root cause since it also normalizes inside compute_hash (so future call sites can't reintroduce the split) and carries a unit test that guards the fix itself rather than just pinning the bug. I think this one is superseded by that, but appreciate the report and the repro tests — they made verifying the root cause easy.

@Christian-Sidak

Copy link
Copy Markdown
Author

Makes sense, normalizing inside compute_hash is the more durable fix and the better test to carry. Closing in favor of #2638. Thanks for the quick review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

buzz-audit: every hash chain fails verification — created_at is hashed at nanosecond precision but stored at microsecond precision

2 participants