Skip to content

Commit 584c0d0

Browse files
authored
universal bootstrap support (#185)
1 parent 7add274 commit 584c0d0

9 files changed

+253
-45
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Package.resolved
88
/docs
99
DerivedData
1010
/.idea
11+
.swiftpm

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ let package = Package(
3535
MANGLE_END */
3636
],
3737
dependencies: [
38-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.14.0"),
38+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.15.0"),
3939
],
4040
targets: [
4141
.target(name: "CNIOBoringSSL"),

Sources/NIOSSL/NIOSSLClientHandler.swift

+29-10
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
import NIO
1616

17-
private extension String {
18-
func isIPAddress() -> Bool {
17+
extension String {
18+
private func isIPAddress() -> Bool {
1919
// We need some scratch space to let inet_pton write into.
2020
var ipv4Addr = in_addr()
2121
var ipv6Addr = in6_addr()
@@ -25,6 +25,21 @@ private extension String {
2525
inet_pton(AF_INET6, ptr, &ipv6Addr) == 1
2626
}
2727
}
28+
29+
func validateSNIServerName() throws {
30+
guard !self.isIPAddress() else {
31+
throw NIOSSLExtraError.cannotUseIPAddressInSNI(ipAddress: self)
32+
}
33+
34+
// no 0 bytes
35+
guard !self.utf8.contains(0) else {
36+
throw NIOSSLExtraError.invalidSNIHostname
37+
}
38+
39+
guard (1 ... 255).contains(self.utf8.count) else {
40+
throw NIOSSLExtraError.invalidSNIHostname
41+
}
42+
}
2843
}
2944

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

4459
connection.setConnectState()
4560
if let serverHostname = serverHostname {
46-
if serverHostname.isIPAddress() {
47-
throw NIOSSLExtraError.cannotUseIPAddressInSNI(ipAddress: serverHostname)
48-
}
61+
try serverHostname.validateSNIServerName()
4962

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

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

7289
connection.setConnectState()
7390
if let serverHostname = serverHostname {
74-
if serverHostname.isIPAddress() {
75-
throw NIOSSLExtraError.cannotUseIPAddressInSNI(ipAddress: serverHostname)
76-
}
91+
try serverHostname.validateSNIServerName()
7792

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

82101
if let verificationCallback = optionalCustomVerificationCallback {

Sources/NIOSSL/SSLErrors.swift

+12
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ extension NIOSSLExtraError {
164164
case failedToValidateHostname
165165
case serverHostnameImpossibleToMatch
166166
case cannotUseIPAddressInSNI
167+
case invalidSNIHostname
167168
}
168169
}
169170

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

182+
/// The SNI hostname requirements have not been met.
183+
///
184+
/// - note: Should the provided SNI hostname be an IP address instead, `.cannotUseIPAddressInSNI` is thrown instead
185+
/// of this error.
186+
///
187+
/// Reasons a hostname might not meet the requirements:
188+
/// - hostname in UTF8 is more than 255 bytes
189+
/// - hostname is the empty string
190+
/// - hostname contains the `0` unicode scalar (which would be encoded as the `0` byte which is unsupported).
191+
public static let invalidSNIHostname = NIOSSLExtraError(baseError: .invalidSNIHostname, description: nil)
192+
181193
@inline(never)
182194
internal static func failedToValidateHostname(expectedName: String) -> NIOSSLExtraError {
183195
let description = "Couldn't find \(expectedName) in certificate from peer"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIO
16+
17+
/// A TLS provider to bootstrap TLS-enabled connections with `NIOClientTCPBootstrap`.
18+
///
19+
/// Example:
20+
///
21+
/// // TLS setup.
22+
/// let configuration = TLSConfiguration.forClient()
23+
/// let sslContext = try NIOSSLContext(configuration: configuration)
24+
///
25+
/// // Creating the "universal bootstrap" with the `NIOSSLClientTLSProvider`.
26+
/// let tlsProvider = NIOSSLClientTLSProvider<ClientBootstrap>(context: sslContext, serverHostname: "example.com")
27+
/// let bootstrap = NIOClientTCPBootstrap(ClientBootstrap(group: group), tls: tlsProvider)
28+
///
29+
/// // Bootstrapping a connection using the "universal bootstrapping mechanism"
30+
/// let connection = bootstrap.enableTLS()
31+
/// .connect(to: "example.com")
32+
/// .wait()
33+
public struct NIOSSLClientTLSProvider<Bootstrap: NIOClientTCPBootstrapProtocol>: NIOClientTLSProvider {
34+
public typealias Bootstrap = Bootstrap
35+
36+
let context: NIOSSLContext
37+
let serverHostname: String?
38+
let customVerificationCallback: NIOSSLCustomVerificationCallback?
39+
40+
/// Construct the TLS provider with the necessary configuration.
41+
public init(context: NIOSSLContext,
42+
serverHostname: String?,
43+
customVerificationCallback: NIOSSLCustomVerificationCallback? = nil) throws {
44+
try serverHostname.map {
45+
try $0.validateSNIServerName()
46+
}
47+
self.context = context
48+
self.serverHostname = serverHostname
49+
self.customVerificationCallback = customVerificationCallback
50+
}
51+
52+
/// Enable TLS on the bootstrap. This is not a function you will typically call as a user, it is called by
53+
/// `NIOClientTCPBootstrap`.
54+
public func enableTLS(_ bootstrap: Bootstrap) -> Bootstrap {
55+
// NIOSSLClientHandler.init only throws because of `malloc` error and invalid SNI hostnames. We want to crash
56+
// on malloc error and we pre-checked the SNI hostname in `init` so that should be impossible here.
57+
return bootstrap.protocolHandlers {
58+
if let customVerificationCallback = self.customVerificationCallback {
59+
return [try! NIOSSLClientHandler(context: self.context,
60+
serverHostname: self.serverHostname,
61+
customVerificationCallback: customVerificationCallback)]
62+
} else {
63+
return [try! NIOSSLClientHandler(context: self.context,
64+
serverHostname: self.serverHostname)]
65+
}
66+
}
67+
}
68+
}

Tests/NIOSSLTests/ClientSNITests+XCTest.swift

+5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ extension ClientSNITests {
3131
("testNoSNILeadsToNoExtension", testNoSNILeadsToNoExtension),
3232
("testSNIIsRejectedForIPv4Addresses", testSNIIsRejectedForIPv4Addresses),
3333
("testSNIIsRejectedForIPv6Addresses", testSNIIsRejectedForIPv6Addresses),
34+
("testSNIIsRejectedForEmptyHostname", testSNIIsRejectedForEmptyHostname),
35+
("testSNIIsRejectedForTooLongHostname", testSNIIsRejectedForTooLongHostname),
36+
("testSNIIsRejectedFor0Byte", testSNIIsRejectedFor0Byte),
37+
("testSNIIsNotRejectedForAnyOfTheFirst1000CodeUnits", testSNIIsNotRejectedForAnyOfTheFirst1000CodeUnits),
38+
("testSNIIsNotRejectedForVeryWeirdCharacters", testSNIIsNotRejectedForVeryWeirdCharacters),
3439
]
3540
}
3641
}

Tests/NIOSSLTests/ClientSNITests.swift

+66-3
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,80 @@ class ClientSNITests: XCTestCase {
7979

8080
func testSNIIsRejectedForIPv4Addresses() throws {
8181
let context = try configuredSSLContext()
82-
83-
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: "192.168.0.1")){ error in
82+
83+
let testString = "192.168.0.1"
84+
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
85+
XCTAssertEqual(.cannotUseIPAddressInSNI, error as? NIOSSLExtraError)
86+
}
87+
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)){ error in
8488
XCTAssertEqual(.cannotUseIPAddressInSNI, error as? NIOSSLExtraError)
8589
}
8690
}
8791

8892
func testSNIIsRejectedForIPv6Addresses() throws {
8993
let context = try configuredSSLContext()
9094

91-
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: "fe80::200:f8ff:fe21:67cf")){ error in
95+
let testString = "fe80::200:f8ff:fe21:67cf"
96+
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
9297
XCTAssertEqual(.cannotUseIPAddressInSNI, error as? NIOSSLExtraError)
9398
}
99+
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)){ error in
100+
XCTAssertEqual(.cannotUseIPAddressInSNI, error as? NIOSSLExtraError)
101+
}
102+
103+
}
104+
105+
func testSNIIsRejectedForEmptyHostname() throws {
106+
let context = try configuredSSLContext()
107+
108+
let testString = ""
109+
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
110+
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
111+
}
112+
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)){ error in
113+
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
114+
}
115+
}
116+
117+
func testSNIIsRejectedForTooLongHostname() throws {
118+
let context = try configuredSSLContext()
119+
120+
let testString = String(repeating: "x", count: 256)
121+
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
122+
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
123+
}
124+
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)){ error in
125+
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
126+
}
127+
}
128+
129+
func testSNIIsRejectedFor0Byte() throws {
130+
let context = try configuredSSLContext()
131+
132+
let testString = String(UnicodeScalar(0)!)
133+
XCTAssertThrowsError(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString)) { error in
134+
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
135+
}
136+
XCTAssertThrowsError(try NIOSSLClientHandler(context: context, serverHostname: testString)) { error in
137+
XCTAssertEqual(.invalidSNIHostname, error as? NIOSSLExtraError)
138+
}
139+
}
140+
141+
func testSNIIsNotRejectedForAnyOfTheFirst1000CodeUnits() throws {
142+
let context = try configuredSSLContext()
143+
144+
for testString in (1...Int(1000)).compactMap({ UnicodeScalar($0).map { String($0) } }) {
145+
XCTAssertNoThrow(try NIOSSLClientHandler(context: context, serverHostname: testString))
146+
XCTAssertNoThrow(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString))
147+
}
148+
}
149+
150+
func testSNIIsNotRejectedForVeryWeirdCharacters() throws {
151+
let context = try configuredSSLContext()
152+
153+
let testString = "😎🥶💥🏴󠁧󠁢󠁥󠁮󠁧󠁿👩‍💻"
154+
XCTAssertLessThanOrEqual(testString.utf8.count, 255) // just to check we didn't make this too large.
155+
XCTAssertNoThrow(try NIOSSLClientHandler(context: context, serverHostname: testString))
156+
XCTAssertNoThrow(try NIOSSLClientTLSProvider<ClientBootstrap>(context: context, serverHostname: testString))
94157
}
95158
}

0 commit comments

Comments
 (0)