Traversio

Public-Key Authentication

Ed25519, RSA, and ECDSA public-key authentication, including raw-key, OpenSSH key-loading, SSH agent, and OpenSSH key-generation paths.

Public-Key Methods

Traversio exposes Ed25519, RSA, plus ECDSA P-256, P-384, and P-521 public-key authentication:

.ed25519PrivateKey(rawRepresentation: [UInt8])
.rsaPrivateKey(pkcs1DERRepresentation: [UInt8])
.ecdsaP256PrivateKey(rawRepresentation: [UInt8])
.ecdsaP384PrivateKey(rawRepresentation: [UInt8])
.ecdsaP521PrivateKey(rawRepresentation: [UInt8])
.publicKey(
    algorithmNames: [String],
    publicKey: [UInt8],
    signatureProvider: @Sendable (SSHPublicKeyAuthenticationSigningRequest) async throws -> [UInt8]
)
try .ed25519PrivateKey(openSSHPrivateKey: String, passphrase: String? = nil)
try .ed25519PrivateKey(contentsOfOpenSSHPrivateKeyFile: String, passphrase: String? = nil)
try .rsaPrivateKey(openSSHPrivateKey: String, passphrase: String? = nil)
try .rsaPrivateKey(contentsOfOpenSSHPrivateKeyFile: String, passphrase: String? = nil)
try .ecdsaPrivateKey(openSSHPrivateKey: String, passphrase: String? = nil)
try .ecdsaPrivateKey(contentsOfOpenSSHPrivateKeyFile: String, passphrase: String? = nil)
try .privateKeyPEM(String, passphrase: String? = nil)
try .privateKeyPEM(contentsOfFile: String, passphrase: String? = nil)

The Ed25519 raw-value case expects a 32-byte private-key seed. The RSA raw-value case expects PKCS#1 DER private-key bytes. The ECDSA raw-value cases expect the corresponding curve-specific raw private-key representation. The OpenSSH-specific throwing helpers load OpenSSH openssh-key-v1 Ed25519, RSA, or ECDSA private keys and return the matching auth case. Use privateKeyPEM(...) for app/user import paths that should also accept OpenSSL-style PEM: PKCS#8 -----BEGIN PRIVATE KEY----- for Ed25519/RSA/ECDSA, traditional -----BEGIN RSA PRIVATE KEY-----, and traditional -----BEGIN EC PRIVATE KEY-----. Pass passphrase: when an OpenSSH key is encrypted with the OpenSSH bcrypt KDF, or when a traditional RSA PEM uses supported OpenSSL legacy AES-CBC or DES-EDE3-CBC encryption. Encrypted PKCS#8 -----BEGIN ENCRYPTED PRIVATE KEY----- and encrypted traditional EC PEM are not part of this loader yet.

OpenSSH Key Metadata

Use SSHOpenSSHPrivateKeyInfo when the app needs to label or inspect an OpenSSH private-key file before it has a passphrase. The parser reads the public envelope and returns the public-key algorithm, ECDSA curve, RSA modulus bit count, cipher, KDF name/options, public-key fingerprint, and an authorized_keys-style public line.

It does not decrypt the private-key block and does not prove that the private key can authenticate. Use SSHAuthenticationMethod.openSSHPrivateKey(...) for strict OpenSSH private-key input, or SSHAuthenticationMethod.privateKeyPEM(...) for broader app/user import input.

import Traversio

func keyLabel(for pem: String) throws -> String {
    let info = try SSHOpenSSHPrivateKeyInfo.parse(pem)

    switch info.primaryPublicKey.algorithm {
    case .ed25519:
        return "Ed25519"
    case let .rsa(modulusBitCount):
        return "RSA \(modulusBitCount)"
    case let .ecdsa(curve):
        return "ECDSA \(curve.rawValue)"
    case let .certificate(algorithmName, _), let .unknown(algorithmName):
        return algorithmName
    }
}

Callback Signing

Use .publicKey(algorithmNames:publicKey:signatureProvider:) when key material lives behind an external signer. Traversio performs the public-key confirmation step, then passes SSHPublicKeyAuthenticationSigningRequest to your async closure.

import Traversio

func callbackBackedAuth(
    publicKey: [UInt8],
    sign: @escaping @Sendable ([UInt8]) async throws -> [UInt8]
) -> SSHAuthenticationMethod {
    .publicKey(
        algorithmNames: ["ssh-ed25519"],
        publicKey: publicKey
    ) { request in
        let rawSignature = try await sign(request.signatureData)
        return request.makeSignatureBlob(rawSignature: rawSignature)
    }
}

SSHPublicKeyAuthenticationSigningRequest contains the username, serviceName, selected algorithmName, raw SSH publicKey, and session-bound signatureData. The callback returns an SSH signature blob. makeSignatureBlob(rawSignature:) wraps raw signer output with the selected SSH algorithm name.

SSH Agent Signing

Use SSHAgentClient when credentials already live in an OpenSSH-compatible agent. The default initializer reads SSH_AUTH_SOCK; pass socketPath: to target a specific agent socket.

import Traversio

enum AgentAuthSetupError: Error {
    case noIdentity
}

func connectWithSSHAgent() async throws {
    let agent = try SSHAgentClient()
    guard let identity = try await agent.identities().first else {
        throw AgentAuthSetupError.noIdentity
    }

    let configuration = SSHClientConfiguration(
        host: "example.com",
        username: "deploy",
        authentication: agent.authenticationMethod(for: identity),
        hostKeyPolicy: .knownHostsFile("/Users/me/.ssh/known_hosts")
    )

    try await SSHClient.withConnection(configuration: configuration) { connection in
        let result = try await connection.execute("whoami")
        print(String(decoding: result.standardOutput, as: UTF8.self))
    }
}

SSHAgentClient.identities() returns SSHAgentIdentity values with the raw SSH public key, agent comment, key type, and supported authentication algorithm names. authenticationMethod(for:) builds the .publicKey(...) authentication method and signs the session-bound request through the agent. RSA identities advertise rsa-sha2-512, rsa-sha2-256, and ssh-rsa; SSHClientConfiguration.legacyAlgorithmOptions still controls whether ssh-rsa is allowed to participate in the connection's public-key algorithm choice. SHA-2 signing requests use the matching OpenSSH agent signature flags.

OpenSSH Key Generation

Traversio also exposes a small OpenSSH key-generation surface:

SSHOpenSSHKeyPair.generate(
    algorithm: .ed25519,
    comment: String = "traversio",
    encryption: SSHOpenSSHPrivateKeyEncryption? = nil
)

Supported key-pair algorithms:

  • .ed25519
  • .ecdsaP256
  • .ecdsaP384
  • .ecdsaP521
  • .rsa(bitCount: Int)

SSHOpenSSHPrivateKeyEncryption(passphrase:cipher:rounds:) supports:

  • aes128-ctr
  • aes192-ctr
  • aes256-ctr
  • aes128-cbc
  • aes192-cbc
  • aes256-cbc

The default encrypted export matches the OpenSSH baseline: bcrypt with aes256-ctr and 24 rounds.

For a full workflow guide with file-writing examples, permission handling, and direct use of the generated authenticationMethod, see Key Generation.

Generation Example

import Foundation
import Traversio

func makeDeploymentKey() throws -> SSHAuthenticationMethod {
    let keyPair = try SSHOpenSSHKeyPair.generate(
        algorithm: .ed25519,
        comment: "[email protected]",
        encryption: SSHOpenSSHPrivateKeyEncryption(
            passphrase: "correct horse battery staple"
        )
    )

    let sshDirectory = FileManager.default.homeDirectoryForCurrentUser
        .appendingPathComponent(".ssh", isDirectory: true)
    let privateKeyURL = sshDirectory.appendingPathComponent("id_traversio")
    let publicKeyURL = sshDirectory.appendingPathComponent("id_traversio.pub")

    try keyPair.privateKeyPEM.write(to: privateKeyURL, atomically: true, encoding: .utf8)
    try keyPair.authorizedKeyLine.write(to: publicKeyURL, atomically: true, encoding: .utf8)
    try FileManager.default.setAttributes(
        [.posixPermissions: 0o600],
        ofItemAtPath: privateKeyURL.path
    )

    return keyPair.authenticationMethod
}

End-to-End Example

import Traversio

func connectWithECDSAKeyFile() async throws {
    let configuration = SSHClientConfiguration(
        host: "example.com",
        username: "deploy",
        authentication: try .ecdsaPrivateKey(
            contentsOfOpenSSHPrivateKeyFile: "/Users/me/.ssh/id_ecdsa",
            passphrase: "correct horse battery staple"
        ),
        hostKeyPolicy: .knownHostsFile("/Users/me/.ssh/known_hosts")
    )

    try await SSHClient.withConnection(configuration: configuration) { connection in
        let result = try await connection.execute("whoami")
        print(String(decoding: result.standardOutput, as: UTF8.self))
    }
}

If you already manage key material in memory, the raw cases remain available for Ed25519, RSA, and each supported ECDSA curve. SSHOpenSSHKeyPair.generate(...) covers the matching OpenSSH file-generation workflow.

Request Flow

For the OpenSSH-compatible path, Traversio:

  1. constructs the method-specific public-key request for ssh-ed25519, rsa-sha2-512 / rsa-sha2-256, optional legacy ssh-rsa, or ecdsa-sha2-nistp*
  2. handles the method-specific SSH_MSG_USERAUTH_PK_OK confirmation step
  3. signs the session-identifier-bound authentication payload
  4. retries with the signed request

For built-in RSA private keys, Traversio prefers rsa-sha2-512 and falls back to rsa-sha2-256 when the server's server-sig-algs extension only advertises that SHA-256 path.

If you enable SSHLegacyAlgorithmOptions.sshRSA on SSHClientConfiguration or one SSHProxyJumpHost, Traversio also enables the legacy RSA/SHA-1 path for that specific connection or hop. It appends ssh-rsa to the preferred host-key list, lets callback-backed or agent-backed public-key auth select ssh-rsa, then retries built-in RSA private-key authentication with ssh-rsa only after the server declines the SHA-2 attempt and still offers publickey. When legacy RSA is disabled, Traversio removes ssh-rsa from callback-backed and agent-backed candidate lists before choosing the public-key signature algorithm, even if the signer advertises it.

This option is only an SSH algorithm compatibility switch. It is separate from private-key file format support such as OpenSSH OPENSSH PRIVATE KEY, PKCS#8 PRIVATE KEY, or traditional RSA PRIVATE KEY / EC PRIVATE KEY PEM parsing.

RSA and ECDSA public-key authentication are already live-validated against OpenSSH. Callback-backed Ed25519 signing and SSH agent-backed signing are live-validated against local OpenSSH and Dropbear targets. Ed25519, RSA, and ECDSA request and signature handling is also covered by deterministic protocol tests. Generated OpenSSH private keys are smoke-tested against the system ssh-keygen -y reader on macOS for the supported algorithms.

Not Currently Supported

The public-key feature set stays focused on direct key loading, OpenSSH file compatibility, callback signers, and OpenSSH-compatible agents.

  • keychain-backed credential loading

Good Fits

Use the public-key API when:

  • you want Ed25519, RSA/SHA-2, or ECDSA authentication
  • you want optional explicit legacy ssh-rsa compatibility for an older endpoint and can enable it on that specific connection or jump-host hop
  • an OpenSSH private key file is acceptable, with passphrase: supplied when it is encrypted, you already control the key material yourself, or an OpenSSH-compatible agent owns the credential
  • you want the most direct supported path in the current public API

Keychain-backed credentials remain outside the current public API.

On this page