Skip to content

Add NIOTransportServices TLS Configuration options #321

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

Closed
Closed
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
27 changes: 27 additions & 0 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,10 @@ public class HTTPClient {
public struct Configuration {
/// TLS configuration, defaults to `TLSConfiguration.forClient()`.
public var tlsConfiguration: Optional<TLSConfiguration>
#if canImport(Network)
/// TLS configuration for NIO Transport services.
public var tsTlsConfiguration: Optional<TSTLSConfiguration>
#endif
/// Enables following 3xx redirects automatically, defaults to `RedirectConfiguration()`.
///
/// Following redirects are supported:
Expand Down Expand Up @@ -658,6 +662,9 @@ public class HTTPClient {
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled) {
self.tlsConfiguration = tlsConfiguration
#if canImport(Network)
self.tsTlsConfiguration = nil
#endif
self.redirectConfiguration = redirectConfiguration ?? RedirectConfiguration()
self.timeout = timeout
self.connectionPool = connectionPool
Expand All @@ -666,6 +673,26 @@ public class HTTPClient {
self.decompression = decompression
}

#if canImport(Network)
@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
public init(tsTlsConfiguration: TSTLSConfiguration,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
connectionPool: ConnectionPool = ConnectionPool(),
proxy: Proxy? = nil,
ignoreUncleanSSLShutdown: Bool = false,
decompression: Decompression = .disabled) {
self.tlsConfiguration = nil
self.tsTlsConfiguration = tsTlsConfiguration
self.redirectConfiguration = redirectConfiguration ?? RedirectConfiguration()
self.timeout = timeout
self.connectionPool = connectionPool
self.proxy = proxy
self.ignoreUncleanSSLShutdown = ignoreUncleanSSLShutdown
self.decompression = decompression
}
#endif

public init(tlsConfiguration: TLSConfiguration? = nil,
redirectConfiguration: RedirectConfiguration? = nil,
timeout: Timeout = Timeout(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@

#if canImport(Network)

import Foundation
import Network
import NIOSSL
import NIOTransportServices

extension TLSVersion {
/// return Network framework TLS protocol version
Expand Down
157 changes: 157 additions & 0 deletions Sources/AsyncHTTPClient/NIOTransportServices/TSTLSConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2020 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if canImport(Network)

import Network

extension tls_protocol_version_t {
var sslProtocol: SSLProtocol {
switch self {
case .TLSv10:
return .tlsProtocol1
case .TLSv11:
return .tlsProtocol11
case .TLSv12:
return .tlsProtocol12
case .TLSv13:
return .tlsProtocol13
case .DTLSv10:
return .dtlsProtocol1
case .DTLSv12:
return .dtlsProtocol12
@unknown default:
return .tlsProtocol1
}
}
}

/// Certificate verification modes.
public enum TSCertificateVerification {
/// All certificate verification disabled.
case none

/// Certificates will be validated against the trust store and checked
/// against the hostname of the service we are contacting.
case fullVerification
}

/// TLS configuration for NIO Transport Services
public struct TSTLSConfiguration {
/// The minimum TLS version to allow in negotiation. Defaults to tlsv1.
public var minimumTLSVersion: tls_protocol_version_t

/// The maximum TLS version to allow in negotiation. If nil, there is no upper limit. Defaults to nil.
public var maximumTLSVersion: tls_protocol_version_t?

/// The trust roots to use to validate certificates. This only needs to be provided if you intend to validate
/// certificates.
public var trustRoots: [SecCertificate]?

/// The identity associated with the leaf certificate.
public var clientIdentity: SecIdentity?

/// The application protocols to use in the connection.
public var applicationProtocols: [String]

/// Whether to verify remote certificates.
public var certificateVerification: TSCertificateVerification

/// Initialize TSTLSConfiguration
public init(
minimumTLSVersion: tls_protocol_version_t = .TLSv10,
maximumTLSVersion: tls_protocol_version_t? = nil,
trustRoots: [SecCertificate]? = nil,
clientIdentity: SecIdentity? = nil,
applicationProtocols: [String] = [],
certificateVerification: TSCertificateVerification = .fullVerification
) {
self.minimumTLSVersion = minimumTLSVersion
self.maximumTLSVersion = maximumTLSVersion
self.trustRoots = trustRoots
self.clientIdentity = clientIdentity
self.applicationProtocols = applicationProtocols
self.certificateVerification = certificateVerification
}
}

@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
extension TSTLSConfiguration {
func getNWProtocolTLSOptions() -> NWProtocolTLS.Options {
let options = NWProtocolTLS.Options()

// minimum TLS protocol
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, self.minimumTLSVersion)
} else {
sec_protocol_options_set_tls_min_version(options.securityProtocolOptions, self.minimumTLSVersion.sslProtocol)
}

// maximum TLS protocol
if let maximumTLSVersion = self.maximumTLSVersion {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, maximumTLSVersion)
} else {
sec_protocol_options_set_tls_max_version(options.securityProtocolOptions, maximumTLSVersion.sslProtocol)
}
}

// set client identity
if let clientIdentity = self.clientIdentity, let secClientIdentity = sec_identity_create(clientIdentity) {
sec_protocol_options_set_local_identity(options.securityProtocolOptions, secClientIdentity)
}

// add application protocols
self.applicationProtocols.forEach {
sec_protocol_options_add_tls_application_protocol(options.securityProtocolOptions, $0)
}

if self.certificateVerification != .fullVerification || self.trustRoots != nil {
// add verify block to control certificate verification
sec_protocol_options_set_verify_block(
options.securityProtocolOptions,
{ _, sec_trust, sec_protocol_verify_complete in
guard self.certificateVerification != .none else {
sec_protocol_verify_complete(true)
return
}

let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
if let trustRootCertificates = trustRoots {
SecTrustSetAnchorCertificates(trust, trustRootCertificates as CFArray)
}
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) {
SecTrustEvaluateAsyncWithError(trust, Self.tlsDispatchQueue) { _, result, _ in
sec_protocol_verify_complete(result)
}
} else {
SecTrustEvaluateAsync(trust, Self.tlsDispatchQueue) { _, result in
switch result {
case .proceed, .unspecified:
sec_protocol_verify_complete(true)
default:
sec_protocol_verify_complete(false)
}
}
}
}, Self.tlsDispatchQueue
)
}
return options
}

/// Dispatch queue used by Network framework TLS to control certificate verification
static var tlsDispatchQueue = DispatchQueue(label: "TSTLSConfiguration")
}
#endif
10 changes: 8 additions & 2 deletions Sources/AsyncHTTPClient/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,14 @@ extension NIOClientTCPBootstrap {
bootstrap = NIOClientTCPBootstrap(tsBootstrap, tls: NIOInsecureNoTLS())
} else {
// create NIOClientTCPBootstrap with NIOTS TLS provider
let tlsConfiguration = configuration.tlsConfiguration ?? TLSConfiguration.forClient()
let parameters = tlsConfiguration.getNWProtocolTLSOptions()
let parameters: NWProtocolTLS.Options
if let tsTlsConfiguration = configuration.tsTlsConfiguration {
parameters = tsTlsConfiguration.getNWProtocolTLSOptions()
} else if let tlsConfiguration = configuration.tlsConfiguration {
parameters = tlsConfiguration.getNWProtocolTLSOptions()
} else {
parameters = .init()
}
let tlsProvider = NIOTSClientTLSProvider(tlsOptions: parameters)
bootstrap = NIOClientTCPBootstrap(tsBootstrap, tls: tlsProvider)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ extension HTTPClientNIOTSTests {
("testTLSFailError", testTLSFailError),
("testConnectionFailError", testConnectionFailError),
("testTLSVersionError", testTLSVersionError),
("testHTTPS", testHTTPS),
]
}
}
18 changes: 17 additions & 1 deletion Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class HTTPClientNIOTSTests: XCTestCase {
let httpBin = HTTPBin(ssl: true)
let httpClient = HTTPClient(
eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(tlsConfiguration: TLSConfiguration.forClient(minimumTLSVersion: .tlsv11, maximumTLSVersion: .tlsv1, certificateVerification: .none))
configuration: .init(tsTlsConfiguration: .init(minimumTLSVersion: .TLSv11, maximumTLSVersion: .TLSv10, certificateVerification: .none))
)
defer {
XCTAssertNoThrow(try httpClient.syncShutdown(requiresCleanClose: true))
Expand All @@ -108,4 +108,20 @@ class HTTPClientNIOTSTests: XCTestCase {
}
#endif
}

func testHTTPS() throws {
guard isTestingNIOTS() else { return }
#if canImport(Network)
let localHTTPBin = HTTPBin(ssl: true)
let localClient = HTTPClient(eventLoopGroupProvider: .shared(self.clientGroup),
configuration: .init(tsTlsConfiguration: .init(certificateVerification: .none)))
defer {
XCTAssertNoThrow(try localClient.syncShutdown())
XCTAssertNoThrow(try localHTTPBin.shutdown())
}

let response = try localClient.get(url: "https://localhost:\(localHTTPBin.port)/get").wait()
XCTAssertEqual(.ok, response.status)
#endif
}
}