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:
Adam Fowler
2021-05-13 13:59:18 +01:00
committed by GitHub
parent 06daedfbbd
commit 9cdf8a01e5
5 changed files with 96 additions and 24 deletions
+2
View File
@@ -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
}
}
+5 -3
View File
@@ -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
}
}