Chronicle.Keyring behaviour (chronicle v0.1.2)

Copy Markdown

Behaviour and dispatcher for versioned audit signing keys.

A keyring returns descriptors, not only secret bytes. Each descriptor carries the inclusive ledger sequence range in which the key is valid. Custom keyrings can resolve secrets from KMS, Vault, PKCS#11, or another secret manager without exposing that integration to the rest of Chronicle.

Configure a module or {module, options}:

integrity: [
  ledger: "primary",
  keyring: {MyApp.AuditKeyring, vault: MyApp.Vault}
]

The built-in Chronicle.Keyring.Config is used when :keyring is omitted.

Exactly one key, or an error

current/3 requires precisely one key to be valid at a given sequence. No key is an error, and so is more than one — reported as :overlapping_key_epochs rather than resolved by preferring the newest or the first match.

That refusal is the point. If two keys are both acceptable at a position, then compromising either one is enough to write history that verifies, and the ledger can no longer say which key was supposed to have signed. An ambiguous signing policy is not a smaller version of a correct one; it is the vulnerability. Overlap is a configuration mistake, and guessing would hide it until it mattered.

A keyring is somebody else's code

Custom implementations reach secret managers over the network and can fail in ways this module cannot anticipate, so results are treated as untrusted input: the return value must be a Chronicle.Key, fetch/4 confirms the descriptor carries the id that was actually requested, and exceptions are converted into {:error, {:keyring_failure, exception_module}} rather than escaping into the middle of a domain transaction. Exception messages and fields are discarded because secret-manager clients may put key material in them.

Summary

Types

options()

@type options() :: keyword()

result()

@type result() :: {:ok, Chronicle.Key.t()} | {:error, term()}

Callbacks

current(t, pos_integer, options)

@callback current(String.t(), pos_integer(), options()) :: result()

fetch(t, t, options)

@callback fetch(String.t(), String.t(), options()) :: result()

Functions

current(ledger, sequence, integrity_opts)

@spec current(String.t(), pos_integer(), keyword()) :: result()

epoch_policy?(integrity_opts)

@spec epoch_policy?(keyword()) :: boolean()

Returns whether explicit epoch policy is configured.

This is useful for operational diagnostics. A single unbounded key remains supported for initial deployments, but it cannot enforce a rotation boundary.

fetch(ledger, key_id, sequence, integrity_opts)

@spec fetch(String.t(), String.t(), pos_integer() | nil, keyword()) :: result()