Skip to content

Commit

Permalink
universal bootstrap support (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
weissi authored Mar 23, 2020
1 parent 7add274 commit 584c0d0
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 45 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Package.resolved
/docs
DerivedData
/.idea
.swiftpm
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ let package = Package(
MANGLE_END */
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.14.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.15.0"),
],
targets: [
.target(name: "CNIOBoringSSL"),
Expand Down
39 changes: 29 additions & 10 deletions Sources/NIOSSL/NIOSSLClientHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

import NIO

private extension String {
func isIPAddress() -> Bool {
extension String {
private func isIPAddress() -> Bool {
// We need some scratch space to let inet_pton write into.
var ipv4Addr = in_addr()
var ipv6Addr = in6_addr()
Expand All @@ -25,6 +25,21 @@ private extension String {
inet_pton(AF_INET6, ptr, &ipv6Addr) == 1
}
}

func validateSNIServerName() throws {
guard !self.isIPAddress() else {
throw NIOSSLExtraError.cannotUseIPAddressInSNI(ipAddress: self)
}

// no 0 bytes
guard !self.utf8.contains(0) else {
throw NIOSSLExtraError.invalidSNIHostname
}

guard (1 ... 255).contains(self.utf8.count) else {
throw NIOSSLExtraError.invalidSNIHostname
}
}
}

/// A channel handler that wraps a channel in TLS using NIOSSL.
Expand All @@ -43,12 +58,14 @@ public final class NIOSSLClientHandler: NIOSSLHandler {

connection.setConnectState()
if let serverHostname = serverHostname {
if serverHostname.isIPAddress() {
throw NIOSSLExtraError.cannotUseIPAddressInSNI(ipAddress: serverHostname)
}
try serverHostname.validateSNIServerName()

// IP addresses must not be provided in the SNI extension, so filter them.
try connection.setServerName(name: serverHostname)
do {
try connection.setServerName(name: serverHostname)
} catch {
preconditionFailure("Bug in NIOSSL (please report): \(Array(serverHostname.utf8)) passed NIOSSL's hostname test but failed in BoringSSL.")
}
}

if let verificationCallback = verificationCallback {
Expand All @@ -71,12 +88,14 @@ public final class NIOSSLClientHandler: NIOSSLHandler {

connection.setConnectState()
if let serverHostname = serverHostname {
if serverHostname.isIPAddress() {
throw NIOSSLExtraError.cannotUseIPAddressInSNI(ipAddress: serverHostname)
}
try serverHostname.validateSNIServerName()

// IP addresses must not be provided in the SNI extension, so filter them.
try connection.setServerName(name: serverHostname)
do {
try connection.setServerName(name: serverHostname)
} catch {
preconditionFailure("Bug in NIOSSL (please report): \(Array(serverHostname.utf8)) passed NIOSSL's hostname test but failed in BoringSSL.")
}
}

if let verificationCallback = optionalCustomVerificationCallback {
Expand Down
12 changes: 12 additions & 0 deletions Sources/NIOSSL/SSLErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ extension NIOSSLExtraError {
case failedToValidateHostname
case serverHostnameImpossibleToMatch
case cannotUseIPAddressInSNI
case invalidSNIHostname
}
}

Expand All @@ -178,6 +179,17 @@ extension NIOSSLExtraError {
/// IP addresses may not be used in SNI.
public static let cannotUseIPAddressInSNI = NIOSSLExtraError(baseError: .cannotUseIPAddressInSNI, description: nil)

/// The SNI hostname requirements have not been met.
///
/// - note: Should the provided SNI hostname be an IP address instead, `.cannotUseIPAddressInSNI` is thrown instead
/// of this error.
///
/// Reasons a hostname might not meet the requirements:
/// - hostname in UTF8 is more than 255 bytes
/// - hostname is the empty string
/// - hostname contains the `0` unicode scalar (which would be encoded as the `0` byte which is unsupported).
public static let invalidSNIHostname = NIOSSLExtraError(baseError: .invalidSNIHostname, description: nil)

@inline(never)
internal static func failedToValidateHostname(expectedName: String) -> NIOSSLExtraError {
let description = "Couldn't find \(expectedName) in certificate from peer"
Expand Down
68 changes: 68 additions & 0 deletions Sources/NIOSSL/UniversalBootstrapSupport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIO

/// A TLS provider to bootstrap TLS-enabled connections with `NIOClientTCPBootstrap`.
///
/// Example:
///
/// // TLS setup.
/// let configuration = TLSConfiguration.forClient()
/// let sslContext = try NIOSSLContext(configuration: configuration)
///
/// // Creating the "universal bootstrap" with the `NIOSSLClientTLSProvider`.
/// let tlsProvider = NIOSSLClientTLSProvider<ClientBootstrap>(context: sslContext, serverHostname: "example.com")
/// let bootstrap = NIOClientTCPBootstrap(ClientBootstrap(group: group), tls: tlsProvider)
///
/// // Bootstrapping a connection using the "universal bootstrapping mechanism"
/// let connection = bootstrap.enableTLS()
/// .connect(to: "example.com")
/// .wait()
public struct NIOSSLClientTLSProvider<Bootstrap: NIOClientTCPBootstrapProtocol>: NIOClientTLSProvider {
public typealias Bootstrap = Bootstrap

let context: NIOSSLContext
let serverHostname: String?
let customVerificationCallback: NIOSSLCustomVerificationCallback?

/// Construct the TLS provider with the necessary configuration.
public init(context: NIOSSLContext,
serverHostname: String?,
customVerificationCallback: NIOSSLCustomVerificationCallback? = nil) throws {
try serverHostname.map {
try $0.validateSNIServerName()
}
self.context = context
self.serverHostname = serverHostname
self.customVerificationCallback = customVerificationCallback
}

/// Enable TLS on the bootstrap. This is not a function you will typically call as a user, it is called by
/// `NIOClientTCPBootstrap`.
public func enableTLS(_ bootstrap: Bootstrap) -> Bootstrap {
// NIOSSLClientHandler.init only throws because of `malloc` error and invalid SNI hostnames. We want to crash
// on malloc error and we pre-checked the SNI hostname in `init` so that should be impossible here.
return bootstrap.protocolHandlers {
if let customVerificationCallback = self.customVerificationCallback {
return [try! NIOSSLClientHandler(context: self.context,
serverHostname: self.serverHostname,
customVerificationCallback: customVerificationCallback)]
} else {
return [try! NIOSSLClientHandler(context: self.context,
serverHostname: self.serverHostname)]
}
}
}
}
5 changes: 5 additions & 0 deletions Tests/NIOSSLTests/ClientSNITests+XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ extension ClientSNITests {
("testNoSNILeadsToNoExtension", testNoSNILeadsToNoExtension),
("testSNIIsRejectedForIPv4Addresses", testSNIIsRejectedForIPv4Addresses),
("testSNIIsRejectedForIPv6Addresses", testSNIIsRejectedForIPv6Addresses),
("testSNIIsRejectedForEmptyHostname", testSNIIsRejectedForEmptyHostname),
("testSNIIsRejectedForTooLongHostname", testSNIIsRejectedForTooLongHostname),
("testSNIIsRejectedFor0Byte", testSNIIsRejectedFor0Byte),
("testSNIIsNotRejectedForAnyOfTheFirst1000CodeUnits", testSNIIsNotRejectedForAnyOfTheFirst1000CodeUnits),
("testSNIIsNotRejectedForVeryWeirdCharacters", testSNIIsNotRejectedForVeryWeirdCharacters),
]
}
}
Expand Down
69 changes: 66 additions & 3 deletions Tests/NIOSSLTests/ClientSNITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,80 @@ class ClientSNITests: XCTestCase {

func testSNIIsRejectedForIPv4Addresses() throws {
let context = try configuredSSLContext()

XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: "192.168.0.1")){ error in

let testString = "192.168.0.1"
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
XCTAssertEqual(.cannotUseIPAddressInSNI, error as? NIOSSLExtraError)
}
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)){ error in
XCTAssertEqual(.cannotUseIPAddressInSNI, error as? NIOSSLExtraError)
}
}

func testSNIIsRejectedForIPv6Addresses() throws {
let context = try configuredSSLContext()

XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: "fe80::200:f8ff:fe21:67cf")){ error in
let testString = "fe80::200:f8ff:fe21:67cf"
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
XCTAssertEqual(.cannotUseIPAddressInSNI, error as? NIOSSLExtraError)
}
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)){ error in
XCTAssertEqual(.cannotUseIPAddressInSNI, error as? NIOSSLExtraError)
}

}

func testSNIIsRejectedForEmptyHostname() throws {
let context = try configuredSSLContext()

let testString = ""
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
}
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)){ error in
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
}
}

func testSNIIsRejectedForTooLongHostname() throws {
let context = try configuredSSLContext()

let testString = String(repeating: "x", count: 256)
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
}
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)){ error in
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
}
}

func testSNIIsRejectedFor0Byte() throws {
let context = try configuredSSLContext()

let testString = String(UnicodeScalar(0)!)
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
}
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)) { error in
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
}
}

func testSNIIsNotRejectedForAnyOfTheFirst1000CodeUnits() throws {
let context = try configuredSSLContext()

for testString in (1...Int(1000)).compactMap({ UnicodeScalar($0).map { String($0) } }) {
XCTAssertNoThrow(try NIOSSLClientHandler(context: context, serverHostname: testString))
XCTAssertNoThrow(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString))
}
}

func testSNIIsNotRejectedForVeryWeirdCharacters() throws {
let context = try configuredSSLContext()

let testString = "😎🥶💥🏴󠁧󠁢󠁥󠁮󠁧󠁿👩‍💻"
XCTAssertLessThanOrEqual(testString.utf8.count, 255) // just to check we didn't make this too large.
XCTAssertNoThrow(try NIOSSLClientHandler(context: context, serverHostname: testString))
XCTAssertNoThrow(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString))
}
}
Loading

0 comments on commit 584c0d0

Please sign in to comment.