A pure Swift implementation of SSH key generation, compatible with OpenSSH formats. SwiftKeyGen provides a modern, type-safe API for generating and managing SSH keys across Apple platforms.
Warning
Experimental software. This project is provided for evaluation and research only and is not intended for production use.
Semantic versioning is not guaranteed—breaking changes may occur in minor or patch releases. Use at your own risk.
- âś… Ed25519 key generation (recommended)
- âś… ECDSA key generation (P-256, P-384, P-521)
- âś… RSA key generation (2048, 3072, 4096 bits)
- âś… Batch key generation for multiple hosts
- âś… Generate all key types for a single identity
- âś… OpenSSH private key format with passphrase encryption
- âś… Key format conversion (OpenSSH, PEM, PKCS#8, RFC4716)
- âś… Import public keys from PEM/PKCS#8 formats
- âś… Import/export keys from stdin/stdout
- âś… Import ECDSA private keys from SEC1 (EC PRIVATE KEY) PEM (unencrypted & legacy encrypted)
- âś… Import ECDSA private keys from encrypted PKCS#8 (PBES2: AES-128/256-CBC, HMAC-SHA1/SHA256)
- âś… Import RSA private keys from legacy encrypted PKCS#1 PEM (Proc-Type/DEK-Info)
- âś… Multiple fingerprint algorithms (SHA256, SHA512, MD5)
- âś… Fingerprint randomart visualization
- âś… Key parsing and validation
- âś… known_hosts file management
- âś… SSH certificate generation and verification
- âś… Full signature verification for all key types
- âś… Secure file I/O with proper permissions (0600 for private keys)
- âś… Passphrase-protected private keys
- âś… Full signature verification (Ed25519, RSA, ECDSA)
- âś… RSA signatures: ssh-rsa (SHA1), rsa-sha2-256, rsa-sha2-512
- âś… ECDSA signatures: P-256 (SHA256), P-384 (SHA384), P-521 (SHA512)
- âś… Apple platforms support (see
Package.swiftfor minimum versions)
Add SwiftKeyGen to your Package.swift:
dependencies: [
.package(url: "https://github.com/nedithgar/SwiftKeyGen.git", from: "0.1.0")
]import SwiftKeyGen
// Generate an Ed25519 key pair (recommended)
let keyPair = try SwiftKeyGen.generateKeyPair(
type: .ed25519,
comment: "[email protected]"
)
// Get the public key in OpenSSH format
print(keyPair.publicKeyString)
// Output: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... [email protected]
// Get the fingerprint in different formats
print(keyPair.fingerprint())
// Output: SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// Get fingerprint with bubble babble format (like ssh-keygen -B)
print(keyPair.fingerprint(hash: .sha256, format: .bubbleBabble))
// Output: xubem-dydek-kysom-puhev-nyroz-byduk-guzex// Generate and save keys to files (similar to ssh-keygen)
try KeyFileManager.generateKeyPairFiles(
type: .ed25519,
privatePath: "~/.ssh/id_ed25519",
publicPath: "~/.ssh/id_ed25519.pub",
comment: "[email protected]"
)// RSA key (3072 bits by default)
let rsaKey = try SwiftKeyGen.generateKeyPair(type: .rsa)
// RSA with standard sizes
let rsa2048 = try SwiftKeyGen.generateKeyPair(type: .rsa, bits: 2048)
let rsa4096 = try SwiftKeyGen.generateKeyPair(type: .rsa, bits: 4096)
// RSA with arbitrary sizes (1024-16384 bits)
let rsa1536 = try SwiftKeyGen.generateKeyPair(type: .rsa, bits: 1536)
let rsa8192 = try SwiftKeyGen.generateKeyPair(type: .rsa, bits: 8192)
// ECDSA keys
let p256Key = try SwiftKeyGen.generateKeyPair(type: .ecdsa256)
let p384Key = try SwiftKeyGen.generateKeyPair(type: .ecdsa384)
let p521Key = try SwiftKeyGen.generateKeyPair(type: .ecdsa521)// Parse a public key string
let publicKeyString = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... user@host"
let (keyType, keyData, comment) = try PublicKeyParser.parsePublicKey(publicKeyString)
// Calculate fingerprint from public key string
let fingerprint = try PublicKeyParser.fingerprint(from: publicKeyString)
// Detect key type
let detectedType = PublicKeyParser.detectKeyType(from: publicKeyString)let keyPair = try SwiftKeyGen.generateKeyPair(type: .ed25519)
// SHA256 (default, OpenSSH 6.8+)
let sha256 = keyPair.fingerprint(hash: .sha256)
// Output: SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// SHA512
let sha512 = keyPair.fingerprint(hash: .sha512)
// Output: SHA512:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// MD5 (legacy, OpenSSH < 6.8)
let md5 = keyPair.fingerprint(hash: .md5)
// Output: xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx// Read a private key
let key = try KeyManager.readPrivateKey(
from: "~/.ssh/id_ed25519",
passphrase: "my-passphrase" // optional
)
// Get key info without decryption
let info = try KeyManager.getKeyInfo(keyPath: "~/.ssh/id_ed25519")
// OR
let info = try KeyManager.getKeyInfo(fromData: someData)
// OR
let info = try KeyManager.getKeyInfo(fromPEM: somePEMString)
print("Key type: \(info.keyType)")
print("Key size: \(info.bitSize) bits")
print("Encrypted: \(info.isEncrypted)")
print("Fingerprint: \(info.fingerprint)")
// Verify a passphrase
let isValid = KeyManager.verifyPassphrase(
keyPath: "~/.ssh/id_ed25519",
passphrase: "test-pass"
)- Best security/performance ratio
- Fixed 256-bit keys
- Small public keys
- Fast signature generation/verification
- Wide compatibility
- Supported sizes: Any size from 1024 to 16384 bits (must be multiple of 8)
- Default: 3072 bits (following OpenSSH 9.0+)
- Pure Swift big-integer implementation (generation). Signing/verification compatible with OpenSSH
- Good performance
- Three NIST curves: P-256, P-384, P-521
- Smaller keys than RSA
- Private keys are written with
0600permissions (owner read/write only) - Public keys are written with
0644permissions (owner write, all read) - Uses secure random number generation
- Memory is properly managed by Swift's ARC
- Apple platforms only (see
Package.swiftfor current minimums). The package is configured for contemporary SDKs/toolchains (Swift 6.2). - Linux support is currently experimental and may require adjustments (some internals use Apple-specific randomness APIs).
// Generate key with passphrase
try KeyFileManager.generateKeyPairFiles(
type: .ed25519,
privatePath: "~/.ssh/id_ed25519",
comment: "[email protected]",
passphrase: "my-secure-passphrase"
)
// Change passphrase on existing key
try KeyManager.changePassphrase(
keyPath: "~/.ssh/id_ed25519",
oldPassphrase: "old-pass",
newPassphrase: "new-pass"
)
// Remove passphrase
try KeyManager.removePassphrase(
keyPath: "~/.ssh/id_ed25519",
currentPassphrase: "current-pass"
)
// Update key comment
try KeyManager.updateComment(
keyPath: "~/.ssh/id_ed25519",
passphrase: "pass", // required if key is encrypted; optional otherwise
newComment: "[email protected]"
)SwiftKeyGen exposes a high-level API to serialize private keys into the OpenSSH proprietary format (openssh-key-v1). When a non-empty passphrase is provided, you can select an authenticated/encryption cipher via a type-safe EncryptionCipher wrapper.
import SwiftKeyGen
let key = try SwiftKeyGen.generateKey(type: .ed25519, comment: "[email protected]")
// Unencrypted (cipher = "none")
let unencrypted = try OpenSSHPrivateKey.serialize(key: key)
// Encrypted with default cipher (currently aes256-ctr)
let encryptedDefault = try OpenSSHPrivateKey.serialize(
key: key,
passphrase: "secret"
)
// Encrypted with an explicit cipher (typed EncryptionCipher)
let encryptedGCM = try OpenSSHPrivateKey.serialize(
key: key,
passphrase: "secret",
cipher: .aes256gcm
)Supported ciphers (OpenSSH names):
.aes128ctr("aes128-ctr").aes192ctr("aes192-ctr").aes256ctr("aes256-ctr").aes128cbc("aes128-cbc").aes192cbc("aes192-cbc").aes256cbc("aes256-cbc").aes128gcm("[email protected]").aes256gcm("[email protected]").des3cbc("3des-cbc").chacha20poly1305("[email protected]")
Notes:
- To produce an unencrypted key, pass
nilor an empty string forpassphrase; the format will set cipher tononeautomatically. - The
swiftkeygenCLI still accepts-Z/--cipheras a string (e.g.,aes256-ctr) and validates it against the same set, mapping it toEncryptionCipherinternally.
let key = try SwiftKeyGen.generateKey(type: .ed25519)
// Convert to PEM format
let pem = try KeyConverter.toPEM(key: key)
// Convert to RFC4716 format (SSH2 public key)
let rfc4716 = try KeyConverter.toRFC4716(key: key)
// Export in multiple formats
let paths = try KeyConverter.exportKey(
key,
formats: [.openssh, .pem, .pkcs8, .rfc4716],
basePath: "~/.ssh/mykey"
)// Read key from stdin
let keyData = try KeyFileManager.readFromStdin()
// Write key to stdout
KeyFileManager.writeStringToStdout(key.publicKeyString())
// Use "-" as filename for stdin/stdout
try KeyFileManager.writeKey(keyPair, to: "-", type: .publicKey)// Parse RFC4716 format public key
let rfc4716String = """
---- BEGIN SSH2 PUBLIC KEY ----
Comment: "[email protected]"
AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
---- END SSH2 PUBLIC KEY ----
"""
let parsed = try PublicKeyParser.parseRFC4716(rfc4716String)
print("Key type: \(parsed.type)")
print("Comment: \(parsed.comment ?? "none")")
// Convert between formats
let options = KeyConversionManager.ConversionOptions(
toFormat: .rfc4716,
fromFormat: .openssh,
input: "~/.ssh/id_ed25519.pub",
output: "~/.ssh/id_ed25519.rfc"
)
try KeyConversionManager.convertKey(options: options)// Parse RSA public key from PEM format
let rsaPublicPEM = """
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAxG6eSjsaTT+PPHobLU5fanucnQ4fKjtMXWadqZGjKnKz1o1hFSb6
QpXW5vVphJ/bCZ2dcSflWnvCpmEQbRhJZBV+hG8n9CL2d6TqJmzR8fK3U2Sk4SJy
...
-----END RSA PUBLIC KEY-----
"""
let rsaPublicKey = try PEMParser.parseRSAPublicKey(rsaPublicPEM)
print(rsaPublicKey.publicKeyString()) // OpenSSH format
// Parse RSA private key from PEM format - SUPPORTED!
let rsaPrivatePEM = """
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZf
...
-----END RSA PRIVATE KEY-----
"""
let rsaPrivateKey = try PEMParser.parseRSAPrivateKey(rsaPrivatePEM)
print(rsaPrivateKey.publicKeyString()) // Extract public key
// Parse ECDSA private key from PEM format - SUPPORTED (unencrypted & legacy OpenSSL-encrypted)!
let ecdsaPrivatePEM = """
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIIGLlamZU9Z83D3g8VsmdqKhu5u47L4RjSXNe3zxQNXPoAoGCCqGSM49
...
-----END EC PRIVATE KEY-----
"""
let ecdsaPrivateKey = try PEMParser.parseECDSAPrivateKey(ecdsaPrivatePEM)
print(ecdsaPrivateKey.publicKeyString()) // Extract public key
// Ed25519 private key OpenSSH format fully supported.
// PEM import:
// - Unencrypted PKCS#8 (PRIVATE KEY): supported
// - Legacy OpenSSL-encrypted PEM (Proc-Type/DEK-Info): supported
// - Encrypted PKCS#8 (ENCRYPTED PRIVATE KEY): not currently supported
let ed25519Private = try PEMParser.parseEd25519PrivateKey(ed25519PEM)
// Parse ECDSA public key from PKCS8 format
let ecdsaPKCS8 = """
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEW3MgvL1V6nh5Fc3YlVJdQi4XQVQZ
Y8VlhTwnDlJZw1D6XB5bEoqFmL0y6kLPFPWNNXaR8HHM86Y7A1A1vBHZ2g==
-----END PUBLIC KEY-----
"""
let ecdsaPublicKey = try PEMParser.parseECDSAPublicKey(ecdsaPKCS8)
print(ecdsaPublicKey.publicKeyString()) // OpenSSH format
// Parse Ed25519 public key from PKCS8 format
let ed25519PKCS8 = """
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4L7SfV2U=
-----END PUBLIC KEY-----
"""
let ed25519Key = try PEMParser.parseEd25519PublicKey(ed25519PKCS8)
print(ed25519Key.publicKeyString()) // OpenSSH format
// Automatic format detection and conversion
let detected = try KeyConversionManager.detectFormat(from: pemString)
print("Detected format: \(detected)")let key = try SwiftKeyGen.generateKey(type: .ed25519)
let art = RandomArt.generate(for: key)
print(art)
// Output:
// +---[ED25519 256]---+
// | ...o |
// | . .. . |
// | = + . * o |
// | . = B + .. E |
// | + o S o . |
// | o o . |
// | . |
// | |
// | |
// +-------------------+let knownHosts = KnownHostsManager()
// Add a host
try knownHosts.addHost(hostname: "example.com", key: hostKey)
// Verify a host key
let result = try knownHosts.verifyHost("example.com", key: presentedKey)
switch result {
case .valid:
print("Host key verified")
case .mismatch:
print("WARNING: Host key has changed!")
case .unknown:
print("Unknown host")
}
// Hash all hostnames for privacy
try knownHosts.hashHostnames()// Generate keys for multiple hosts
let hosts = ["server1", "server2", "server3"]
let results = try await BatchKeyGenerator.generateForHosts(
hosts: hosts,
keyType: .ed25519,
outputDirectory: "~/.ssh/keys/"
)
// Generate all key types for one identity
let allTypes = try await BatchKeyGenerator.generateAllTypes(
identity: "[email protected]",
outputDirectory: "~/.ssh/"
)Note:
- Programmatic conversion to PEM/PKCS#8 requires a private key instance (as shown above).
- The CLI
convertcommand currently performs conversions for public key formats (OpenSSH <-> RFC4716). Converting PEM/PKCS#8 via CLI is not supported because it requires private key material.
// Generate a CA key pair (supports Ed25519, RSA, and ECDSA)
let caKey = try SwiftKeyGen.generateKeyPair(
type: .ed25519, // or .rsa, .ecdsa256, .ecdsa384, .ecdsa521
comment: "[email protected]"
)
// Save CA keys
try KeyFileManager.generateKeyPairFiles(
type: .ed25519,
privatePath: "~/.ssh/ca_key",
publicPath: "~/.ssh/ca_key.pub",
comment: "[email protected]"
)// Load CA key
let caKey = try KeyManager.readPrivateKey(
from: "~/.ssh/ca_key",
passphrase: "ca-passphrase"
)
// Generate or load user key
let userKey = try SwiftKeyGen.generateKey(type: .ed25519)
// Sign user certificate
let userCert = try CertificateAuthority.signCertificate(
publicKey: userKey,
caKey: caKey,
keyId: "john.doe",
principals: ["john", "jdoe"],
validFrom: Date(),
validTo: Date().addingTimeInterval(30 * 24 * 60 * 60), // 30 days
certificateType: .user,
extensions: [
.permitX11Forwarding,
.permitAgentForwarding,
.permitPortForwarding,
.permitPty,
.permitUserRc
]
)
// Save certificate
try CertificateManager.saveCertificate(
userCert,
to: "~/.ssh/id_ed25519-cert.pub",
comment: "[email protected]"
)// Sign host certificate with wildcards
let hostCert = try CertificateAuthority.signCertificate(
publicKey: hostKey,
caKey: caKey,
keyId: "web-server-01",
principals: ["web.example.com", "*.example.com"],
serial: 1001,
validFrom: Date(),
validTo: Date().addingTimeInterval(365 * 24 * 60 * 60), // 1 year
certificateType: .host
)// Create restricted user certificate
let restrictedCert = try CertificateManager.createUserCertificate(
publicKey: userKey,
caKey: caKey,
username: "contractor",
validityDays: 7,
forceCommand: "/usr/bin/git-shell",
sourceAddress: "203.0.113.0/24"
)// Read and verify a certificate
let cert = try CertificateManager.readCertificate(
from: "~/.ssh/id_ed25519-cert.pub"
)
// Verify certificate validity (supports RSA, ECDSA, and Ed25519 CA signatures)
let result = CertificateVerifier.verifyCertificate(
cert,
caKey: caKey, // Can be any key type: Ed25519, RSA, or ECDSA
options: CertificateVerificationOptions()
)
switch result {
case .valid:
print("Certificate is valid")
case .expired:
print("Certificate has expired")
case .invalidSignature:
print("Certificate signature is invalid")
case .invalidPrincipal:
print("Principal not authorized")
default:
print("Certificate verification failed")
}
// Verify for specific host
let hostResult = CertificateManager.verifyCertificateForHost(
cert,
hostname: "web.example.com",
caKey: caKey
)
// Verify for specific user
let userResult = CertificateManager.verifyCertificateForUser(
cert,
username: "john",
caKey: caKey
)// Parse and display certificate info
let certInfo = try CertificateManager.parseCertificateString(
"[email protected] AAAAA..."
)
print(certInfo)
// Output:
// Type: [email protected] user certificate
// Key ID: "john.doe"
// Serial: 12345
// Valid: from 2024-01-01T00:00:00 to 2024-01-31T23:59:59
// Principals:
// john
// jdoe
// Critical Options:
// (none)
// Extensions:
// permit-X11-forwarding
// permit-agent-forwarding
// permit-port-forwarding
// permit-pty
// permit-user-rc
// Signing CA: ssh-ed25519 SHA256:...// Generate certificates for multiple hosts
let hosts = ["web01", "web02", "db01"]
let results = try CertificateManager.generateCertificatesForHosts(
hosts: hosts,
caKeyPath: "~/.ssh/host_ca_key",
keyType: .ed25519,
outputDirectory: "/etc/ssh/",
validityDays: 365
)
for (host, certPath) in results {
print("Generated certificate for \(host): \(certPath)")
}SwiftKeyGen supports full signature verification for all key types:
// Prepare message data
let messageData: Data = ...
// RSA public-key verification (SSH-formatted signature)
let rsaKey = try RSAKeyGenerator.generate(bits: 2048)
let rsaPublic = rsaKey.publicOnlyKey() // any SSHPublicKey
// 'signatureFromPeer' must be SSH-formatted (type + blob), e.g. "rsa-sha2-256"
let rsaValid = try rsaPublic.verify(signature: signatureFromPeer, for: messageData)
// Supports: ssh-rsa (SHA1), rsa-sha2-256, rsa-sha2-512
// ECDSA public-key verification (SSH-formatted signature)
let ecdsaKey = try ECDSAKeyGenerator.generateP256()
let ecdsaPublic = ecdsaKey.publicOnlyKey() // any SSHPublicKey
// 'signatureFromPeerECDSA' must be SSH-formatted with r,s and matching curve type
let ecdsaValid = try ecdsaPublic.verify(signature: signatureFromPeerECDSA, for: messageData)
// Supports: P-256 (SHA256), P-384 (SHA384), P-521 (SHA512)
// Public-key-only verification (generic)
let pub: any SSHPublicKey = rsaKey.publicOnlyKey()
let canVerify = try pub.verify(signature: signatureFromPeer, for: messageData)- SSH certificate generation and signing
- Full signature verification (RSA, ECDSA, Ed25519)
- [ ] DSA key support (legacy) - KRL (Key Revocation List) support
- FIDO/U2F security key support
- Moduli generation for DH groups
- SSH agent integration
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.