SigilGuard. Audit
(SigilGuard v0.2.0)
View Source
Tamper-evident audit logging for the SIGIL protocol.
Provides structured audit events with HMAC-SHA256 chain integrity. Each event's HMAC incorporates the previous event's HMAC, forming a hash chain that detects any tampering, insertion, or deletion of events.
Chain Integrity
The HMAC chain works as follows:
- First event:
HMAC(key, canonical_bytes(event) <> "genesis") - Subsequent events:
HMAC(key, canonical_bytes(event) <> prev_hmac)
Verification walks the chain, enforcing that each event links to its
actual predecessor (the first event's prev_hmac must be nil, every
later event's must equal the previous event's hmac) and recomputing
each HMAC. Within the verified sequence this detects modification,
insertion, deletion, and reordering of events.
What the chain cannot detect
Truncation of the chain tail is undetectable from the events alone:
a chain with its last events removed is still a valid chain. To defend
against truncation, persist the most recent hmac out of band and
compare it to the last event's, or verify continuation segments
against a stored tip via the :prev_hmac option of verify_chain/3.
Audit Logger Behaviour
Implement SigilGuard.Audit.Logger to persist audit events to your
preferred backend (database, file, external service):
defmodule MyApp.AuditLogger do
@behaviour SigilGuard.Audit.Logger
@impl true
def log(event) do
MyApp.Repo.insert!(event_to_schema(event))
:ok
end
end
Summary
Functions
Build a chain of signed events from a list of unsigned events.
Produce the canonical byte representation of an event for HMAC computation.
Create a new audit event (unsigned).
Sign an event with an HMAC, linking it to the previous event in the chain.
Verify the integrity of an audit event chain.
Types
@type event_type() :: String.t()
@type t() :: %SigilGuard.Audit{ action: String.t(), action_info: SigilGuard.Audit.Action.t() | nil, actor: String.t(), actor_info: SigilGuard.Audit.Actor.t() | nil, event_type: SigilGuard.Audit.EventType.t() | nil, hmac: String.t() | nil, id: String.t(), metadata: map(), prev_hmac: String.t() | nil, result: String.t(), result_info: SigilGuard.Audit.ExecutionResult.t() | nil, timestamp: String.t(), type: event_type() }
Functions
Build a chain of signed events from a list of unsigned events.
Signs each event in sequence, linking each to the previous via HMAC.
Examples
unsigned = [event1, event2, event3]
signed = SigilGuard.Audit.build_chain(unsigned, secret_key)
:ok = SigilGuard.Audit.verify_chain(signed, secret_key)
Produce the canonical byte representation of an event for HMAC computation.
Fields are serialized as compact JSON with lexicographic key order.
Only id, type, actor, action, result, timestamp are included
(not hmac, prev_hmac, or metadata).
Create a new audit event (unsigned).
The event gets a unique ID and timestamp but no HMAC yet.
Call sign_event/2 or sign_event/3 to add chain integrity.
Examples
event = SigilGuard.Audit.new_event("mcp.tool_call", "did:web:alice", "read_file", "success")
Sign an event with an HMAC, linking it to the previous event in the chain.
For the first event in a chain, pass nil as prev_hmac.
Examples
# First event in chain
signed = SigilGuard.Audit.sign_event(event, secret_key)
# Subsequent events
signed = SigilGuard.Audit.sign_event(event, secret_key, prev_event.hmac)
@spec verify_chain([t()], binary(), keyword()) :: :ok | {:broken, non_neg_integer()}
Verify the integrity of an audit event chain.
Returns :ok if the chain is contiguous and every HMAC is valid, or
{:broken, index} identifying the first event that fails.
An event fails verification if its prev_hmac does not link to its
actual predecessor's hmac (or to the :prev_hmac anchor/genesis for
the first event), or if its recomputed HMAC does not match. This
detects tampered, deleted, inserted, and reordered events. Truncation
of the chain tail cannot be detected — see the module documentation.
Options
:prev_hmac— verify a segment that continues from a known tip rather than from genesis. Pass thehmacof the event immediately preceding the segment (default:nil, the chain starts at genesis).
Examples
:ok = SigilGuard.Audit.verify_chain(events, secret_key)
{:broken, 3} = SigilGuard.Audit.verify_chain(tampered_events, secret_key)
# Verify a continuation segment against a persisted tip
:ok = SigilGuard.Audit.verify_chain(segment, secret_key, prev_hmac: stored_tip)