diff --git a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift index f19f8c8..c112d7e 100644 --- a/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift +++ b/Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift @@ -38,6 +38,12 @@ public struct NIOSSHPrivateKey { self.backingKey = .ecdsaP256(key) } + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + public init(secureEnclaveP256Key key: SecureEnclave.P256.Signing.PrivateKey) { + self.backingKey = .secureEnclaveP256(key) + } + #endif + // The algorithms that apply to this host key. internal var hostKeyAlgorithms: [Substring] { switch self.backingKey { @@ -45,6 +51,10 @@ public struct NIOSSHPrivateKey { return ["ssh-ed25519"] case .ecdsaP256: return ["ecdsa-sha2-nistp256"] + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + case .secureEnclaveP256: + return ["ecdsa-sha2-nistp256"] + #endif } } } @@ -54,6 +64,10 @@ extension NIOSSHPrivateKey { internal enum BackingKey { case ed25519(Curve25519.Signing.PrivateKey) case ecdsaP256(P256.Signing.PrivateKey) + + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + case secureEnclaveP256(SecureEnclave.P256.Signing.PrivateKey) + #endif } } @@ -70,6 +84,14 @@ extension NIOSSHPrivateKey { try key.signature(for: ptr) } return SSHSignature(backingSignature: .ecdsaP256(signature)) + + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + case .secureEnclaveP256(let key): + let signature = try digest.withUnsafeBytes { ptr in + try key.signature(for: ptr) + } + return SSHSignature(backingSignature: .ecdsaP256(signature)) + #endif } } @@ -81,6 +103,11 @@ extension NIOSSHPrivateKey { case .ecdsaP256(let key): let signature = try key.signature(for: payload.bytes.readableBytesView) return SSHSignature(backingSignature: .ecdsaP256(signature)) + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + case .secureEnclaveP256(let key): + let signature = try key.signature(for: payload.bytes.readableBytesView) + return SSHSignature(backingSignature: .ecdsaP256(signature)) + #endif } } } @@ -93,6 +120,10 @@ extension NIOSSHPrivateKey { return NIOSSHPublicKey(backingKey: .ed25519(privateKey.publicKey)) case .ecdsaP256(let privateKey): return NIOSSHPublicKey(backingKey: .ecdsaP256(privateKey.publicKey)) + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + case .secureEnclaveP256(let privateKey): + return NIOSSHPublicKey(backingKey: .ecdsaP256(privateKey.publicKey)) + #endif } } } diff --git a/Tests/NIOSSHTests/EndToEndTests.swift b/Tests/NIOSSHTests/EndToEndTests.swift index 41688f6..2e0656e 100644 --- a/Tests/NIOSSHTests/EndToEndTests.swift +++ b/Tests/NIOSSHTests/EndToEndTests.swift @@ -136,6 +136,47 @@ final class UserEventExpecter: ChannelInboundHandler { } } +final class PrivateKeyClientAuth: NIOSSHClientUserAuthenticationDelegate { + private var key: NIOSSHPrivateKey? + + init(_ key: NIOSSHPrivateKey) { + self.key = key + } + + func nextAuthenticationType(availableMethods: NIOSSHAvailableUserAuthenticationMethods, nextChallengePromise: EventLoopPromise) { + guard availableMethods.contains(.publicKey), let key = self.key else { + nextChallengePromise.succeed(nil) + return + } + + self.key = nil + nextChallengePromise.succeed(.init(username: "foo", serviceName: "ssh-connection", offer: .privateKey(.init(privateKey: key)))) + } +} + +final class ExpectPublicKeyAuth: NIOSSHServerUserAuthenticationDelegate { + private var key: NIOSSHPublicKey + + init(_ key: NIOSSHPublicKey) { + self.key = key + } + + let supportedAuthenticationMethods: NIOSSHAvailableUserAuthenticationMethods = .publicKey + + func requestReceived(request: NIOSSHUserAuthenticationRequest, responsePromise: EventLoopPromise) { + guard case .publicKey(let actualKey) = request.request else { + responsePromise.succeed(.failure) + return + } + + if actualKey.publicKey == self.key { + responsePromise.succeed(.success) + } else { + responsePromise.succeed(.failure) + } + } +} + class EndToEndTests: XCTestCase { var channel: BackToBackEmbeddedChannel! @@ -325,4 +366,33 @@ class EndToEndTests: XCTestCase { handler?.sendTCPForwardingRequest(.listen(host: "localhost", port: 1234), promise: promise) XCTAssertEqual(err as? ChannelError, .ioOnClosedChannel) } + + func testSecureEnclaveKeys() throws { + // This is a quick end-to-end test that validates that we support secure enclave private keys + // on appropriate platforms. + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) + // If we can't create this key, we skip the test. + let key: NIOSSHPrivateKey + do { + key = try .init(secureEnclaveP256Key: .init()) + } catch { + return + } + + // We use the Secure Enclave keys for everything, just because we can. + var harness = TestHarness() + harness.serverHostKeys = [key] + harness.clientAuthDelegate = PrivateKeyClientAuth(key) + harness.serverAuthDelegate = ExpectPublicKeyAuth(key.publicKey) + + XCTAssertNoThrow(try self.channel.configureWithHarness(harness)) + XCTAssertNoThrow(try self.channel.activate()) + XCTAssertNoThrow(try self.channel.interactInMemory()) + + // Create a channel, again, just because we can. + _ = try self.channel.createNewChannel() + XCTAssertNoThrow(try self.channel.interactInMemory()) + XCTAssertEqual(self.channel.activeServerChannels.count, 1) + #endif + } }