RequestSeal.Custody.Local.Owner (RequestSeal v0.4.0)

Copy Markdown View Source

Caller-supervised owner of Ed25519 signing handles.

Add {RequestSeal.Custody.Local.Owner, name: MyApp.Custody, keys: keys} to your supervision tree. keys is a keyword list of name: {"ed25519", source} entries. Sources are {:env, variable, encoding}, {:file, path, encoding}, or {:file_from_env, variable, encoding}; the last reads a file path from the variable. Encodings are :base64url (padding optional), :base64 (padded), :hex (either case), or :raw. Only ASCII space, tab, CR, and LF are trimmed from text encodings; raw bytes are unchanged. Base64 encodings must be canonical. Decoded seeds must be exactly 32 bytes. Source inputs are limited to 4,096 bytes, including whitespace. There is no guess-decoding.

Files must be regular, not symbolic links, with mode 0600 or stricter (no execute, group, other, or special permission bits). Use {:file, ...} in production: environment seeds are inherited by OS child processes. Avoid :raw environment seeds; environment values cannot safely represent arbitrary binary data. Keep source paths and their parent directories under trusted control during startup.

Bad keys do not prevent startup. status/1 reports :ready, :unconfigured (missing or empty variable, missing file, or stopped holder), or {:error, reason} with :invalid_seed, :insecure_file, or :unreadable. fetch/2 returns {:error, :unconfigured} for a missing or failed key. Calls to a stopped or busy owner return {:error, :owner_unavailable} from fetch/2 and status/1; ready?/2 is false. No signing capability is issued for a bad source. Key names, variable names, and paths are configuration, never seed values.

The owner becomes sensitive before reading sources. Each decoded seed goes directly to RequestSeal.Custody.Local.new/2; only handles and bounded statuses remain in state. Holders monitor this owner, not the process calling fetch/2. Sources are read on every start and restart; environment variables are never deleted. Registration after startup is unsupported. Fetch per operation, or fetch again and retry once on custody :key_not_found after an owner restart. During restart, :owner_unavailable means wait for the supervised owner to become available before fetching again. A file that disappears or changes identity after its initial checks reports :insecure_file. Abnormal exits retain a bounded atom reason; private reason terms are discarded. Loading the library starts nothing.

Summary

Functions

Build a child specification containing source descriptors, never loaded seeds.

Fetch a handle; distinguish an unconfigured key from an unavailable owner.

Report whether a handle is available for the named key.

Start a linked owner with a fixed set of source descriptors.

Return each configured key's bounded status.

Types

encoding()

@type encoding() :: :base64url | :base64 | :hex | :raw

key_status()

@type key_status() ::
  :ready
  | :unconfigured
  | {:error, :invalid_seed | :insecure_file | :unreadable}

options()

@type options() :: [name: atom(), keys: [{atom(), {binary(), source()}}]]

source()

@type source() :: {:env | :file | :file_from_env, binary(), encoding()}

Functions

child_spec(init_arg)

@spec child_spec(options()) :: Supervisor.child_spec()

Build a child specification containing source descriptors, never loaded seeds.

fetch(owner, name)

@spec fetch(GenServer.server(), atom()) ::
  {:ok, RequestSeal.KeyHandle.t()}
  | {:error, :unconfigured | :owner_unavailable}

Fetch a handle; distinguish an unconfigured key from an unavailable owner.

ready?(owner, name)

@spec ready?(GenServer.server(), atom()) :: boolean()

Report whether a handle is available for the named key.

start_link(opts)

@spec start_link(options()) :: GenServer.on_start()

Start a linked owner with a fixed set of source descriptors.

status(owner)

@spec status(GenServer.server()) ::
  %{required(atom()) => key_status()} | {:error, :owner_unavailable}

Return each configured key's bounded status.