reckon_gater_canonical (reckon_gater v2.3.1)

View Source

Canonical encoding for tamper-resistance integrity computations.

Provides deterministic byte representation of Erlang terms so that the same event record always produces identical bytes -- required for reproducible HMAC computation and chain hashing across cluster nodes and across recompiles.

Why this exists

term_to_binary/1 is NOT canonical: map iteration order, atom encoding choices, and ETF minor-version selection can all vary across OTP releases and runtime conditions. Two BEAM nodes computing an HMAC over the "same" event with the default term_to_binary/1 could produce different bytes and disagree on the MAC.

term_to_binary/2 with the deterministic flag (added in OTP 26) sorts map keys lexicographically before encoding, fixes atom encoding, and is documented as stable across nodes. That is what this module relies on.

Algorithm identifier

This module implements the wire format identified to external verifiers as "sha256-deterministic-etf-v1":

  • hash function: SHA-256
  • canonical encoding: term_to_binary/2 with [deterministic, {minor_version, 2}]
  • format version: v1

External consumers reading the chain hash through reckon-gateway can reproduce the canonical bytes if they hold a compatible ETF decoder/encoder -- for cross-language verification (Go / Python / Rust clients) a CBOR-canonical alternate encoding is a future addition (out of scope for 2.1.0).

Domain-separation tags

The encoder prefixes the canonical bytes with a short domain tag that distinguishes between:

  • <<"evt|">> -- event MAC input
  • <<"snap|">> -- snapshot MAC input
  • <<"chain|">> -- chain hash input

Domain separation prevents cross-protocol substitution attacks: an attacker who recovers a snapshot MAC cannot replay it as an event MAC, because the inputs hashed under each were tagged differently.

Summary

Functions

Canonically encode an Erlang term to a deterministic binary.

Canonical bytes to feed into the chain hash computation.

Canonical bytes to feed into an HMAC computation, domain-tagged.

Functions

encode(Term)

-spec encode(term()) -> binary().

Canonically encode an Erlang term to a deterministic binary.

Uses term_to_binary/2 with the deterministic flag (OTP 26+), which sorts map keys before encoding and otherwise stabilises atom and small-integer encoding choices. Same input term always produces byte-identical output, regardless of node, OTP minor version, or process state.

This is the foundation primitive -- both encode_for_mac/2 and encode_for_chain/2 build on top of it by adding domain-separation tags.

encode_for_chain(EventMinusIntegrity, PrevEventHash)

-spec encode_for_chain(term(), binary()) -> iolist().

Canonical bytes to feed into the chain hash computation.

The chain input is:

"chain|" ++ canonical_encode(event_minus_integrity_fields) ++ prev_event_hash

For event version 0 (start of stream), PrevEventHash is the all-zero 32-byte binary -- a stable "genesis" value rather than special-casing on 'undefined'. This means the chain-hash function is a pure function of (event, prev_hash) with no nil/undefined branches in the cryptographic path.

Returns an iolist suitable for crypto:hash(sha256, _).

encode_for_mac(_, EventMinusMac)

-spec encode_for_mac(event | snapshot, term()) -> iolist().

Canonical bytes to feed into an HMAC computation, domain-tagged.

Returns an iolist (cheaper than concatenating to a binary) suitable for direct passing to crypto:mac(hmac, sha256, Key, _).

The Domain parameter is 'event' or 'snapshot' -- extending this list requires a new domain tag constant above.