RequestSeal.Custody behaviour (RequestSeal v0.1.0)

Copy Markdown View Source

Caller-owned signing, secret verification, key unwrapping, and public metadata.

Each callback runs in a fresh runner, never linked to the caller. A monitored middle process traps exits, monitors the caller before linking the runner, and terminates the runner when the caller dies, even when a callback is blocked. Both processes set Process.flag(:sensitive, true) before any custody work, for every operation. Runner exceptions are reduced to :custodian_failure without logging exception text. Deadline expiry kills the runner and drains its uniquely tagged result. Loading the library starts no process, store, or background work. Local handle construction explicitly starts its key holder.

sign/3, verify/4, and unwrap/3 accept only timeout: milliseconds (default 5,000; 1–300,000, no infinity). Unknown and duplicate options reject. Public-key and identity resolution use the default deadline. Exact input bytes are limited to 1,048,576; signatures and public container inputs are bounded to 16,384 bytes. Signing returns bytes; verification establishes mathematical validity only. Neither establishes identity attribution, authorization, or replay protection.

Custodians are trusted code: they must return only public keys and nonsecret identities, never log secrets, and must propagate the context deadline and cancellation to any additional work they own. Killing the worker closes sockets it owns; it cannot revoke work already accepted by an external peer.

Summary

Functions

Resolve internal trusted equivalence; keep the value out of general results.

Resolve validated public-only material; symmetric keys return :no_public_key.

Sign exact bounded bytes with the algorithm bound to a handle.

Unwrap a bounded RSA-OAEP encrypted key using the handle's exact algorithm.

Verify exact bytes through secret custody or the custodian's public key.

Types

algorithm()

@type algorithm() :: RequestSeal.Crypto.algorithm() | unwrap_algorithm()

unwrap_algorithm()

@type unwrap_algorithm() :: {:jwe, binary()}

Callbacks

identity(term)

@callback identity(term()) :: RequestSeal.KeyIdentity.t()

public_key(term)

@callback public_key(term()) ::
  {:ok, RequestSeal.PublicKey.t()}
  | {:error, RequestSeal.Custody.Error.reason()}

sign(term, algorithm, binary, t)

unwrap(term, unwrap_algorithm, binary, t)

(optional)
@callback unwrap(term(), unwrap_algorithm(), binary(), RequestSeal.Custody.Context.t()) ::
  {:ok, binary()} | {:error, RequestSeal.Custody.Error.reason()}

verify(term, algorithm, binary, binary, t)

(optional)

Functions

identity(handle)

Resolve internal trusted equivalence; keep the value out of general results.

public_key(handle)

@spec public_key(RequestSeal.KeyHandle.t()) ::
  {:ok, RequestSeal.PublicKey.t()} | {:error, RequestSeal.Custody.Error.t()}

Resolve validated public-only material; symmetric keys return :no_public_key.

sign(handle, bytes, opts \\ [])

@spec sign(RequestSeal.KeyHandle.t(), binary(), keyword()) ::
  {:ok, binary()} | {:error, RequestSeal.Custody.Error.t()}

Sign exact bounded bytes with the algorithm bound to a handle.

unwrap(handle, encrypted_key, opts \\ [])

@spec unwrap(RequestSeal.KeyHandle.t(), binary(), keyword()) ::
  {:ok, binary()} | {:error, RequestSeal.Custody.Error.t()}

Unwrap a bounded RSA-OAEP encrypted key using the handle's exact algorithm.

Only {:jwe, "RSA-OAEP"} and {:jwe, "RSA-OAEP-256"} handles with :unwrap capability are accepted. Encrypted keys are 1–1,024 bytes and must match the RSA modulus width inside the custodian. Returns unwrapped bytes, never the private key. OAEP failures return :decryption_failed; callers must authenticate content before exposing plaintext. RequestSeal.JOSE.JWE performs that step with a random fallback CEK on unwrap failure.

Unwrapped bytes pass from the sensitive local holder through the sensitive custody runner and middle process to the caller. On the JWE handle path that caller is JWE's sensitive worker, which authenticates content before returning plaintext. A direct unwrap/3 caller receives the bytes and owns their protection. An empty OAEP plaintext returns :decryption_failed.

JWE deliberately maps a released or unavailable handle to the same error as a forged message. Operators should check RequestSeal.Custody.public_key(handle) at startup to distinguish custody availability from message rejection; {:ok, public_key} confirms public resolution, while {:error, error} exposes the custody failure. Deadline expiration remains a distinct JWE error.

verify(handle, bytes, signature, opts \\ [])

@spec verify(RequestSeal.KeyHandle.t(), binary(), binary(), keyword()) ::
  :ok | {:error, RequestSeal.Custody.Error.t()}

Verify exact bytes through secret custody or the custodian's public key.