reckon_gater_integrity (reckon_gater v2.3.1)
View SourceTamper-resistance helpers for events and snapshots.
Computes and verifies the integrity fields introduced in reckon-gater 2.1.0 -- prev_event_hash (chain), mac (HMAC), and snapshot anchor_hash / mac.
This module is pure functions. It holds no state, opens no connections, and starts no processes. The HMAC key is passed in explicitly by callers (typically reckon-db loads it from persistent_term at the write/read boundary).
Algorithm
See reckon_gater_canonical for the canonical-encoding contract. Algorithm identifier:
sha256-deterministic-etf-v1
This identifier is exposed to external verifiers via the gateway's GetServerInfo RPC.
Cryptographic primitives
Uses OTP's crypto module directly:
- crypto:hash(sha256, _) for chain hashing
- crypto:mac(hmac, sha256, Key, _) for HMAC
- crypto:hash_equals/2 for constant-time MAC comparison
These are OpenSSL-backed NIFs (already C-implemented in the BEAM crypto application). Per-event integrity overhead is approximately 5-10 microseconds on commodity hardware -- see plans/PLAN_TAMPER_RESISTANCE.md in reckon-db for the performance budget.
Key handling
The HMAC key is a binary, conventionally 32 random bytes. Callers should never log it, include it in error tuples, or send it over the wire. This module accepts it as a function argument and does not retain it.
The KeyId integer accompanying the MAC is reserved for future key-rotation support (2.2.0); in 2.1.0 callers should always pass KeyId = 1.
Summary
Functions
Compute the chain hash of an event.
Compute the HMAC for an event.
Compute the HMAC for a snapshot.
The chain-hash predecessor value for event version 0.
Is this an event from before 2.1.0 (no integrity fields)?
Is this a snapshot from before 2.1.0 (no integrity fields)?
Verify both the chain hash and the HMAC of an event.
Verify a snapshot's MAC and anchor hash.
Types
-type integrity_failure_kind() ::
mac_mismatch | chain_mismatch | missing_integrity | snapshot_anchor_mismatch |
snapshot_mac_mismatch.
-type integrity_failure_layer() :: storage | snapshot | replay | gateway.
-type integrity_violation() :: {integrity_violation, #{layer := integrity_failure_layer(), stream_id := binary(), version := non_neg_integer() | undefined, kind := integrity_failure_kind(), context => map()}}.
-type key() :: binary().
-type key_id() :: pos_integer().
Functions
-spec compute_chain_hash(#event{event_id :: binary(), event_type :: binary(), stream_id :: binary(), version :: non_neg_integer(), data :: map() | binary(), metadata :: map(), tags :: [binary()] | undefined, timestamp :: integer(), epoch_us :: integer(), data_content_type :: binary(), metadata_content_type :: binary(), prev_event_hash :: binary() | undefined, mac :: {KeyId :: non_neg_integer(), MacBytes :: binary()} | undefined, signature :: binary() | undefined}, binary()) -> binary().
Compute the chain hash of an event.
The returned hash is what the NEXT event in the stream will carry as its prev_event_hash field. Equivalently: it is what a verifier must use as the running tip when verifying the next event.
The integrity fields on the input record (prev_event_hash, mac, signature) are stripped before hashing, so this function produces the same result whether they are populated or not. What matters is the PrevHash argument -- that is what links this event to its predecessor in the chain.
For event version 0, callers pass genesis_prev_hash/0 (32 zero bytes). For event version N > 0, callers pass the chain hash of event N-1 (the running tip cached by the writer).
Note this function is NOT used to compute what goes into the input event's own prev_event_hash field -- that field carries the chain hash of the PREDECESSOR, not of this event. See the #event.prev_event_hash documentation in reckon_gater_types.hrl.
-spec compute_event_mac(#event{event_id :: binary(), event_type :: binary(), stream_id :: binary(), version :: non_neg_integer(), data :: map() | binary(), metadata :: map(), tags :: [binary()] | undefined, timestamp :: integer(), epoch_us :: integer(), data_content_type :: binary(), metadata_content_type :: binary(), prev_event_hash :: binary() | undefined, mac :: {KeyId :: non_neg_integer(), MacBytes :: binary()} | undefined, signature :: binary() | undefined}, key()) -> mac_tuple().
Compute the HMAC for an event.
The event should already have prev_event_hash set to its final value. The MAC is computed over the event with mac and signature blanked.
Returns the canonical {KeyId, MacBytes} tuple suitable for storing in the #event.mac field.
-spec compute_snapshot_mac(#snapshot{stream_id :: binary(), version :: non_neg_integer(), data :: map() | binary(), metadata :: map(), timestamp :: integer(), anchor_hash :: binary() | undefined, mac :: {KeyId :: non_neg_integer(), MacBytes :: binary()} | undefined}, key()) -> mac_tuple().
Compute the HMAC for a snapshot.
The snapshot should already have anchor_hash set. The MAC is computed over the snapshot with mac blanked.
-spec genesis_prev_hash() -> binary().
The chain-hash predecessor value for event version 0.
32 zero bytes. Exported so callers don't have to reach into the module's macros to produce it.
-spec is_legacy_event(#event{event_id :: binary(), event_type :: binary(), stream_id :: binary(), version :: non_neg_integer(), data :: map() | binary(), metadata :: map(), tags :: [binary()] | undefined, timestamp :: integer(), epoch_us :: integer(), data_content_type :: binary(), metadata_content_type :: binary(), prev_event_hash :: binary() | undefined, mac :: {KeyId :: non_neg_integer(), MacBytes :: binary()} | undefined, signature :: binary() | undefined}) -> boolean().
Is this an event from before 2.1.0 (no integrity fields)?
-spec is_legacy_snapshot(#snapshot{stream_id :: binary(), version :: non_neg_integer(), data :: map() | binary(), metadata :: map(), timestamp :: integer(), anchor_hash :: binary() | undefined, mac :: {KeyId :: non_neg_integer(), MacBytes :: binary()} | undefined}) -> boolean().
Is this a snapshot from before 2.1.0 (no integrity fields)?
-spec verify_event(#event{event_id :: binary(), event_type :: binary(), stream_id :: binary(), version :: non_neg_integer(), data :: map() | binary(), metadata :: map(), tags :: [binary()] | undefined, timestamp :: integer(), epoch_us :: integer(), data_content_type :: binary(), metadata_content_type :: binary(), prev_event_hash :: binary() | undefined, mac :: {KeyId :: non_neg_integer(), MacBytes :: binary()} | undefined, signature :: binary() | undefined}, binary(), key()) -> ok | integrity_violation().
Verify both the chain hash and the HMAC of an event.
PrevHash is the chain hash of the prior event in the stream as observed by the verifier (cached running tip, or freshly computed from the predecessor). For event version 0, callers pass genesis_prev_hash/0.
Returns 'ok' on success, or an integrity_violation tuple on any failure. The error map records which check failed and where -- the 'layer' field is 'storage' here; callers at other surfaces (snapshot, replay, gateway) can re-wrap with their own layer if needed.
-spec verify_snapshot(#snapshot{stream_id :: binary(), version :: non_neg_integer(), data :: map() | binary(), metadata :: map(), timestamp :: integer(), anchor_hash :: binary() | undefined, mac :: {KeyId :: non_neg_integer(), MacBytes :: binary()} | undefined}, binary(), key()) -> ok | integrity_violation().
Verify a snapshot's MAC and anchor hash.
ActualChainHashAtVersion is the chain hash at the snapshot's version as observed by re-reading the underlying stream -- if the snapshot is honest, this should match Snapshot#snapshot.anchor_hash.
Returns 'ok' on success, or an integrity_violation tuple on failure.