diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..2fb33437e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.build +.git \ No newline at end of file diff --git a/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift b/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift index 6dcebb52a..86867c6f9 100644 --- a/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift +++ b/Sources/AsyncHTTPClient/NIOTransportServices/TLSConfiguration.swift @@ -16,6 +16,7 @@ import Foundation import Network + import NIO import NIOSSL import NIOTransportServices @@ -58,9 +59,25 @@ /// create NWProtocolTLS.Options for use with NIOTransportServices from the NIOSSL TLSConfiguration /// - /// - Parameter queue: Dispatch queue to run `sec_protocol_options_set_verify_block` on. + /// - Parameter eventLoop: EventLoop to wait for creation of options on + /// - Returns: Future holding NWProtocolTLS Options + func getNWProtocolTLSOptions(on eventLoop: EventLoop) -> EventLoopFuture { + let promise = eventLoop.makePromise(of: NWProtocolTLS.Options.self) + Self.tlsDispatchQueue.async { + do { + let options = try self.getNWProtocolTLSOptions() + promise.succeed(options) + } catch { + promise.fail(error) + } + } + return promise.futureResult + } + + /// create NWProtocolTLS.Options for use with NIOTransportServices from the NIOSSL TLSConfiguration + /// /// - Returns: Equivalent NWProtocolTLS Options - func getNWProtocolTLSOptions() -> NWProtocolTLS.Options { + func getNWProtocolTLSOptions() throws -> NWProtocolTLS.Options { let options = NWProtocolTLS.Options() let useMTELGExplainer = """ @@ -109,6 +126,11 @@ preconditionFailure("TLSConfiguration.keyLogCallback is not supported. \(useMTELGExplainer)") } + // the certificate chain + if self.certificateChain.count > 0 { + preconditionFailure("TLSConfiguration.certificateChain is not supported. \(useMTELGExplainer)") + } + // private key if self.privateKey != nil { preconditionFailure("TLSConfiguration.privateKey is not supported. \(useMTELGExplainer)") @@ -117,30 +139,60 @@ // renegotiation support key is unsupported // trust roots - if let trustRoots = self.trustRoots { - guard case .default = trustRoots else { - preconditionFailure("TLSConfiguration.trustRoots != .default is not supported. \(useMTELGExplainer)") + var secTrustRoots: [SecCertificate]? + switch trustRoots { + case .some(.certificates(let certificates)): + secTrustRoots = try certificates.compactMap { certificate in + try SecCertificateCreateWithData(nil, Data(certificate.toDERBytes()) as CFData) + } + case .some(.file(let file)): + let certificates = try NIOSSLCertificate.fromPEMFile(file) + secTrustRoots = try certificates.compactMap { certificate in + try SecCertificateCreateWithData(nil, Data(certificate.toDERBytes()) as CFData) } + + case .some(.default), .none: + break } - switch self.certificateVerification { - case .none: + precondition(self.certificateVerification != .noHostnameVerification, + "TLSConfiguration.certificateVerification = .noHostnameVerification is not supported. \(useMTELGExplainer)") + + if certificateVerification != .fullVerification || trustRoots != nil { // add verify block to control certificate verification sec_protocol_options_set_verify_block( options.securityProtocolOptions, - { _, _, sec_protocol_verify_complete in - sec_protocol_verify_complete(true) - }, TLSConfiguration.tlsDispatchQueue + { _, 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 = secTrustRoots { + SecTrustSetAnchorCertificates(trust, trustRootCertificates as CFArray) + } + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) { + dispatchPrecondition(condition: .onQueue(Self.tlsDispatchQueue)) + SecTrustEvaluateAsyncWithError(trust, Self.tlsDispatchQueue) { _, result, error in + if let error = error { + print("Trust failed: \(error.localizedDescription)") + } + 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 ) - - case .noHostnameVerification: - precondition(self.certificateVerification != .noHostnameVerification, - "TLSConfiguration.certificateVerification = .noHostnameVerification is not supported. \(useMTELGExplainer)") - - case .fullVerification: - break } - return options } } diff --git a/Sources/AsyncHTTPClient/Utils.swift b/Sources/AsyncHTTPClient/Utils.swift index 1af7899b2..6069222b1 100644 --- a/Sources/AsyncHTTPClient/Utils.swift +++ b/Sources/AsyncHTTPClient/Utils.swift @@ -180,9 +180,11 @@ extension NIOClientTCPBootstrap { // if eventLoop is compatible with NIOTransportServices create a NIOTSConnectionBootstrap if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *), let tsBootstrap = NIOTSConnectionBootstrap(validatingGroup: eventLoop) { // create NIOClientTCPBootstrap with NIOTS TLS provider - let parameters = tlsConfiguration.getNWProtocolTLSOptions() - let tlsProvider = NIOTSClientTLSProvider(tlsOptions: parameters) - return eventLoop.makeSucceededFuture(NIOClientTCPBootstrap(tsBootstrap, tls: tlsProvider)) + return tlsConfiguration.getNWProtocolTLSOptions(on: eventLoop) + .map { parameters in + let tlsProvider = NIOTSClientTLSProvider(tlsOptions: parameters) + return NIOClientTCPBootstrap(tsBootstrap, tls: tlsProvider) + } } #endif diff --git a/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests+XCTest.swift index c0d86085f..cc33f6aee 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests+XCTest.swift @@ -29,6 +29,7 @@ extension HTTPClientNIOTSTests { ("testTLSFailError", testTLSFailError), ("testConnectionFailError", testConnectionFailError), ("testTLSVersionError", testTLSVersionError), + ("testTrustRootCertificateLoadFail", testTrustRootCertificateLoadFail), ] } } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift index a8d2088d7..9a0070229 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientNIOTSTests.swift @@ -111,4 +111,19 @@ class HTTPClientNIOTSTests: XCTestCase { } #endif } + + func testTrustRootCertificateLoadFail() { + guard isTestingNIOTS() else { return } + #if canImport(Network) + let tlsConfig = TLSConfiguration.forClient(trustRoots: .file("not/a/certificate")) + XCTAssertThrowsError(try tlsConfig.getNWProtocolTLSOptions()) { error in + switch error { + case let error as NIOSSL.NIOSSLError where error == .failedToLoadCertificate: + break + default: + XCTFail("\(error)") + } + } + #endif + } }