Audit Event Contract
Audit event schema, canonical encoding, and Merkle/signing rules
Esta página aún no está disponible en tu idioma.
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.
Scope of this contract
Section titled “Scope of this contract”- 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.
Event body (wire schema)
Section titled “Event body (wire schema)”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. |
Subject
Section titled “Subject”| 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.
Hash function
Section titled “Hash function”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.
Merkle construction
Section titled “Merkle construction”For a batch of events [e_1, ..., e_n]:
- Compute
leaf_i = BLAKE3(0x00 || encode_event(e_i)). - Internal nodes:
node = BLAKE3(0x01 || left || right). - Odd-count levels duplicate the last leaf up to the next power of two (standard RFC 6962-style, except using BLAKE3).
- Root is 32 bytes.
The leading domain-separation byte (0x00 for leaves, 0x01 for internal) prevents second-preimage attacks across levels.
Batch envelope
Section titled “Batch envelope”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.
Signing
Section titled “Signing”- 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_idstarts a new chain segment. Verifier accepts a chain that changessigner_key_idat a batch boundary if the new key is present in the product’s trust root.
Verifier contract
Section titled “Verifier contract”A verifier that implements this contract MUST:
- Decode each batch envelope with deterministic CBOR.
- For each batch, re-encode each event with
encode_event, recompute leaf hashes and Merkle root, and fail if the recomputed root does not matchmerkle_root. - Verify the ed25519 signature against the signer key identified by
signer_key_id. - Verify
prev_merkle_rootequals the previous batch’smerkle_root(or is zero forbatch_seq == 0). - Verify
batch_seqis 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
Section titled “Cross-product verification”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.
Breaking changes
Section titled “Breaking changes”Any change to:
- field set of
EventorBatchEnvelope, - encoding rules,
- hash function,
- Merkle construction,
- signature algorithm,
is a breaking change. Procedure:
- Bump
schema_version. - Update the reference verifier to accept both old and new versions for a deprecation window.
- Coordinate a synchronous release of
vortex-audit-coreacross 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.
