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-ctraes192-ctraes256-ctraes128-cbcaes192-cbcaes256-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:
- constructs the method-specific public-key request for
ssh-ed25519,rsa-sha2-512/rsa-sha2-256, optional legacyssh-rsa, orecdsa-sha2-nistp* - handles the method-specific
SSH_MSG_USERAUTH_PK_OKconfirmation step - signs the session-identifier-bound authentication payload
- 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-rsacompatibility 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.