fix(agent-mesh): make MerkleAuditChain root reproducible by an independent rebuild#3361
fix(agent-mesh): make MerkleAuditChain root reproducible by an independent rebuild#3361chopmob-cloud wants to merge 3 commits into
Conversation
…ndent rebuild MerkleAuditChain built its tree two ways that were meant to agree but did not. add_entry updated the tree incrementally and padded interior levels with singleton zero nodes, while _rebuild_tree (the documented verification path) pads at the leaf level and hashes the zero padding upward. The two roots diverged once a real entry's authentication path reached an interior padding node, first at five entries and again at 6, 9, 10 and most non power of two sizes. Because export() emits the incremental root, a verifier that rebuilds a standard Merkle tree from the exported entries computed a different root and rejected an untampered log. Rebuild the tree from all leaves on each append so the recorded root is byte identical to an independent recomputation. Add regression tests covering root reproducibility, inclusion proof verification, and tamper detection.
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
🤖 AI Agent: docs-sync-checker — Docs Sync
Docs SyncDocumentation is in sync. |
🤖 AI Agent: contributor-guide — What you did well:
Welcome, and thank you for your detailed contribution! It's great to see the thorough analysis and testing you've included. What you did well:Your fix is well-documented, with clear reasoning and robust test coverage for reproducibility and tamper detection. Actionable items:
For more details on contributing, please refer to our CONTRIBUTING.md. |
🤖 AI Agent: breaking-change-detector — API Compatibility
API Compatibility
|
🤖 AI Agent: code-reviewer — View details
TL;DR: 0 blockers, 1 warning. The fix improves Merkle tree reproducibility but introduces a breaking change requiring downstream consumers to re-verify archived logs.
Action items:
Warnings:
|
🤖 AI Agent: test-generator — `agent-mesh/src/agentmesh/governance/audit.py`
|
🤖 AI Agent: security-scanner — View details
No security issues found. |
PR Review Summary
Verdict: AI review comments are untrusted advisory output. The summary reports workflow-generated completion status only, not model-authored pass/fail claims. |
|
🔴 Contributor Check: HIGH
Automated check by AGT Contributor Check. |
MohammadHaroonAbuomar
left a comment
There was a problem hiding this comment.
- Replace full-rebuild-per-append with a root-equivalent incremental fix. Measured: main appends 5000 entries in 0.24s; this PR takes 87.6s (~365x, O(n^2)). ADR-0017 requires sub-millisecond per-entry overhead and AUDIT-COMPLIANCE-1.0.md section 9.3 says the tree MUST be built incrementally. Preferred ~5-line fix: pad interior level k with the empty-subtree constant E(k) where E(0)='0'*64, E(k+1)=SHA-256(E(k)||E(k)) — both constructions then converge to the canonical root with O(log n) appends. Alternative: dirty-flag lazy rebuild in get_root_hash/get_proof.
- test_incremental_root_matches_rebuild_for_every_size is tautological after the fix: _rebuilt_root() calls chain._rebuild_tree(), but add_entry now IS _rebuild_tree, so the test compares the function with itself and can never fail. Inline a genuinely independent pure-hashlib textbook recomputation instead. Also add the empty-chain case (root is None).
- Add a BREAKING_CHANGES.md entry: exported merkle_root values change for most chain sizes (22 of the first 32) vs prior releases; consumers who archived export() output for compliance evidence will see mismatches on re-verification.
- (follow-up, non-blocking) pin the canonical construction in AUDIT-COMPLIANCE-1.0.md 9.3; consider RFC-6962-style leaf/interior domain separation while roots are already changing; delete the now-dead odd-duplication branch in _rebuild_tree.
Minor:
- The bug itself is real and precisely diagnosed — verified empirically: main diverges from an independent textbook recomputation at n=5,6,9-14,17-30; the fix diverges nowhere. Severity honestly classified (over-rejection of honest logs, not tamper concealment).
…t (review) Address review on microsoft#3361: replace the O(n^2) rebuild-per-append with an O(log n) incremental fix, per ADR-0017 and AUDIT-COMPLIANCE-1.0.md 9.3. - add_entry pads interior level k with the empty-subtree constant E(k) (E(0)='0'*64, E(k+1)=SHA-256(E(k)||E(k))) instead of the leaf sentinel, so the incremental root converges on the same canonical root as a from-scratch rebuild while staying O(log n) per append (5000 appends in ~0.3s vs ~88s for the rebuild approach). - Tests recompute the root with an independent pure-hashlib textbook Merkle instead of comparing _rebuild_tree with itself; add the empty-chain case and a separate incremental-vs-rebuild cross-check. - Remove the now-dead odd-duplication branch in _rebuild_tree. - Document the exported-root change in agent-mesh/CHANGELOG.md and BREAKING_CHANGES.md.
|
Thanks, this is exactly right on all points. Pushed the preferred incremental fix. E(k) incremental (no more full rebuild). Tests are no longer tautological. The reproducibility test now recomputes the root with an independent pure-hashlib textbook Merkle that shares no code with Docs. Added a Also done. Removed the now-dead odd-duplication branch in Verification: On the non-blocking follow-ups: happy to pin the canonical construction in AUDIT-COMPLIANCE-1.0.md 9.3 and to add RFC-6962-style leaf/interior domain separation while roots are already changing, if you would like those in this PR or a follow-up. Thanks again for the careful review and the independent reproduction. |
Summary
MerkleAuditChaininagent-mesh/src/agentmesh/governance/audit.pybuilds its tamper-evidence Merkle tree two ways that are meant to agree but do not, so the exportedmerkle_rootis not reproducible by an independent verifier for most chain sizes.Root cause
The class has two constructions:
add_entryupdates the tree incrementally. When it grows capacity it pads interior tree levels with singleton zero nodes (MerkleNode(hash="0" * 64))._rebuild_tree(docstring: "full rebuild, used for verification") pads at the leaf level and hashes the zero padding upward, so an interior empty subtree issha256(ZERO + ZERO)..., not"0" * 64.The two roots agree while every interior node on a real leaf's authentication path is itself real, but diverge as soon as that path reaches an interior padding node. That first happens at five entries, and again at 6, 9, 10 and most non power of two sizes.
AuditLog.export()emits the incremental root asmerkle_root. A third party (or the module's own_rebuild_tree) that recomputes a standard Merkle tree from the exported entries gets a different root and rejects a genuine, untampered log. This is a correctness and reproducibility defect in the integrity tooling; it does not let an attacker conceal tampering (the effect is over rejection of honest logs, and the liveverify_integritypath uses the linearprevious_hashchain, which is unaffected).Reproduction
On a clean install of the shipped package, comparing
get_root_hash()against an independent textbook Merkle recomputation of the same leaf hashes:Fix
Rebuild the tree from all leaves on each
add_entryvia the existing_rebuild_tree, removing the divergent incremental interior padding. The recorded root is then byte identical to an independent recomputation for every entry count. A full rebuild is O(n) per append, which is negligible for audit volumes next to a non reproducible tamper evidence root.Validation
Clean
python:3.12-slimcontainer, package installed from source.test_step_receipt_to_audit_chain.py,test_governance.py, and the newtest_merkle_audit_root_reproducible.py, with the 45 pre existing audit tests unchanged (zero regressions).n=1..32: all reproduce; tampering with any leaf still changes the recomputed root.New tests in
test_merkle_audit_root_reproducible.pycover incremental vs rebuild agreement across the first two capacity doublings, the five entry minimal reproducer, inclusion proof verification against the recorded root, and tamper detection.Note on classification
I read this as a correctness bug rather than an exploitable vulnerability, since it causes honest logs to fail verification rather than letting tampering pass, and no trust boundary is crossed. If maintainers see a security angle I have missed, I am happy to move the discussion to a private channel.