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
@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.
@spec new(RequestSeal.Custody.algorithm(), tuple(), keyword()) :: {:ok, RequestSeal.KeyHandle.t()} | {:error, RequestSeal.Custody.Error.t()}
Construct an algorithm-bound signing or RSA-OAEP unwrap capability from OTP material.
@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.