Audit-Chain Primitive
This page is for developers adding new host-side system audit event chains to the HoloMUSH core. If you are writing a plugin that emits ABAC audit events, see Emitting Audit Events from Plugins instead.
The auditchain primitive (internal/eventbus/audit/chain/) provides a
generalized per-scope tamper-evident sequence for host-owned system audit events.
It was introduced in Phase 5 sub-epic E to generalize the policy_set chain from
sub-epic D into a reusable primitive.
What the primitive provides
Section titled “What the primitive provides”- Per-scope hash chain: Each chain registration is scoped by a
domain-specific key (e.g.,
scene:01ABCfor a per-context rekey chain). Each event in a scope carries aprev_hashlinking it to its predecessor, and aself_hashover its own payload. - Boot-time verifier:
auditchain.VerifierSubsystemwalks every registered chain at server boot. If any chain has a break (hash mismatch or gap), the server refuses to start withAUDIT_CHAIN_BROKEN. - Emitter helper:
auditchain.Emitter.ComputePrevHashForfetches the current chain head for a scope and returns theprev_hashandprev_event_idto embed in the next event.
Chain registration
Section titled “Chain registration”Register a chain at wiring time in cmd/holomush/core.go by calling the
chain’s owning package’s registration function (e.g., dek.RegisterRekey(v)).
The Chain struct
Section titled “The Chain struct”package chain // internal/eventbus/audit/chain
// Chain describes a single hash-chained audit-event family.// Pure metadata — no behavior is carried on the struct.// Behavior (canonicalize / scope extraction) lives as standalone// functions in the owning chain's package, registered via Handler.type Chain struct { SubjectPrefix string // e.g. "events.<game>.system.rekey" SelfHashField string // dot-path of self_hash in payload PrevHashField string // dot-path of prev_hash in payload ScopePayloadField string // dot-path identifying this chain's scope}The Handler struct
Section titled “The Handler struct”The Handler bundles per-chain behavior and is passed to
VerifierSubsystem.Register at wiring time:
type Handler struct { Chain Chain SubjectFor func(scope string) string ScopeFromSubject func(subject string) (string, error) ScopeFromPayload func(payload []byte) (string, error) Canonicalize func(payload []byte) ([]byte, error) PrevHashOf func(payload []byte) ([]byte, error) SelfHashOf func(payload []byte) ([]byte, error)}Contracts you must satisfy
Section titled “Contracts you must satisfy”Subject prefix (INV-E26)
Section titled “Subject prefix (INV-E26)”Every registered chain’s SubjectFor(scope) MUST return a string starting
with events.<game>. — for example:
events.<game>.system.rekey.<context_type>.<context_id>Chain-bearing audit events must live under the events.> JetStream
SubjectFilter so they reach events_audit (where auditchain.Repo reads
from). The audit.* prefix is reserved for future non-chain forensic
emits and must not be used for registered chains.
A meta-test (TestAuditChainRegistry_AllChainsUseEventsPrefix) enforces
this for every chain in the registry.
Scope cross-check (INV-E27)
Section titled “Scope cross-check (INV-E27)”You must register both ScopeFromSubject and ScopeFromPayload. The
verifier independently derives the chain scope from the event’s subject and
from its payload, then asserts they agree. A mismatch is rejected with
AUDIT_CHAIN_SCOPE_MISMATCH.
Both functions must return the same canonical scope string for a given event.
For the rekey chain, the scope is "<context_type>:<context_id>", derived
from the subject as the last two components and from the payload as
context.type + ":" + context.id.
A meta-test (TestAuditChainRegistry_AllChainsRegisterScopeFromPayload)
enforces that both functions are non-nil for every registered chain.
Self-hash composition (INV-E28)
Section titled “Self-hash composition (INV-E28)”The self-hash is computed as:
SHA-256(Canonicalize(zero(payload, SelfHashFieldName)))This composition is pinned at the primitive level via
auditchain.RecomputeSelfHash. You must not diverge from it:
- The hash function is SHA-256 — always.
- The composition order is: zero the self-hash field → canonicalize → SHA-256.
- Your
Canonicalizefunction handles domain-specific normalization (e.g., empty byte slices tonullfor D’s policy_set chain). Plain JCS over the JSON payload is sufficient for most chains.
// auditchain.RecomputeSelfHash is the single authoritative recompute.// Call this in your verifier tests to confirm your chain's self_hash// is reproducible.func RecomputeSelfHash(payload map[string]any, selfHashField string) ([]byte, error)JCS canonicalization pin
Section titled “JCS canonicalization pin”All chains MUST use the vendored
github.com/cyberphone/json-canonicalization jsoncanonicalizer.Transform
function pinned in go.mod. This is enforced by
TestJCSCanonicalizationLockedToVendoredImpl.
Genesis vs linked entries
Section titled “Genesis vs linked entries”- Genesis entry: The first event in a chain scope has
prev_hash: null(or the zero value). The verifier accepts a nullprev_hashonly for the first entry for a given scope. - Linked entry: Every subsequent event must carry the
prev_hashequal to theself_hashof its predecessor. Callauditchain.Emitter.ComputePrevHashFor(ctx, handler, scope)at emit time to fetch the current head.
VerifierSubsystem lifecycle
Section titled “VerifierSubsystem lifecycle”- All chains are registered with
VerifierSubsystemat wiring time (beforeStartis called). - At
Start,VerifierSubsystemcallsVerifier.VerifyAllfor every registered chain, walking every discovered scope. - If any scope has a broken chain,
StartreturnsAUDIT_CHAIN_BROKENand the server refuses to boot. - After boot, no further verification runs automatically; chain integrity
is maintained by the per-event
prev_hashlinkage.
Worked example: dek.RekeyChain
Section titled “Worked example: dek.RekeyChain”The rekey chain (internal/eventbus/crypto/dek/) is the canonical example.
Its registration shape:
var RekeyChain = auditchain.Chain{ SubjectPrefix: "events.<game>.system.rekey", SelfHashField: "rekey_chain.self_hash", PrevHashField: "rekey_chain.prev_hash", ScopePayloadField: "context",}
var RekeyHandler = auditchain.Handler{ Chain: RekeyChain, SubjectFor: func(scope string) string { ct, cid := splitContextScope(scope) return fmt.Sprintf("events.%s.system.rekey.%s.%s", currentGameID, ct, cid) }, ScopeFromSubject: parseRekeyScopeFromSubject, ScopeFromPayload: parseRekeyScopeFromPayload, Canonicalize: CanonicalizeRekeyPayload, // plain JCS; no empty-form normalization PrevHashOf: extractRekeyPrevHash, SelfHashOf: extractRekeySelfHash,}Registration at wiring time (cmd/holomush/core.go):
verifierSubsystem := auditchain.NewVerifierSubsystem(auditchainRepo, log)policy.RegisterPolicySet(verifierSubsystem) // D's policy_set chaindek.RegisterRekey(verifierSubsystem) // E's rekey chainAdding a new chain
Section titled “Adding a new chain”- Define a
Chainmetadata struct and aHandlerbundle in your owning package (e.g.,internal/eventbus/crypto/kek/). - Implement
SubjectFor,ScopeFromSubject,ScopeFromPayload,Canonicalize,PrevHashOf,SelfHashOfas standalone functions in your package. - Add a
Register<YourChain>(v *auditchain.VerifierSubsystem)function that callsv.Register(handler). - Call
Register<YourChain>(verifierSubsystem)incore.go’s wiring block. - Add a unit test that asserts:
SubjectFor(someScope)starts withevents.<game>.ScopeFromSubject(SubjectFor(scope)) == scopeScopeFromPayload(examplePayload) == scopeRecomputeSelfHashover a fixture matches the storedself_hash
- Add an integration test that verifies the chain survives a round-trip
through
events_auditandVerifierSubsystem.Start.
Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
AUDIT_CHAIN_BROKEN | Boot-time verifier detected a hash-chain break in a registered chain |
AUDIT_CHAIN_SCOPE_MISMATCH | A row’s subject-derived scope differs from its payload-derived scope (INV-E27) |
See also
Section titled “See also”- Crypto Runbook — operator procedure for the rekey chain
- Crypto Monitoring — alert rules for chain-integrity failures
- Sub-epic E design spec §3.6–§3.8 — full primitive design and R6 amendment
- Master spec §4.6 — audit event shapes