Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
Copy link

Copilot AI Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change raises the minimum swift-crypto version from 1.0.0 to 3.0.0, which is a breaking change for consumers who may be using swift-crypto 1.x or 2.x. While this is necessary for _CryptoExtras RSA support, this breaking change should be clearly documented in the package release notes, changelog, or migration guide. Consider whether this warrants a major version bump for SwiftNIO-SSH.

Suggested change
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"),
// NOTE: The minimum swift-crypto version was raised to 3.x to enable _CryptoExtras
// (e.g. RSA) support. This is a breaking change for users pinned to swift-crypto
// 1.x or 2.x and must be called out in release notes / migration guides and
// considered when deciding whether to perform a major version bump of swift-nio-ssh.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

@daiimus daiimus Dec 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment about swift-crypto 3.x being a breaking change.

// NOTE: Minimum swift-crypto raised to 3.x for _CryptoExtras (RSA) support.
// This is a breaking change for users on swift-crypto 1.x or 2.x.
.package(url: "https://github.com/apple/swift-crypto.git", "3.0.0"..<"4.0.0"),

.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"4.0.0"),
// NOTE: Minimum swift-crypto raised to 3.x for _CryptoExtras (RSA) support.
// This is a breaking change for users on swift-crypto 1.x or 2.x.
.package(url: "https://github.com/apple/swift-crypto.git", "3.0.0"..<"4.0.0"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.0.2"),
],
targets: [
Expand All @@ -49,6 +51,7 @@ let package = Package(
.product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "_CryptoExtras", package: "swift-crypto"),
.product(name: "Atomics", package: "swift-atomics"),
],
swiftSettings: swiftSettings
Expand Down
29 changes: 29 additions & 0 deletions Sources/NIOSSH/Keys And Signatures/NIOSSHCertifiedPublicKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,11 +359,40 @@ extension NIOSSHCertifiedPublicKey {
return Self.p384KeyPrefix
case .ecdsaP521:
return Self.p521KeyPrefix
case .rsa:
preconditionFailure("RSA certificates are not currently supported")
case .certified:
preconditionFailure("base key cannot be certified")
}
}

internal var signatureAlgorithmPrefix: String.UTF8View {
switch self.key.backingKey {
case .ed25519:
return Self.ed25519KeyPrefix
case .ecdsaP256:
return Self.p256KeyPrefix
case .ecdsaP384:
return Self.p384KeyPrefix
case .ecdsaP521:
return Self.p521KeyPrefix
case .rsa:
preconditionFailure("RSA certificates are not currently supported")
case .certified:
preconditionFailure("base key cannot be certified")
}
}

/// Returns the algorithm name to use for authentication, supporting RSA algorithm selection.
internal func algorithmName(forRSA rsaAlgorithm: RSASignatureAlgorithm) -> String.UTF8View {
switch self.key.backingKey {
case .rsa:
return rsaAlgorithm.wireBytes
default:
return self.signatureAlgorithmPrefix
}
}

internal func isValidSignature<DigestBytes: Digest>(_ signature: NIOSSHSignature, for digest: DigestBytes) -> Bool {
self.key.isValidSignature(signature, for: digest)
}
Expand Down
83 changes: 82 additions & 1 deletion Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
//===----------------------------------------------------------------------===//

@preconcurrency import Crypto
import _CryptoExtras
import NIOCore
import NIOFoundationCompat

#if canImport(FoundationEssentials)
import FoundationEssentials
Expand Down Expand Up @@ -52,6 +54,14 @@ public struct NIOSSHPrivateKey: Sendable {
self.backingKey = .ecdsaP521(key)
}

/// Create a private key from an RSA key.
///
/// RSA support is provided for interoperability with legacy systems. For new deployments,
/// consider using Ed25519 or ECDSA keys instead.
public init(rsaKey key: _RSA.Signing.PrivateKey) {
self.backingKey = .rsa(key)
}

#if canImport(Darwin)
public init(secureEnclaveP256Key key: SecureEnclave.P256.Signing.PrivateKey) {
self.backingKey = .secureEnclaveP256(key)
Expand All @@ -69,6 +79,10 @@ public struct NIOSSHPrivateKey: Sendable {
return ["ecdsa-sha2-nistp384"]
case .ecdsaP521:
return ["ecdsa-sha2-nistp521"]
case .rsa:
// Modern servers prefer rsa-sha2-512 > rsa-sha2-256 > ssh-rsa
// ssh-rsa uses SHA-1 which is deprecated but still needed for compatibility
return ["rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"]
#if canImport(Darwin)
case .secureEnclaveP256:
return ["ecdsa-sha2-nistp256"]
Expand All @@ -84,6 +98,7 @@ extension NIOSSHPrivateKey {
case ecdsaP256(P256.Signing.PrivateKey)
case ecdsaP384(P384.Signing.PrivateKey)
case ecdsaP521(P521.Signing.PrivateKey)
case rsa(_RSA.Signing.PrivateKey)

#if canImport(Darwin)
case secureEnclaveP256(SecureEnclave.P256.Signing.PrivateKey)
Expand Down Expand Up @@ -114,6 +129,24 @@ extension NIOSSHPrivateKey {
try key.signature(for: ptr)
}
return NIOSSHSignature(backingSignature: .ecdsaP521(signature))
case .rsa(let key):
// For RSA, the signature algorithm identifier should match the digest type.
// Per RFC 8332, we support rsa-sha2-256 and rsa-sha2-512.
// SHA-384 is not defined in SSH, so we use rsa-sha2-512 as the strongest available.
let signature = try key.signature(for: digest, padding: .insecurePKCS1v1_5)

switch DigestBytes.byteCount {
case SHA256.byteCount:
return NIOSSHSignature(backingSignature: .rsaSHA256(signature))
case SHA384.byteCount:
// SHA-384 has no SSH algorithm; use strongest available (rsa-sha2-512)
return NIOSSHSignature(backingSignature: .rsaSHA512(signature))
case SHA512.byteCount:
return NIOSSHSignature(backingSignature: .rsaSHA512(signature))
default:
// For any other digest size (including SHA-1's 20 bytes), use SHA-512
return NIOSSHSignature(backingSignature: .rsaSHA512(signature))
}

#if canImport(Darwin)
case .secureEnclaveP256(let key):
Expand All @@ -125,7 +158,7 @@ extension NIOSSHPrivateKey {
}
}

func sign(_ payload: UserAuthSignablePayload) throws -> NIOSSHSignature {
func sign(_ payload: UserAuthSignablePayload, rsaSignatureAlgorithm: RSASignatureAlgorithm = .sha512) throws -> NIOSSHSignature {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter shouldn't be defaulted: we should be able to produce an answer to it in all places. Doing that will help confirm that we haven't missed any important usage-sites.

switch self.backingKey {
case .ed25519(let key):
let signature = try key.signature(for: payload.bytes.readableBytesView)
Expand All @@ -139,13 +172,59 @@ extension NIOSSHPrivateKey {
case .ecdsaP521(let key):
let signature = try key.signature(for: payload.bytes.readableBytesView)
return NIOSSHSignature(backingSignature: .ecdsaP521(signature))
case .rsa(let key):
// Sign using the specified RSA algorithm (RFC 8332)
let bytesView = payload.bytes.readableBytesView
switch rsaSignatureAlgorithm {
case .sha512:
let digest = SHA512.hash(data: bytesView)
let signature = try key.signature(for: digest, padding: .insecurePKCS1v1_5)
return NIOSSHSignature(backingSignature: .rsaSHA512(signature))
case .sha256:
let digest = SHA256.hash(data: bytesView)
let signature = try key.signature(for: digest, padding: .insecurePKCS1v1_5)
return NIOSSHSignature(backingSignature: .rsaSHA256(signature))
case .sha1:
let digest = Insecure.SHA1.hash(data: bytesView)
let signature = try key.signature(for: digest, padding: .insecurePKCS1v1_5)
return NIOSSHSignature(backingSignature: .rsaSHA1(signature))
}
#if canImport(Darwin)
case .secureEnclaveP256(let key):
let signature = try key.signature(for: payload.bytes.readableBytesView)
return NIOSSHSignature(backingSignature: .ecdsaP256(signature))
#endif
}
}

/// Sign a payload with a specific RSA signature algorithm.
///
/// This is used when the server negotiates a specific RSA algorithm (ssh-rsa, rsa-sha2-256, rsa-sha2-512).
func signRSA(_ payload: UserAuthSignablePayload, algorithm: Substring) throws -> NIOSSHSignature {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see no evidence this function is used at all.

guard case .rsa(let key) = self.backingKey else {
preconditionFailure("signRSA called on non-RSA key")
}

let bytesView = payload.bytes.readableBytesView

switch algorithm {
case "ssh-rsa":
// ssh-rsa uses SHA-1 (deprecated but still used for compatibility)
let digest = Insecure.SHA1.hash(data: bytesView)
let signature = try key.signature(for: digest, padding: .insecurePKCS1v1_5)
return NIOSSHSignature(backingSignature: .rsaSHA1(signature))
case "rsa-sha2-256":
let digest = SHA256.hash(data: bytesView)
let signature = try key.signature(for: digest, padding: .insecurePKCS1v1_5)
return NIOSSHSignature(backingSignature: .rsaSHA256(signature))
case "rsa-sha2-512":
let digest = SHA512.hash(data: bytesView)
let signature = try key.signature(for: digest, padding: .insecurePKCS1v1_5)
return NIOSSHSignature(backingSignature: .rsaSHA512(signature))
default:
preconditionFailure("Unknown RSA algorithm: \(algorithm)")
}
}
}

extension NIOSSHPrivateKey {
Expand All @@ -160,6 +239,8 @@ extension NIOSSHPrivateKey {
return NIOSSHPublicKey(backingKey: .ecdsaP384(privateKey.publicKey))
case .ecdsaP521(let privateKey):
return NIOSSHPublicKey(backingKey: .ecdsaP521(privateKey.publicKey))
case .rsa(let privateKey):
return NIOSSHPublicKey(backingKey: .rsa(privateKey.publicKey))
#if canImport(Darwin)
case .secureEnclaveP256(let privateKey):
return NIOSSHPublicKey(backingKey: .ecdsaP256(privateKey.publicKey))
Expand Down
Loading