RequestSeal.Custody.Local (RequestSeal v0.2.0)

Copy Markdown View Source

Caller-owned OTP private keys and symmetric secrets, held in sensitive processes.

new/3 accepts the signing descriptors documented by RequestSeal.Crypto. It also accepts {:jwe, "RSA-OAEP"} or {:jwe, "RSA-OAEP-256"} with {:rsa, otp_private_key} for unwrap-only custody. RSA keys are 2048–8192 bits; each handle binds exactly one algorithm and cannot sign or verify. import/4 accepts a single unencrypted private PEM (PKCS #1 RSA, SEC1 EC, PKCS #8 RSA/RSASSA-PSS/EC/Ed25519), PKCS #8 DER (:der), or private JWK map. Public-only material, inconsistent private components, algorithm/type/curve mismatches, and encrypted containers reject. A PSS-only private key never becomes a PKCS #1 signing key; parameter-constrained PSS containers reject rather than dropping restrictions. Imported EC/OKP public components must agree with the derived private key. PKCS #8 v2 (OneAsymmetricKey) containers reject with :unsupported_format.

All six HTTP algorithms and the eight explicit JWS extensions in Crypto are supported. Construction derives a public key and performs an actual sign/verify before returning a signing handle. Unwrap construction validates RSA CRT components and performs an actual OAEP wrap/unwrap. Only equivalence: nonsecret_binary is accepted, and only for HMAC (1–256 bytes). Absent HMAC equivalence remains :unknown; no value is derived from the secret. HMAC verification stays inside the custodian.

PEM/DER inputs are at most 16,384 bytes. JWK maps have at most 32 fields; components must be canonical unpadded Base64url. alg, use, and key_ops restrictions must permit the selected operation. For OAEP, use must be absent or enc, and key_ops must be absent or permit decrypt or unwrapKey; signing operations reject. Public metadata retains these restrictions. Private JWK metadata is checked at import; the handle's capabilities are limited to its permitted operations. No private key export, global store, server, or telemetry is provided.

Each successful construction starts one unsupervised holder process. It marks itself sensitive before receiving key material; the handle references only its PID and a random 32-byte token. The holder performs OTP cryptography and returns signature bytes, unwrapped key bytes, verification results, or public metadata, never private state. RequestSeal.Custody.unwrap/3 shares signing deadlines, cancellation, and safe errors. RSA-OAEP uses SHA-1/MGF1 SHA-1 and RSA-OAEP-256 uses SHA-256/MGF1 SHA-256, both with an empty label, as defined by RFC 7518 Section 4.3. Process and system introspection do not export that state.

Holders live until release/1 or the creating process exits, including after a handle is transferred. Dropping a handle does not stop its holder. A long-lived owner such as a GenServer must call release/1 for handles it no longer needs, or create handles once at startup and reuse them for its lifetime. Operations against a stopped holder return :key_not_found. No supervisor, registry, application callback, or process starts when the library is loaded.

Decryption custody

Import a recipient key once in its long-lived owner's process, then return the handle from the JWE policy resolver:

{:ok, handle} = RequestSeal.Custody.Local.import({:jwe, "RSA-OAEP-256"}, private_pem, :pem)
policy = %{
  algorithms: ["RSA-OAEP-256"],
  encryption: ["A256GCM"],
  max_plaintext: 1_048_576,
  timeout: 5_000,
  key_resolver: fn _ -> {:ok, %{algorithm: "RSA-OAEP-256", key: handle}} end
}
RequestSeal.JOSE.JWE.decrypt(compact, policy)

The resolver's work and the holder request share the JWE deadline. Private RSA material stays in the holder. The CEK passes through the sensitive custody runner and middle process to JWE's sensitive worker for authenticated content decryption. OAEP padding and GCM tag failures return the same complete RequestSeal.JOSE.Error. A released or unavailable handle produces that same error as a forged message, by design; use RequestSeal.Custody.public_key(handle) as a startup health check to expose custody availability independently. Deadline expiration remains distinct. Release the handle with release/1 when its owner no longer needs it.

Summary

Functions

Import one unencrypted private PEM, PKCS #8 DER, or private JWK with an explicit algorithm.

Construct an algorithm-bound signing or RSA-OAEP unwrap capability from OTP material.

Stop a local holder and discard its key material. Releasing it again returns :ok.

Functions

import(algorithm, value, format, opts \\ [])

@spec import(RequestSeal.Custody.algorithm(), term(), :pem | :der | :jwk, keyword()) ::
  {:ok, RequestSeal.KeyHandle.t()} | {:error, RequestSeal.Custody.Error.t()}

Import one unencrypted private PEM, PKCS #8 DER, or private JWK with an explicit algorithm.

new(algorithm, material, opts \\ [])

Construct an algorithm-bound signing or RSA-OAEP unwrap capability from OTP material.

release(handle)

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

Stop a local holder and discard its key material. Releasing it again returns :ok.