fix(buzz-audit): close object_id/detail field-boundary collision in compute_hash (#4173) - #4249
fix(buzz-audit): close object_id/detail field-boundary collision in compute_hash (#4173)#4249iroiro147 wants to merge 1 commit into
Conversation
…ompute_hash (block#4173) `compute_hash` hashed two variable-length fields adjacently — `object_id` and the canonical JSON of `detail` — with no frame between them. Any byte shift across that boundary left the preimage identical: `("x", 12)` and `("x1", 2)` produced the same digest, so an entry could be substituted for a different one and `verify_chain` still passed. The documented property for `detail` ("Included in the hash so tampering with it is detectable") was violated. Fix by moving the fixed-width `prev_hash` between `object_id` and `detail` in the preimage order (suggested fix block#2 on the issue). No byte shift across the boundary can now keep the full preimage unchanged. This is a chain-invalidating field-order change by design, like any other change to `compute_hash`'s fixed field order — existing chains must be migrated to re-verify. Adds a regression test pinning the exact repro pair from the issue plus a second string-skewed pair to exercise the same boundary at different byte lengths. Signed-off-by: Sarthak Singh <sarthak.singh@juspay.in>
|
Cross-linking #4216, which fixes the same collision by versioning the encoding instead (rows verify under their stored version; new rows are length-framed; no data migration). The two approaches could genuinely compose — this reorder could simply be the next encoding version — so it may be worth a maintainer call on the shape. One cost worth surfacing either way: after a field-order change, every pre-existing row fails Also: the reorder closes the reported boundary, but the preimage keeps other unframed variable-length neighbours — |
|
@alanshurafa is right on both counts, and the cost he surfaces is the deciding factor. Conceding the two points explicitly so the maintainer call is on the real question:
So the honest read: this reorder should not land as-is. #4216's versioned-length framing is the strictly better shape — rows verify under their stored version, new rows are length-framed, no migration, and it fixes the whole preimage family rather than one boundary. The compose option: if the maintainers want the reorder too, it becomes the v2 layout inside #4216's framing (v1 = legacy rows verified as-is, v2 = length-framed + reordered). I'm happy to either (a) rebase my branch onto #4216 to make the reorder be its v2 encoding, or (b) close this PR in favour of #4216 and fold anything useful there. Which shape do the maintainers want? I don't want to keep both open competing if there's a preference. |
Summary
Fixes #4173 — takes the issue author's suggested fix #2 (move a fixed-width field between the two variable-length fields).
compute_hashincrates/buzz-audit/src/hash.rshashedobject_id(Option<String>, free text) immediately followed bycanonical_json(detail)(arbitraryserde_json::Value, possibly a bare scalar), unframed. Any byte shift across their boundary left the preimage identical:object_iddetail"x"12x‖12=x12"x1"2x1‖2=x12Both entries hashed to the same digest. A detail edit survived
verify_chain— the propertyentry.rsdocuments ("Arbitrary JSON context. Included in the hash (serialized with sorted keys for determinism) so tampering with it is detectable.") was violated.Fix
Move the fixed-width
prev_hashinto the preimage betweenobject_idanddetail:The 32-byte
prev_hashcan't slide, so no byte shift across the (now separated)object_id/detailboundary can preserve the full preimage.This is a chain-invalidating field-order change, by design — the same migration treatment any change to
compute_hash's fixed field order requires. Existing chains must be re-hashed, or the deployment must wipe and rebuild its audit chain. Maintenance of existing chains is the relay operator's responsibility, as documented on the issue.Picked fix #2 over length-prefixing because it is the smallest structural change with no framing format to spec, parse, or bug on, and it composes with the existing
Some(vec![tag])/Nonepresence tags unchanged. Length-prefixing every variable-length field is a strictly stronger option if a future format revision is planned.Why this is safe
crates/buzz-audit/src/hash.rschanges — 44 insertions, 1 deletion.compute_hashcallers (service,lib,entry,error) pass entries, not preimages — they don't depend on field order.prev_hashwas already part of the hash; only its position changed.Testing
New regression test
hash::tests::object_id_detail_boundary_is_unambiguouspins both the exact repro pair from the issue (object_id="x"/detail=12versusobject_id="x1"/detail=2) and a second string-skewed pair ("ab"+"c"versus"a"+"bc") that exercises the same boundary at different byte lengths.🤖 Generated with Claude Code