Skip to content

Add ssh-agent support#230

Open
rhx wants to merge 1 commit intoapple:mainfrom
rhx:ssh-agent
Open

Add ssh-agent support#230
rhx wants to merge 1 commit intoapple:mainfrom
rhx:ssh-agent

Conversation

@rhx
Copy link
Copy Markdown

@rhx rhx commented Mar 7, 2026

Add SSH Agent support

This adds support for delegated signing in NIOSSHPrivateKey.
Closes #189

Background

NIOSSHPrivateKey currently requires direct access to the private key at construction time.
Every initialiser takes a concrete CryptoKit key (Curve25519.Signing.PrivateKey,
P256.Signing.PrivateKey, etc.) and all signing methods operate on that in-memory key.

This makes it impossible to implement public-key authentication through, say, an SSH agent,
hardware security module, or any other system where the private key is not directly accessible
to the caller. A NIOSSHClientUserAuthenticationDelegate that wants to use an agent-held key
has no way to produce a valid NIOSSHPrivateKey. This is because the agent only exposes a
public key and a sign operation — it never reveals the private key bytes.

NIOSSHPrivateKey.BackingKey is a closed enum with one case per concrete key type. There is
no case that pairs a public key with an external signing operation, and no public API on
NIOSSHSignature to construct a signature from raw bytes returned by an agent.

Internally, two signing paths exist:

  1. sign(digest:) — used during key exchange (host key proof); operates on a pre-hashed digest.
  2. sign(_: UserAuthSignablePayload) — used during user authentication; operates on the raw signable payload bytes.

An external signer only needs to support path (2), since it is the client user-auth path.
Path (1) is server-side host key proving and does not apply to agent-held keys.

Relationship to PR #220

These two proposals were developed independently in parallel and I only saw it just now,
before submitting this PR. Both approaches have their pros and cons.

PR #220 proposes an NIOSSHExternalSigner protocol for the same problem. This PR takes a
deliberately simpler approach — a single @Sendable closure instead of a protocol — to
minimise the API surface and avoid introducing new types. The two approaches are functionally
equivalent; this one is smaller and requires no new protocol conformances from callers.

Modifications

NIOSSHPrivateKey

  • Added a .signingDelegate(@Sendable (ByteBufferView) throws -> NIOSSHSignature, NIOSSHPublicKey) case to BackingKey.
  • Added public init(publicKey:signingCallback:) so callers can create a private key backed by an external signer.
  • Wired the new case through hostKeyAlgorithms, publicKey, and both sign methods:
    • sign(_: UserAuthSignablePayload) delegates to the callback.
    • sign(digest:) throws — this path is for host key exchange and does not apply to agent-held client keys.
  • Marked BackingKey as Sendable (required by the @Sendable closure).

NIOSSHSignature

  • Added public factory methods for all supported key types so that signing delegates can construct a valid NIOSSHSignature from raw signature bytes:
    • .ed25519(signature:)
    • .ecdsaP256(signature:)
    • .ecdsaP384(signature:)
    • .ecdsaP521(signature:)

Status

Callers can authenticate via public-key auth without holding private key material. For
example, an SSH agent client can list the agent's keys, wrap each in an NIOSSHPrivateKey
with a signing callback that forwards the sign request to the agent, and pass it to
NIOSSHClientUserAuthenticationDelegate as normal.

I have tried to keep this a minimal, non-breaking change without altering existing API
surface.

Limitations

Delegated signing only works with key types that NIOSSH already supports — Ed25519, ECDSA
P-256, P-384, and P-521 — because NIOSSHPublicKey and NIOSSHSignature are also closed
enums over the same set. An SSH agent may hold (or an SSH server may request) RSA keys,
but NIOSSH has no RSA support (swift-crypto does not provide RSA signing), so those keys
cannot be represented as an NIOSSHPublicKey in the first place.

Adding RSA would require broader changes across the key, signature, and
algorithm-negotiation layers, which is out of scope here.

Examples

Example usage

let privateKey = NIOSSHPrivateKey(
    publicKey: agentPublicKey,
    signingCallback: { bufferViewToSign in
        let rawSignature = try agent.sign(data: Data(bufferViewToSign), key: agentPublicKey)
        return try .ed25519(signature: rawSignature)
    }
)

// Use in auth delegate as usual
offer.offer = .privateKey(.init(privateKey: privateKey, publicKey: agentPublicKey))

Example project

This has been tested with a simple Swift ssh client CLI tool that replicates the
corresponding functionality of the ssh client:

https://github.com/rhx/swift-ssh-client

This allows delegating signing to an agent in cases where
the private key is not available to the client and signing
must be performed through, e.g. a socket connected to a
forwarded ssh agent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add SSH Agent Support

1 participant