Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(go-audit): hash entries with length-prefix encoding, not | delimiter#2177

Merged
imran-siddique merged 1 commit into
microsoft:mainfrom
aegis-initiative:fix/go-audit-hash-length-prefix
May 12, 2026
Merged

fix(go-audit): hash entries with length-prefix encoding, not | delimiter#2177
imran-siddique merged 1 commit into
microsoft:mainfrom
aegis-initiative:fix/go-audit-hash-length-prefix

Conversation

@finnoybu

Copy link
Copy Markdown
Contributor

Summary

computeHash in audit.go joined AuditEntry fields with a literal | separator before hashing. The separator was forgeable across field boundaries when any user-controlled field contained a |.

For example, with the previous format:

AgentID Action Pre-hash bytes
a b|c ...|a|b|c|...
a|b c ...|a|b|c|...

Both fields are caller-supplied via AuditLogger.Log, so an attacker who controls one of them can fabricate an entry that hashes identically to a legitimate one with the field boundary shifted — undermining the tamper-detection guarantee the hash chain is meant to provide.

Change

Replace the |-concatenated pre-hash with length-prefixed encoding:

[1 byte version] [4-byte BE length | UTF-8 bytes] x 5 fields

Each field's length is committed to the digest, so no value can impersonate a different field-boundary layout. A 1-byte version prefix is reserved at the head so the encoding can be evolved later without ambiguity. Length-prefix is more principled here than JSON-encoding the hash input: no schema dependency, deterministic byte layout, and no allocator surprises.

Compatibility

The audit log is in-memory only and there is no ImportJSON / load-and-verify path in this package — Verify() always recomputes hashes from in-store fields using the current format. So changing the hash input does not break any persisted-and-re-verified state.

Tests

go test ./... from agent-governance-golang/packages/agentmesh/ passes. New regression tests:

  • TestComputeHashSeparatorForgery — the canonical AgentID=\"a\", Action=\"b|c\" vs AgentID=\"a|b\", Action=\"c\" pair, which collided under the old format, now hashes differently.
  • TestComputeHashDistinguishesFieldBoundaries — exercises the action/decision and decision/previous_hash boundaries as well.

All existing audit tests continue to pass (TestAuditLogAndVerify, TestAuditHashDeterministic, the retention/seam tests, etc.).

Test plan

  • go test ./... passes from agent-governance-golang/packages/agentmesh/.
  • Two field-sets that previously hashed identically across the | boundary now produce different hashes.
  • Hash chain Verify() continues to pass for normal sequences.
  • Tamper-detection tests continue to reject mutated entries.

Surfaced during independent audit conducted by @finnoybu (Ken Tannenbaum, AEGIS Initiative); [LOW, Go].

…miter

computeHash concatenated AuditEntry fields with "|" as a separator before
hashing:

    data := e.Timestamp.Format(...) + "|" +
        e.AgentID + "|" + e.Action + "|" +
        string(e.Decision) + "|" + e.PreviousHash

This was forgeable across field boundaries when any user-controlled field
contained a "|". For example AgentID="a", Action="b|c" produced the
identical pre-hash bytes "...|a|b|c|..." as AgentID="a|b", Action="c".
Both AgentID and Action are caller-supplied via AuditLogger.Log, so an
attacker who controls one field can fabricate an entry that hashes the
same as a legitimate one with the boundary shifted, breaking the
tamper-detection guarantee the hash chain is meant to give.

Replace the "|" concatenation with length-prefixed encoding:

  [1 byte version] [4-byte BE length | bytes] x 5 fields

Length-prefix commits every field's length to the digest, so no value
can impersonate a different field-boundary layout. This is more
principled than JSON-encoding for a hash input — it has no schema
dependency, a deterministic byte layout, and no allocator surprises.

A 1-byte version prefix is reserved at the head so the encoding can be
evolved later without ambiguity.

Compatibility: the audit log is in-memory only and there is no
ImportJSON / load-and-verify path in this package, so changing the hash
input does not break any persisted-and-re-verified state. Verify()
continues to recompute hashes from in-store fields, so it always uses
the current format.

Regression tests:
- TestComputeHashSeparatorForgery — the canonical AgentID="a",
  Action="b|c" vs AgentID="a|b", Action="c" pair, which collided under
  the old format, now hashes differently.
- TestComputeHashDistinguishesFieldBoundaries — also exercises the
  action/decision and decision/previous_hash boundaries.

All existing audit tests continue to pass.
@github-actions

Copy link
Copy Markdown
🤖 AI Agent: docs-sync-checker — Docs Sync

Docs Sync

  • computeHash() in audit.go -- missing docstring
  • README.md -- section on hashing or audit entries needs update
  • CHANGELOG.md -- missing entry for behavioral change in hash computation

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: breaking-change-detector — API Compatibility

API Compatibility

Severity Change Impact
Breaking Changed the hashing mechanism from ` `-separated fields to length-prefixed encoding

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: test-generator — `audit.go`

audit.go

  • TestComputeHashSeparatorForgery -- validates that hash collisions do not occur across field boundaries due to separator forgery.
  • TestComputeHashDistinguishesFieldBoundaries -- ensures distinct hashes for entries differing at action/decision and decision/previous_hash boundaries.

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: security-scanner — Security Review

Security Review

Severity Finding Fix
LOW The previous hash computation method allowed for hash collisions due to user-controlled input containing the ` ` delimiter, which could lead to forgery of audit entries.

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: code-reviewer — View details

TL;DR: 0 blockers, 0 warnings. No issues found. Clean change.

@github-actions github-actions Bot added the size/M Medium PR (< 200 lines) label May 12, 2026
@github-actions

Copy link
Copy Markdown

🟡 Contributor Check: MEDIUM

Check Result
Profile MEDIUM
Credential NONE
Overall MEDIUM

Automated check by AGT Contributor Check.

@github-actions github-actions Bot added the needs-review:MEDIUM Contributor check flagged MEDIUM risk label May 12, 2026
@github-actions

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ✅ Passed No issues found
🛡️ Security Scan ✅ Completed Analysis complete
🔄 Breaking Changes ✅ Completed Analysis complete
📝 Docs Sync ✅ Completed Analysis complete
🧪 Test Coverage ✅ Completed Analysis complete

Verdict: ✅ Ready for human review

@imran-siddique imran-siddique merged commit b41a134 into microsoft:main May 12, 2026
13 of 14 checks passed
MohammadHaroonAbuomar pushed a commit to MohammadHaroonAbuomar/agt-acs that referenced this pull request Jun 1, 2026
…miter (microsoft#2177)

computeHash concatenated AuditEntry fields with "|" as a separator before
hashing:

    data := e.Timestamp.Format(...) + "|" +
        e.AgentID + "|" + e.Action + "|" +
        string(e.Decision) + "|" + e.PreviousHash

This was forgeable across field boundaries when any user-controlled field
contained a "|". For example AgentID="a", Action="b|c" produced the
identical pre-hash bytes "...|a|b|c|..." as AgentID="a|b", Action="c".
Both AgentID and Action are caller-supplied via AuditLogger.Log, so an
attacker who controls one field can fabricate an entry that hashes the
same as a legitimate one with the boundary shifted, breaking the
tamper-detection guarantee the hash chain is meant to give.

Replace the "|" concatenation with length-prefixed encoding:

  [1 byte version] [4-byte BE length | bytes] x 5 fields

Length-prefix commits every field's length to the digest, so no value
can impersonate a different field-boundary layout. This is more
principled than JSON-encoding for a hash input — it has no schema
dependency, a deterministic byte layout, and no allocator surprises.

A 1-byte version prefix is reserved at the head so the encoding can be
evolved later without ambiguity.

Compatibility: the audit log is in-memory only and there is no
ImportJSON / load-and-verify path in this package, so changing the hash
input does not break any persisted-and-re-verified state. Verify()
continues to recompute hashes from in-store fields, so it always uses
the current format.

Regression tests:
- TestComputeHashSeparatorForgery — the canonical AgentID="a",
  Action="b|c" vs AgentID="a|b", Action="c" pair, which collided under
  the old format, now hashes differently.
- TestComputeHashDistinguishesFieldBoundaries — also exercises the
  action/decision and decision/previous_hash boundaries.

All existing audit tests continue to pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:MEDIUM Contributor check flagged MEDIUM risk size/M Medium PR (< 200 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants