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:

  1. First event: HMAC(key, canonical_bytes(event) <> "genesis")
  2. 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

event_type()

@type event_type() :: String.t()

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_chain(events, key)

@spec build_chain([t()], binary()) :: [t()]

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)

canonical_bytes(event)

@spec canonical_bytes(t()) :: binary()

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).

new_event(type, actor, action, result, metadata \\ %{})

@spec new_event(event_type(), String.t(), String.t(), String.t(), map()) :: t()

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_event(event, key, prev_hmac \\ nil)

@spec sign_event(t(), binary(), String.t() | nil) :: t()

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)

verify_chain(events, key, opts \\ [])

@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 the hmac of 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)