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
@spec new( RequestSeal.Crypto.algorithm(), binary(), RequestSeal.PublicKey.t(), keyword() ) :: {:ok, RequestSeal.KeyHandle.t()} | {:error, RequestSeal.Custody.Error.t()}
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}withnameone 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 trustedRequestSeal.PublicKey.t/0of the agent key; it must fitalgorithm, 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. Default5_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 invalidsocket_path),:unsupported_algorithm,:invalid_key(publicfails validation),:key_mismatch(publicdoes not fitalgorithmor 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)