Skip to content
↑↓Navigate↵SelectescClose

Audit Event Contract

Audit event schema, canonical encoding, and Merkle/signing rules

This page is the source of truth for the audit event schema used by both Aegis and nexus. It documents the wire contract of the vortex-audit-core crate, which both products embed.

  • Event body schema (fields, types, encoding).
  • Canonical byte encoding for hashing and signing.
  • Hash function and Merkle construction.
  • Signing envelope and verification rules.

Storage substrate, retention, and WORM policy are per-product. Current product implementations use these substrates:

  • Aegis: SQLite + local FS WORM.
  • nexus: Postgres hot path + S3/MinIO WORM.

Both products consume the same crate, emit events with the same fields, and produce batch envelopes that the other product’s verifier can read. Cross-product verification (see below) checks this property before changes ship.

A single audit event is a struct with the fields below. Field order is fixed; canonical encoding depends on this order.

Field Type Required Description
schema_version u16 yes Starts at 1. Bumped only on a breaking schema change.
tenant_id string yes Opaque tenant identifier. For Aegis MVP this is "default".
event_id u128 ULID yes Unique per event, monotonic within a tenant.
ts_unix_nanos u64 yes Wall clock at emission, unix nanoseconds. Not cryptographically trusted; ordering is provided by event_id and batch chain.
kind string yes Event taxonomy key, e.g. request.completed, model.loaded, auth.denied. Full taxonomy lives in each product’s audit documentation.
actor Actor yes Who caused the event. See Actor schema below.
subject Subject yes What the event is about. See Subject schema below.
classification string yes Classification label in the normalized form produced by vortex-classification (lowercase kebab-case). The vocabulary is tenant-configured; producers record the resolved label for the event’s scope, or internal for service-internal events.
attrs map<string, cbor> yes Free-form attributes. Keys are lowercase snake_case. Values are CBOR-native scalars, arrays, or maps.
causation_id u128? no Optional pointer to the parent event’s event_id.
Field Type Description
kind string user, service, cli, system.
id string Stable identifier within kind.
principal_hash [u8; 32]? Optional BLAKE3 hash of the authenticated principal material when the raw id is sensitive.
Field Type Description
kind string request, model, session, apikey, policy, config, org, project, providerkey, user, membership (lowercase wire labels of SubjectKind).
id string Subject id (e.g. request_id, model_id, org_id).

Data-access reads of the audit log itself are recorded as data_access.audit_viewed events with an org subject and org_id, view (list | detail | proof), and optional event_id attrs.

Canonical encoding for hashing and signing

Section titled “Canonical encoding for hashing and signing”

Events and batches are serialized with deterministic CBOR (RFC 8949 §4.2.1 core deterministic rules):

  • Integers encoded in the shortest form.
  • Map keys sorted lexicographically by their encoded bytes.
  • No indefinite-length items.
  • No tags except those explicitly required by the schema.

The vortex-audit-core crate exposes a single encode_event(&Event) -> Vec<u8> function that is the only supported way to produce canonical bytes. A verifier that round-trips an event through encode_event MUST produce byte-identical output to the original.

BLAKE3 with default 32-byte output for:

  • per-event content hash,
  • Merkle tree nodes,
  • batch roots.

SHA-256 appears only in model provenance as a compatibility sidecar for external consumers. Audit uses BLAKE3 exclusively.

For a batch of events [e_1, ..., e_n]:

  1. Compute leaf_i = BLAKE3(0x00 || encode_event(e_i)).
  2. Internal nodes: node = BLAKE3(0x01 || left || right).
  3. Odd-count levels duplicate the last leaf up to the next power of two (standard RFC 6962-style, except using BLAKE3).
  4. Root is 32 bytes.

The leading domain-separation byte (0x00 for leaves, 0x01 for internal) prevents second-preimage attacks across levels.

Each sealed batch produces an envelope structure encoded with the same deterministic CBOR rules:

Field Type Description
schema_version u16 Starts at 1.
tenant_id string Matches all events in the batch.
batch_seq u64 Monotonic per-tenant. First batch is 0.
prev_merkle_root [u8; 32] All zeros for batch_seq == 0.
merkle_root [u8; 32] Root of the Merkle tree over this batch’s events.
event_count u32 Number of leaves.
sealed_at_unix_nanos u64 Emission time of the seal.
signer_key_id string Identifier of the signing key used.
signature [u8; 64] ed25519 signature over encode_envelope_for_signing(envelope).

encode_envelope_for_signing is encode_envelope with the signature field omitted.

  • Algorithm: ed25519 (RFC 8032).
  • Keys are per-product and per-deployment. Products document where signing keys are stored and how trust roots are distributed.
  • Key rotation: a new signer_key_id starts a new chain segment. Verifier accepts a chain that changes signer_key_id at a batch boundary if the new key is present in the product’s trust root.

A verifier that implements this contract MUST:

  1. Decode each batch envelope with deterministic CBOR.
  2. For each batch, re-encode each event with encode_event, recompute leaf hashes and Merkle root, and fail if the recomputed root does not match merkle_root.
  3. Verify the ed25519 signature against the signer key identified by signer_key_id.
  4. Verify prev_merkle_root equals the previous batch’s merkle_root (or is zero for batch_seq == 0).
  5. Verify batch_seq is strictly monotonic per tenant with no gaps.

The vortex-audit-core crate provides a reference verifier. Both products’ CLI verifier commands use it.

Cross-product verification produces a sealed batch from one product and verifies it with the other product’s verifier binary, and vice versa. This proves the schema is truly shared and catches drift before it ships.

Any change to:

  • field set of Event or BatchEnvelope,
  • encoding rules,
  • hash function,
  • Merkle construction,
  • signature algorithm,

is a breaking change. Procedure:

  1. Bump schema_version.
  2. Update the reference verifier to accept both old and new versions for a deprecation window.
  3. Coordinate a synchronous release of vortex-audit-core across Aegis and nexus.

See also Audit Proof Format for the inclusion-proof structure this contract underpins, Runbook: Audit Verifier Failure for recovering from nexus-audit-verify failures, and Audit Archive Export for copying archive objects to customer-controlled storage.