RequestSeal.Custody.SSHAgent (RequestSeal v0.5.2)

Copy Markdown View Source

Non-exporting signing through a caller-selected OpenSSH agent Unix socket.

Protocol: draft-miller-ssh-agent-14, RFC 8709, RFC 8332, and RFC 5656.

new/4 binds a trusted public key to an explicit socket path and proves possession before returning a handle. It never reads SSH_AUTH_SOCK, starts an agent, loads keys, or accesses private files. The caller owns agent access and authorization. An OpenSSH agent is a separate process, not an HSM guarantee.

new/4 lists the supported algorithms; RSA PKCS #1 v1.5 SHA-256 signing uses agent flag 2. Each sign uses one new connection; replies cannot spill into another operation. Identity lists are bounded to 256 keys, receive frames to 1,048,576 bytes, key blobs and comments to 16,384 bytes. Selection uses exact public components, never a comment or key ID. ECDSA mpints convert to fixed-width r || s, and every returned signature must verify with Crypto before release. Verification uses the trusted public key locally; it does not assert current agent possession or authorization.

Outgoing request frames have no library bound beyond the max_bytes: option of RequestSeal.Custody.sign/3. OpenSSH caps the whole agent message at 256 KiB, including protocol overhead, so payloads slightly under 262,144 bytes are the practical limit. Observed with OpenSSH 9.9 and Ed25519: 262,000 bytes signed successfully; 262,200 returned :custodian_protocol. A larger max_bytes: does not raise the agent's limit.

Removed keys during signing yield :key_not_found. Connect and pre-send connection failures yield retryable :custodian_unavailable. After a request is written, a closed connection, short frame, or malformed reply yields nonretryable :custodian_protocol; deadline expiry yields :deadline_exceeded. Deadline or caller death closes the worker's connection. No logs or telemetry contain the path, references, comments, bases, or signatures.

Summary

Functions

Bind a supported algorithm, explicit socket, and trusted public key after a real possession check.

Functions

new(algorithm, socket_path, public, opts \\ [])

Bind a supported algorithm, explicit socket, and trusted public key after a real possession check.

Construction proves possession by asking the agent to sign empty bytes and checking the returned signature against public. The returned handle has capabilities [:sign, :verify]; each later signature first confirms the key is still listed by the agent.

Parameters

  • algorithm - "ed25519", "rsa-v1_5-sha256", "ecdsa-p256-sha256", "ecdsa-p384-sha384", or {:jws, name} with name one of "EdDSA", "RS256", "ES256", or "ES384". PSS and HMAC reject.
  • socket_path - an absolute agent socket path of 1–103 bytes with no NUL byte.
  • public - the trusted RequestSeal.PublicKey.t/0 of the agent key; it must fit algorithm, and any JWK restrictions must permit verification with it.
  • opts - keyword list; see Options.

Options

  • :timeout - deadline in milliseconds for the possession check, integer 1–300,000. Default 5_000. No other option is accepted.

Returns

  • {:ok, %RequestSeal.KeyHandle{}} - a signing and verification handle.
  • {:error, %RequestSeal.Custody.Error{}} - reasons this function can return: :invalid_options (unknown, duplicate, or out-of-range option, or an invalid socket_path), :unsupported_algorithm, :invalid_key (public fails validation), :key_mismatch (public does not fit algorithm or its restrictions), :custodian_unavailable (no socket, refused, or closed before the request), :custodian_rejected (the agent does not hold the key, refuses, or returns a signature that fails verification), :custodian_protocol (malformed, short, or oversized reply, or a connection closed after the request), :deadline_exceeded, and :custodian_failure. Meanings: RequestSeal.Custody.Error.

Examples

Algorithm and path checks run before any agent connection:

iex> {public, _seed} = :crypto.generate_key(:eddsa, :ed25519)
iex> {:ok, key} = RequestSeal.PublicKey.import({:ed25519, public}, :raw)
iex> RequestSeal.Custody.SSHAgent.new("hmac-sha256", "/run/agent.sock", key)
{:error, %RequestSeal.Custody.Error{reason: :unsupported_algorithm, retryable: false}}
iex> RequestSeal.Custody.SSHAgent.new("ed25519", "agent.sock", key)
{:error, %RequestSeal.Custody.Error{reason: :invalid_options, retryable: false}}
iex> RequestSeal.Custody.SSHAgent.new("ecdsa-p256-sha256", "/run/agent.sock", key)
{:error, %RequestSeal.Custody.Error{reason: :key_mismatch, retryable: false}}

Fragment (requires a running OpenSSH agent holding the Ed25519 key; not executed here):

{:ok, key} = RequestSeal.PublicKey.import(trusted_public_key_pem, :pem)
{:ok, handle} = RequestSeal.Custody.SSHAgent.new("ed25519", "/run/user/1000/agent.sock", key, timeout: 2_000)
{:ok, signature} = RequestSeal.Custody.sign(handle, "deploy build 9134 to staging.example.com")
:ok = RequestSeal.Custody.verify(handle, "deploy build 9134 to staging.example.com", signature)