mirror of
https://github.com/swift-server/async-http-client.git
synced 2026-05-03 07:32:29 +00:00
Generate trust roots SecCertificate for Transport Services (#350)
This PR is a result of another #321. In that PR I provided an alternative structure to TLSConfiguration for when connecting with Transport Services. In this one I construct the NWProtocolTLS.Options from TLSConfiguration. It does mean a little more work for whenever we make a connection, but having spoken to @weissi he doesn't seem to think that is an issue. Also there is no method to create a SecIdentity at the moment. We need to generate a pkcs#12 from the certificate chain and private key, which can then be used to create the SecIdentity. This should resolve #292
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.build
|
||||
.git
|
||||
@@ -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<NWProtocolTLS.Options> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
switch self.certificateVerification {
|
||||
case .none:
|
||||
// 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
|
||||
)
|
||||
|
||||
case .noHostnameVerification:
|
||||
precondition(self.certificateVerification != .noHostnameVerification,
|
||||
"TLSConfiguration.certificateVerification = .noHostnameVerification is not supported. \(useMTELGExplainer)")
|
||||
|
||||
case .fullVerification:
|
||||
case .some(.default), .none:
|
||||
break
|
||||
}
|
||||
|
||||
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_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
|
||||
)
|
||||
}
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ extension HTTPClientNIOTSTests {
|
||||
("testTLSFailError", testTLSFailError),
|
||||
("testConnectionFailError", testConnectionFailError),
|
||||
("testTLSVersionError", testTLSVersionError),
|
||||
("testTrustRootCertificateLoadFail", testTrustRootCertificateLoadFail),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user