-
Notifications
You must be signed in to change notification settings - Fork 61
Add RSA key support (ssh-rsa, rsa-sha2-256, rsa-sha2-512) #219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,9 @@ | |
| //===----------------------------------------------------------------------===// | ||
|
|
||
| @preconcurrency import Crypto | ||
| import _CryptoExtras | ||
| import NIOCore | ||
| import NIOFoundationCompat | ||
|
|
||
| #if canImport(FoundationEssentials) | ||
| import FoundationEssentials | ||
|
|
@@ -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) | ||
|
|
@@ -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"] | ||
Lukasa marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| #if canImport(Darwin) | ||
| case .secureEnclaveP256: | ||
| return ["ecdsa-sha2-nistp256"] | ||
|
|
@@ -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) | ||
|
|
@@ -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): | ||
|
|
@@ -125,7 +158,7 @@ extension NIOSSHPrivateKey { | |
| } | ||
| } | ||
|
|
||
| func sign(_ payload: UserAuthSignablePayload) throws -> NIOSSHSignature { | ||
| func sign(_ payload: UserAuthSignablePayload, rsaSignatureAlgorithm: RSASignatureAlgorithm = .sha512) throws -> NIOSSHSignature { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
@@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
|
@@ -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)) | ||
|
|
||
There was a problem hiding this comment.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.