Skip to content
Merged
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
31 changes: 31 additions & 0 deletions Sources/NIOSSH/Keys And Signatures/NIOSSHPrivateKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,23 @@ 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 {
case .ed25519:
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
}
}
}
Expand All @@ -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
}
}

Expand All @@ -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
}
}

Expand All @@ -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
}
}
}
Expand All @@ -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
}
}
}
70 changes: 70 additions & 0 deletions Tests/NIOSSHTests/EndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<NIOSSHUserAuthenticationOffer?>) {
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<NIOSSHUserAuthenticationOutcome>) {
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!

Expand Down Expand Up @@ -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
}
}