Attesto.Keystore behaviour (Attesto v1.15.0)

Copy Markdown View Source

The behaviour Attesto uses to obtain signing and verification keys.

A keystore answers two questions:

  • What key do we sign new tokens with? signing_pem/0 returns the private signing key PEM. Attesto derives the public half and the kid from it (Attesto.Key), so the keystore never has to compute a thumbprint.

  • What keys may verify a presented token? verification_pems/0 returns a list of PEMs (private or public) whose public halves are trusted. With a single key this is just [signing_pem()]. During a key rotation it carries both the outgoing and incoming keys so tokens minted under either verify, and Attesto.Token.verify/3 selects the right one by the JWS header kid.

Implementations decide where keys come from - an environment variable, a secrets manager, or a file. The legacy signing path consumes a private PEM. A module backed by an HSM, KMS, enclave, or other non-extractable custody system instead implements Attesto.Signer alongside this behaviour and may omit signing_pem/0; its verification_pems/0 still publishes public keys.

A FAPI deployment that uses RSA must provision every server signing and verification key with a modulus of at least 2048 bits. The generic keystore contract remains profile-neutral so non-FAPI applications can make their own compatibility decisions.

Attesto.Keystore.Static is a ready-made implementation for the common single-key (or manually-rotated) case.

Summary

Callbacks

Optional per-key JOSE algorithm metadata, keyed by RFC 7638 kid.

Optional global algorithm for the current signing key.

The private signing-key PEM used to sign newly issued tokens.

Optional operator metadata for verification keys, keyed by RFC 7638 kid.

The PEMs (private or public) whose public halves are trusted to verify a presented token. MUST include the public half of whatever signing_pem/0 currently returns.

Functions

Inspect a keystore's rotation contract.

Types

key_metadata()

@type key_metadata() :: %{optional(:not_after) => DateTime.t() | non_neg_integer()}

rotation_health()

@type rotation_health() :: %{
  status: :healthy | :warning | :invalid,
  signing_kid: String.t(),
  overlap?: boolean(),
  key_count: non_neg_integer(),
  keys: [rotation_key()],
  unknown_metadata_kids: [String.t()],
  issues: [atom()]
}

rotation_key()

@type rotation_key() :: %{
  kid: String.t(),
  alg: String.t(),
  current?: boolean(),
  not_after: DateTime.t() | nil,
  state: :current | :overlap | :expiring | :expired
}

Callbacks

key_algs()

(optional)
@callback key_algs() :: %{required(String.t()) => String.t()} | keyword(String.t())

Optional per-key JOSE algorithm metadata, keyed by RFC 7638 kid.

When omitted, Attesto infers an algorithm from the public key type and curve: RSA -> RS256, P-256 -> ES256, P-384 -> ES384, P-521 -> ES512, and Ed25519/Ed448 -> legacy EdDSA. Use this callback to label RSA keys that should verify as PS256, select RFC 9864 Ed25519 / Ed448 for the matching curve, or make a rotation window explicit. Ed448 deployments must configure JOSE with Curve448 and SHAKE256 support.

signing_alg()

(optional)
@callback signing_alg() :: String.t()

Optional global algorithm for the current signing key.

This is a convenience for single-key RSA deployments that want PS256 without precomputing the signing key's kid. A custom keystore whose value differs from key inference MUST expose the same binding through key_algs/0 so its newly minted tokens also verify. Attesto.Keystore.Static does that automatically. Verification otherwise uses key_algs/0 when present, then key inference.

signing_pem()

(optional)
@callback signing_pem() :: String.t()

The private signing-key PEM used to sign newly issued tokens.

The key must support one of the asymmetric algorithms accepted by Attesto.SigningAlg. FAPI server deployments using RSA require a modulus of at least 2048 bits.

verification_key_metadata()

(optional)
@callback verification_key_metadata() :: %{optional(String.t()) => key_metadata()}

Optional operator metadata for verification keys, keyed by RFC 7638 kid.

:not_after is either a UTC DateTime or a Unix timestamp. It is operational metadata: JWT verification continues to use token validity and the published key set, while rotation_health/2 surfaces expired or soon-to-expire keys. Keys must be non-empty string kid values that exist in verification_pems/0; malformed and unknown metadata fails closed.

verification_pems()

@callback verification_pems() :: [String.t()]

The PEMs (private or public) whose public halves are trusted to verify a presented token. MUST include the public half of whatever signing_pem/0 currently returns.

Functions

rotation_health(keystore, opts \\ [])

@spec rotation_health(
  module(),
  keyword()
) :: rotation_health()

Inspect a keystore's rotation contract.

The current signing key must appear in verification_pems/0; otherwise newly issued tokens cannot be verified and the result is :invalid. More than one verification key is reported as an active overlap window. Optional verification_key_metadata/0 expiry values turn expired and soon-to-expire keys into explicit health issues. An expired current signing key and metadata for an unknown key make the result :invalid; expired overlap keys warn.

Options:

  • :now - UTC DateTime or Unix timestamp; defaults to the current time.
  • :expiry_warning_seconds - warning horizon; defaults to seven days.