From 3cb206964bc7d025168a44a5c56e374cbd3515e9 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 30 Sep 2024 11:19:06 +0100 Subject: [PATCH 1/6] Add extra initialisers to TLS config --- Package.swift | 7 ++ .../TLSConfig.swift | 77 +++++++++++++++++-- .../TLSConfig.swift | 17 +++- .../HTTP2TransportNIOPosixTests.swift | 8 +- 4 files changed, 93 insertions(+), 16 deletions(-) diff --git a/Package.swift b/Package.swift index 145e2c0..e22ecd2 100644 --- a/Package.swift +++ b/Package.swift @@ -57,6 +57,10 @@ let dependencies: [Package.Dependency] = [ url: "https://github.com/apple/swift-nio-extras.git", from: "1.4.0" ), + .package( + url: "https://github.com/apple/swift-certificates.git", + from: "1.5.0" + ), ] let defaultSwiftSettings: [SwiftSetting] = [ @@ -104,6 +108,7 @@ let targets: [Target] = [ .target(name: "GRPCNIOTransportCore"), .product(name: "GRPCCore", package: "grpc-swift"), .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), ], swiftSettings: defaultSwiftSettings ), @@ -132,6 +137,8 @@ let targets: [Target] = [ name: "GRPCNIOTransportHTTP2Tests", dependencies: [ .target(name: "GRPCNIOTransportHTTP2"), + .product(name: "X509", package: "swift-certificates"), + .product(name: "NIOSSL", package: "swift-nio-ssl"), ] ) ] diff --git a/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift b/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift index 0defef5..29a125e 100644 --- a/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift +++ b/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift @@ -171,6 +171,27 @@ extension HTTP2ServerTransport.Posix.Config { /// /// If this is set to `true` but the client does not support ALPN, then the connection will be rejected. public var requireALPN: Bool + + /// Create a new HTTP2 NIO Posix server transport TLS config. + /// - Parameters: + /// - certificateChain: The certificates the server will offer during negotiation. + /// - privateKey: The private key associated with the leaf certificate. + /// - clientCertificateVerification: How to verify the client certificate, if one is presented. + /// - trustRoots: The trust roots to be used when verifying client certificates. + /// - requireALPN: Whether ALPN is required. + public init( + certificateChain: [TLSConfig.CertificateSource], + privateKey: TLSConfig.PrivateKeySource, + clientCertificateVerification: TLSConfig.CertificateVerification, + trustRoots: TLSConfig.TrustRootsSource, + requireALPN: Bool + ) { + self.certificateChain = certificateChain + self.privateKey = privateKey + self.clientCertificateVerification = clientCertificateVerification + self.trustRoots = trustRoots + self.requireALPN = requireALPN + } /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted: /// - `clientCertificateVerificationMode` equals `doNotVerify` @@ -180,18 +201,22 @@ extension HTTP2ServerTransport.Posix.Config { /// - Parameters: /// - certificateChain: The certificates the server will offer during negotiation. /// - privateKey: The private key associated with the leaf certificate. + /// - configure: A closure which allows you to modify the defaults before returning them. /// - Returns: A new HTTP2 NIO Posix transport TLS config. public static func defaults( certificateChain: [TLSConfig.CertificateSource], - privateKey: TLSConfig.PrivateKeySource + privateKey: TLSConfig.PrivateKeySource, + configure: (_ config: inout Self) -> Void = { _ in } ) -> Self { - Self( + var config = Self( certificateChain: certificateChain, privateKey: privateKey, clientCertificateVerification: .noVerification, trustRoots: .systemDefault, requireALPN: false ) + configure(&config) + return config } /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match @@ -203,18 +228,22 @@ extension HTTP2ServerTransport.Posix.Config { /// - Parameters: /// - certificateChain: The certificates the server will offer during negotiation. /// - privateKey: The private key associated with the leaf certificate. + /// - configure: A closure which allows you to modify the defaults before returning them. /// - Returns: A new HTTP2 NIO Posix transport TLS config. public static func mTLS( certificateChain: [TLSConfig.CertificateSource], - privateKey: TLSConfig.PrivateKeySource + privateKey: TLSConfig.PrivateKeySource, + configure: (_ config: inout Self) -> Void = { _ in } ) -> Self { - Self( + var config = Self( certificateChain: certificateChain, privateKey: privateKey, clientCertificateVerification: .noHostnameVerification, trustRoots: .systemDefault, requireALPN: false ) + configure(&config) + return config } } } @@ -255,6 +284,27 @@ extension HTTP2ClientTransport.Posix.Config { /// An optional server hostname to use when verifying certificates. public var serverHostname: String? + + /// Create a new HTTP2 NIO Posix client transport TLS config. + /// - Parameters: + /// - certificateChain: The certificates the client will offer during negotiation. + /// - privateKey: The private key associated with the leaf certificate. + /// - serverCertificateVerification: How to verify the server certificate, if one is presented. + /// - trustRoots: The trust roots to be used when verifying server certificates. + /// - serverHostname: An optional server hostname to use when verifying certificates. + public init( + certificateChain: [TLSConfig.CertificateSource], + privateKey: TLSConfig.PrivateKeySource? = nil, + serverCertificateVerification: TLSConfig.CertificateVerification, + trustRoots: TLSConfig.TrustRootsSource, + serverHostname: String? = nil + ) { + self.certificateChain = certificateChain + self.privateKey = privateKey + self.serverCertificateVerification = serverCertificateVerification + self.trustRoots = trustRoots + self.serverHostname = serverHostname + } /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted: /// - `certificateChain` equals `[]` @@ -263,35 +313,46 @@ extension HTTP2ClientTransport.Posix.Config { /// - `trustRoots` equals `systemDefault` /// - `serverHostname` equals `nil` /// + /// - Parameters: + /// - configure: A closure which allows you to modify the defaults before returning them. /// - Returns: A new HTTP2 NIO Posix transport TLS config. - public static var defaults: Self { - Self( + public static func defaults( + configure: (_ config: inout Self) -> Void = { _ in } + ) -> Self { + var config = Self( certificateChain: [], privateKey: nil, serverCertificateVerification: .fullVerification, trustRoots: .systemDefault, serverHostname: nil ) + configure(&config) + return config } /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match /// the requirements of mTLS: /// - `trustRoots` equals `systemDefault` + /// - `serverCertificateVerification` equals `fullVerification` /// /// - Parameters: /// - certificateChain: The certificates the client will offer during negotiation. /// - privateKey: The private key associated with the leaf certificate. + /// - configure: A closure which allows you to modify the defaults before returning them. /// - Returns: A new HTTP2 NIO Posix transport TLS config. public static func mTLS( certificateChain: [TLSConfig.CertificateSource], - privateKey: TLSConfig.PrivateKeySource + privateKey: TLSConfig.PrivateKeySource, + configure: (_ config: inout Self) -> Void = { _ in } ) -> Self { - Self( + var config = Self( certificateChain: certificateChain, privateKey: privateKey, serverCertificateVerification: .fullVerification, trustRoots: .systemDefault ) + configure(&config) + return config } } } diff --git a/Sources/GRPCNIOTransportHTTP2TransportServices/TLSConfig.swift b/Sources/GRPCNIOTransportHTTP2TransportServices/TLSConfig.swift index baa8b20..1784748 100644 --- a/Sources/GRPCNIOTransportHTTP2TransportServices/TLSConfig.swift +++ b/Sources/GRPCNIOTransportHTTP2TransportServices/TLSConfig.swift @@ -45,6 +45,18 @@ extension HTTP2ServerTransport.TransportServices.Config { /// If this is set to `true` but the client does not support ALPN, then the connection will be rejected. public var requireALPN: Bool + /// Create a new HTTP2 NIO Transport Services transport TLS config. + /// - Parameters: + /// - requireALPN: Whether ALPN is required. + /// - identityProvider: A provider for the `SecIdentity` to be used when setting up TLS. + public init( + requireALPN: Bool, + identityProvider: @Sendable @escaping () throws -> SecIdentity + ) { + self.requireALPN = requireALPN + self.identityProvider = identityProvider + } + /// Create a new HTTP2 NIO Transport Services transport TLS config, with some values defaulted: /// - `requireALPN` equals `false` /// @@ -52,10 +64,7 @@ extension HTTP2ServerTransport.TransportServices.Config { public static func defaults( identityProvider: @Sendable @escaping () throws -> SecIdentity ) -> Self { - Self( - identityProvider: identityProvider, - requireALPN: false - ) + Self(requireALPN: false, identityProvider: identityProvider) } } } diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOPosixTests.swift index 79e1439..d094151 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOPosixTests.swift @@ -410,7 +410,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } func testClientTLSConfig_Defaults() throws { - let grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults + let grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults() let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) XCTAssertEqual(nioSSLTLSConfig.certificateChain, []) @@ -422,7 +422,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } func testClientTLSConfig_CustomCertificateChainAndPrivateKey() throws { - var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults + var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults() grpcTLSConfig.certificateChain = [ .bytes(Array(Self.samplePemCert.utf8), format: .pem) ] @@ -451,7 +451,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } func testClientTLSConfig_CustomTrustRoots() throws { - var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults + var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults() grpcTLSConfig.trustRoots = .certificates([.bytes(Array(Self.samplePemCert.utf8), format: .pem)]) let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) @@ -467,7 +467,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } func testClientTLSConfig_CustomCertificateVerification() throws { - var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults + var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults() grpcTLSConfig.serverCertificateVerification = .noHostnameVerification let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) From a3a7cc7068cba899350e3a8e08a41271a8e497d5 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 8 Oct 2024 13:42:51 +0100 Subject: [PATCH 2/6] Add E2E TLS tests --- .../HTTP2TransportTLSEnabledTests.swift | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift new file mode 100644 index 0000000..8480d54 --- /dev/null +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift @@ -0,0 +1,343 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import GRPCNIOTransportHTTP2Posix +import Testing +import X509 +import Crypto +import SwiftASN1 +import Foundation +import NIOSSL + +@Suite("HTTP/2 transport E2E tests with TLS enabled") +struct HTTP2TransportTLSEnabledTests { + // - MARK: Test Utilities + + // A combination of client and server transport kinds. + struct Transport: Sendable { + var server: ServerKind + var client: ClientKind + + enum ClientKind: Sendable { + case posix(HTTP2ClientTransport.Posix.Config.TransportSecurity) + } + + enum ServerKind: Sendable { + case posix(HTTP2ServerTransport.Posix.Config.TransportSecurity) + } + } + + func executeUnaryRPCForEachTransportPair( + transportProvider: (TestSecurity) -> [Transport] + ) async throws { + let security = try TestSecurity() + for pair in transportProvider(security) { + try await withThrowingTaskGroup(of: Void.self) { group in + let (server, address) = try await self.runServer( + in: &group, + kind: pair.server + ) + + let target: any ResolvableTarget + if let ipv4 = address.ipv4 { + target = .ipv4(host: ipv4.host, port: ipv4.port) + } else if let ipv6 = address.ipv6 { + target = .ipv6(host: ipv6.host, port: ipv6.port) + } else if let uds = address.unixDomainSocket { + target = .unixDomainSocket(path: uds.path) + } else { + Issue.record("Unexpected address to connect to") + return + } + + let client = try self.makeClient( + kind: pair.client, + target: target + ) + + group.addTask { + try await client.run() + } + + let control = ControlClient(wrapping: client) + try await self.executeUnaryRPC(control: control, pair: pair) + + server.beginGracefulShutdown() + client.beginGracefulShutdown() + } + } + } + + private func runServer( + in group: inout ThrowingTaskGroup, + kind: Transport.ServerKind + ) async throws -> (GRPCServer, GRPCNIOTransportHTTP2Posix.SocketAddress) { + let services = [ControlService()] + + switch kind { + case .posix(let transportSecurity): + let server = GRPCServer( + transport: .http2NIOPosix( + address: .ipv4(host: "127.0.0.1", port: 0), + config: .defaults(transportSecurity: transportSecurity) + ), + services: services + ) + + group.addTask { + try await server.serve() + } + + let address = try await server.listeningAddress! + return (server, address) + } + } + + private func makeClient( + kind: Transport.ClientKind, + target: any ResolvableTarget + ) throws -> GRPCClient { + let transport: any ClientTransport + + switch kind { + case .posix(let transportSecurity): + var serviceConfig = ServiceConfig() + serviceConfig.loadBalancingConfig = [.roundRobin] + transport = try HTTP2ClientTransport.Posix( + target: target, + config: .defaults(transportSecurity: transportSecurity) { config in + config.backoff.initial = .milliseconds(100) + config.backoff.multiplier = 1 + config.backoff.jitter = 0 + }, + serviceConfig: serviceConfig + ) + } + + return GRPCClient(transport: transport) + } + + private func executeUnaryRPC(control: ControlClient, pair: Transport) async throws { + let input = ControlInput.with { + $0.echoMetadataInHeaders = true + $0.echoMetadataInTrailers = true + $0.numberOfMessages = 1 + $0.payloadParameters = .with { + $0.content = 0 + $0.size = 1024 + } + } + + let metadata: Metadata = ["test-key": "test-value"] + let request = ClientRequest(message: input, metadata: metadata) + + try await control.unary(request: request) { response in + let message = try response.message + #expect(message.payload == Data(repeating: 0, count: 1024)) + + let initial = response.metadata + #expect(Array(initial["echo-test-key"]) == ["test-value"]) + + let trailing = response.trailingMetadata + #expect(Array(trailing["echo-test-key"]) == ["test-value"]) + } + } + + // - MARK: Tests + + @Test("When using defaults, server does not perform client verification") + func testRPC_Defaults_OK() async throws { + try await self.executeUnaryRPCForEachTransportPair { security in + [ + HTTP2TransportTLSEnabledTests.Transport( + server: .posix(.tls(.defaults( + certificateChain: [.bytes(security.server.certificate, format: .der)], + privateKey: .bytes(security.server.key, format: .der) + ))), + client: .posix(.tls(.defaults { + $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) + $0.serverHostname = "localhost" + })) + ) + ] + } + } + + @Test("When using mTLS defaults, both client and server verify each others' certificates") + func testRPC_mTLS_OK() async throws { + try await self.executeUnaryRPCForEachTransportPair { security in + [ + HTTP2TransportTLSEnabledTests.Transport( + server: .posix(.tls(.mTLS( + certificateChain: [.bytes(security.server.certificate, format: .der)], + privateKey: .bytes(security.server.key, format: .der)) { + $0.trustRoots = .certificates([.bytes(security.client.certificate, format: .der)]) + })), + client: .posix(.tls(.mTLS( + certificateChain: [.bytes(security.client.certificate, format: .der)], + privateKey: .bytes(security.client.key, format: .der)) { + $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) + $0.serverHostname = "localhost" + })) + ) + ] + } + } + + @Test("Error is surfaced when client fails server verification") + // Verification should fail because the custom hostname is missing on the client. + func testClientFailsServerValidation() async throws { + await #expect(performing: { + try await self.executeUnaryRPCForEachTransportPair { security in + [ + HTTP2TransportTLSEnabledTests.Transport( + server: .posix(.tls(.mTLS( + certificateChain: [.bytes(security.server.certificate, format: .der)], + privateKey: .bytes(security.server.key, format: .der)) { + $0.trustRoots = .certificates([.bytes(security.client.certificate, format: .der)]) + })), + client: .posix(.tls(.mTLS( + certificateChain: [.bytes(security.client.certificate, format: .der)], + privateKey: .bytes(security.client.key, format: .der)) { + $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) + })) + ) + ] + } + }, throws: { error in + guard let rootError = error as? RPCError else { + Issue.record("Should be an RPC error") + return false + } + #expect(rootError.code == .unavailable) + #expect(rootError.message == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface.") + + guard + let sslError = rootError.cause as? NIOSSLExtraError, + case .failedToValidateHostname = sslError + else { + Issue.record("Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))") + return false + } + + return true + }) + } + + @Test("Error is surfaced when server fails client verification") + // Verification should fail because the server does not have trust roots containing the client cert. + func testServerFailsClientValidation() async throws { + await #expect(performing: { + try await self.executeUnaryRPCForEachTransportPair { security in + [ + HTTP2TransportTLSEnabledTests.Transport( + server: .posix(.tls(.mTLS( + certificateChain: [.bytes(security.server.certificate, format: .der)], + privateKey: .bytes(security.server.key, format: .der) + ))), + client: .posix(.tls(.mTLS( + certificateChain: [.bytes(security.client.certificate, format: .der)], + privateKey: .bytes(security.client.key, format: .der)) { + $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) + $0.serverHostname = "localhost" + })) + ) + ] + } + }, throws: { error in + guard let rootError = error as? RPCError else { + Issue.record("Should be an RPC error") + return false + } + #expect(rootError.code == .unavailable) + #expect(rootError.message == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface.") + + guard + let sslError = rootError.cause as? NIOSSL.BoringSSLError, + case .sslError = sslError + else { + Issue.record("Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))") + return false + } + + return true + }) + } +} + +struct TestSecurity { + struct Server { + let certificate: [UInt8] + let key: [UInt8] + } + + struct Client { + let certificate: [UInt8] + let key: [UInt8] + } + + let server: Server + let client: Client + + init() throws { + let server = try Self.createSelfSignedDERCertificateAndPrivateKey(name: "Server Certificate") + let client = try Self.createSelfSignedDERCertificateAndPrivateKey(name: "Client Certificate") + + self.server = Server(certificate: server.cert, key: server.key) + self.client = Client(certificate: client.cert, key: client.key) + } + + private static func createSelfSignedDERCertificateAndPrivateKey( + name: String + ) throws -> (cert: [UInt8], key: [UInt8]) { + let swiftCryptoKey = P256.Signing.PrivateKey() + let key = Certificate.PrivateKey(swiftCryptoKey) + let subjectName = try DistinguishedName { CommonName(name) } + let issuerName = subjectName + let now = Date() + let extensions = try Certificate.Extensions { + Critical( + BasicConstraints.isCertificateAuthority(maxPathLength: nil) + ) + Critical( + KeyUsage(digitalSignature: true, keyCertSign: true) + ) + Critical( + try ExtendedKeyUsage([.serverAuth, .clientAuth]) + ) + SubjectAlternativeNames([.dnsName("localhost")]) + } + let certificate = try Certificate( + version: .v3, + serialNumber: Certificate.SerialNumber(), + publicKey: key.publicKey, + notValidBefore: now.addingTimeInterval(-60 * 60), + notValidAfter: now.addingTimeInterval(60 * 60 * 24 * 365), + issuer: issuerName, + subject: subjectName, + signatureAlgorithm: .ecdsaWithSHA256, + extensions: extensions, + issuerPrivateKey: key + ) + + var serializer = DER.Serializer() + try serializer.serialize(certificate) + + let certBytes = serializer.serializedBytes + let keyBytes = try key.serializeAsPEM().derBytes + return (certBytes, keyBytes) + } +} From 522b419e2bd312066d5cb6ca128f5d50fa640f63 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 8 Oct 2024 22:52:12 +0000 Subject: [PATCH 3/6] Formatting --- .../TLSConfig.swift | 4 +- .../HTTP2TransportTLSEnabledTests.swift | 237 +++++++++++------- 2 files changed, 150 insertions(+), 91 deletions(-) diff --git a/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift b/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift index 29a125e..1e7e22b 100644 --- a/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift +++ b/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift @@ -171,7 +171,7 @@ extension HTTP2ServerTransport.Posix.Config { /// /// If this is set to `true` but the client does not support ALPN, then the connection will be rejected. public var requireALPN: Bool - + /// Create a new HTTP2 NIO Posix server transport TLS config. /// - Parameters: /// - certificateChain: The certificates the server will offer during negotiation. @@ -284,7 +284,7 @@ extension HTTP2ClientTransport.Posix.Config { /// An optional server hostname to use when verifying certificates. public var serverHostname: String? - + /// Create a new HTTP2 NIO Posix client transport TLS config. /// - Parameters: /// - certificateChain: The certificates the client will offer during negotiation. diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift index 8480d54..3eda5ce 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift @@ -14,13 +14,13 @@ * limitations under the License. */ -import GRPCNIOTransportHTTP2Posix -import Testing -import X509 import Crypto -import SwiftASN1 import Foundation +import GRPCNIOTransportHTTP2Posix import NIOSSL +import SwiftASN1 +import Testing +import X509 @Suite("HTTP/2 transport E2E tests with TLS enabled") struct HTTP2TransportTLSEnabledTests { @@ -163,14 +163,22 @@ struct HTTP2TransportTLSEnabledTests { try await self.executeUnaryRPCForEachTransportPair { security in [ HTTP2TransportTLSEnabledTests.Transport( - server: .posix(.tls(.defaults( - certificateChain: [.bytes(security.server.certificate, format: .der)], - privateKey: .bytes(security.server.key, format: .der) - ))), - client: .posix(.tls(.defaults { - $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) - $0.serverHostname = "localhost" - })) + server: .posix( + .tls( + .defaults( + certificateChain: [.bytes(security.server.certificate, format: .der)], + privateKey: .bytes(security.server.key, format: .der) + ) + ) + ), + client: .posix( + .tls( + .defaults { + $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) + $0.serverHostname = "localhost" + } + ) + ) ) ] } @@ -181,17 +189,27 @@ struct HTTP2TransportTLSEnabledTests { try await self.executeUnaryRPCForEachTransportPair { security in [ HTTP2TransportTLSEnabledTests.Transport( - server: .posix(.tls(.mTLS( - certificateChain: [.bytes(security.server.certificate, format: .der)], - privateKey: .bytes(security.server.key, format: .der)) { - $0.trustRoots = .certificates([.bytes(security.client.certificate, format: .der)]) - })), - client: .posix(.tls(.mTLS( - certificateChain: [.bytes(security.client.certificate, format: .der)], - privateKey: .bytes(security.client.key, format: .der)) { - $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) - $0.serverHostname = "localhost" - })) + server: .posix( + .tls( + .mTLS( + certificateChain: [.bytes(security.server.certificate, format: .der)], + privateKey: .bytes(security.server.key, format: .der) + ) { + $0.trustRoots = .certificates([.bytes(security.client.certificate, format: .der)]) + } + ) + ), + client: .posix( + .tls( + .mTLS( + certificateChain: [.bytes(security.client.certificate, format: .der)], + privateKey: .bytes(security.client.key, format: .der) + ) { + $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) + $0.serverHostname = "localhost" + } + ) + ) ) ] } @@ -200,81 +218,122 @@ struct HTTP2TransportTLSEnabledTests { @Test("Error is surfaced when client fails server verification") // Verification should fail because the custom hostname is missing on the client. func testClientFailsServerValidation() async throws { - await #expect(performing: { - try await self.executeUnaryRPCForEachTransportPair { security in - [ - HTTP2TransportTLSEnabledTests.Transport( - server: .posix(.tls(.mTLS( - certificateChain: [.bytes(security.server.certificate, format: .der)], - privateKey: .bytes(security.server.key, format: .der)) { - $0.trustRoots = .certificates([.bytes(security.client.certificate, format: .der)]) - })), - client: .posix(.tls(.mTLS( - certificateChain: [.bytes(security.client.certificate, format: .der)], - privateKey: .bytes(security.client.key, format: .der)) { - $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) - })) + await #expect( + performing: { + try await self.executeUnaryRPCForEachTransportPair { security in + [ + HTTP2TransportTLSEnabledTests.Transport( + server: .posix( + .tls( + .mTLS( + certificateChain: [.bytes(security.server.certificate, format: .der)], + privateKey: .bytes(security.server.key, format: .der) + ) { + $0.trustRoots = .certificates([ + .bytes(security.client.certificate, format: .der) + ]) + } + ) + ), + client: .posix( + .tls( + .mTLS( + certificateChain: [.bytes(security.client.certificate, format: .der)], + privateKey: .bytes(security.client.key, format: .der) + ) { + $0.trustRoots = .certificates([ + .bytes(security.server.certificate, format: .der) + ]) + } + ) + ) + ) + ] + } + }, + throws: { error in + guard let rootError = error as? RPCError else { + Issue.record("Should be an RPC error") + return false + } + #expect(rootError.code == .unavailable) + #expect( + rootError.message + == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." + ) + + guard + let sslError = rootError.cause as? NIOSSLExtraError, + case .failedToValidateHostname = sslError + else { + Issue.record( + "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))" ) - ] - } - }, throws: { error in - guard let rootError = error as? RPCError else { - Issue.record("Should be an RPC error") - return false - } - #expect(rootError.code == .unavailable) - #expect(rootError.message == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface.") - - guard - let sslError = rootError.cause as? NIOSSLExtraError, - case .failedToValidateHostname = sslError - else { - Issue.record("Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))") - return false - } + return false + } - return true - }) + return true + } + ) } @Test("Error is surfaced when server fails client verification") // Verification should fail because the server does not have trust roots containing the client cert. func testServerFailsClientValidation() async throws { - await #expect(performing: { - try await self.executeUnaryRPCForEachTransportPair { security in - [ - HTTP2TransportTLSEnabledTests.Transport( - server: .posix(.tls(.mTLS( - certificateChain: [.bytes(security.server.certificate, format: .der)], - privateKey: .bytes(security.server.key, format: .der) - ))), - client: .posix(.tls(.mTLS( - certificateChain: [.bytes(security.client.certificate, format: .der)], - privateKey: .bytes(security.client.key, format: .der)) { - $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) - $0.serverHostname = "localhost" - })) + await #expect( + performing: { + try await self.executeUnaryRPCForEachTransportPair { security in + [ + HTTP2TransportTLSEnabledTests.Transport( + server: .posix( + .tls( + .mTLS( + certificateChain: [.bytes(security.server.certificate, format: .der)], + privateKey: .bytes(security.server.key, format: .der) + ) + ) + ), + client: .posix( + .tls( + .mTLS( + certificateChain: [.bytes(security.client.certificate, format: .der)], + privateKey: .bytes(security.client.key, format: .der) + ) { + $0.trustRoots = .certificates([ + .bytes(security.server.certificate, format: .der) + ]) + $0.serverHostname = "localhost" + } + ) + ) + ) + ] + } + }, + throws: { error in + guard let rootError = error as? RPCError else { + Issue.record("Should be an RPC error") + return false + } + #expect(rootError.code == .unavailable) + #expect( + rootError.message + == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." + ) + + guard + let sslError = rootError.cause as? NIOSSL.BoringSSLError, + case .sslError = sslError + else { + Issue.record( + "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))" ) - ] - } - }, throws: { error in - guard let rootError = error as? RPCError else { - Issue.record("Should be an RPC error") - return false - } - #expect(rootError.code == .unavailable) - #expect(rootError.message == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface.") - - guard - let sslError = rootError.cause as? NIOSSL.BoringSSLError, - case .sslError = sslError - else { - Issue.record("Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))") - return false - } + return false + } - return true - }) + return true + } + ) } } From 4d300a0d08960317e0ebe699ba72eac4d09952b1 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Wed, 9 Oct 2024 14:58:35 +0000 Subject: [PATCH 4/6] Fix tests --- .../HTTP2TransportTLSEnabledTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift index 3eda5ce..0f6b0e0 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift @@ -259,7 +259,7 @@ struct HTTP2TransportTLSEnabledTests { #expect(rootError.code == .unavailable) #expect( rootError.message - == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." + == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." ) guard @@ -318,7 +318,7 @@ struct HTTP2TransportTLSEnabledTests { #expect(rootError.code == .unavailable) #expect( rootError.message - == "Channel isn't ready. The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." + == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." ) guard From 70dd65ebeb277c326f629e317f3ae5a3b0107fd6 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 14 Oct 2024 14:16:58 +0000 Subject: [PATCH 5/6] PR changes --- .../TLSConfig.swift | 17 +- .../HTTP2TransportNIOPosixTests.swift | 8 +- .../HTTP2TransportTLSEnabledTests.swift | 574 ++++++++++-------- 3 files changed, 326 insertions(+), 273 deletions(-) diff --git a/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift b/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift index 1e7e22b..1cbf291 100644 --- a/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift +++ b/Sources/GRPCNIOTransportHTTP2Posix/TLSConfig.swift @@ -294,10 +294,10 @@ extension HTTP2ClientTransport.Posix.Config { /// - serverHostname: An optional server hostname to use when verifying certificates. public init( certificateChain: [TLSConfig.CertificateSource], - privateKey: TLSConfig.PrivateKeySource? = nil, + privateKey: TLSConfig.PrivateKeySource?, serverCertificateVerification: TLSConfig.CertificateVerification, trustRoots: TLSConfig.TrustRootsSource, - serverHostname: String? = nil + serverHostname: String? ) { self.certificateChain = certificateChain self.privateKey = privateKey @@ -330,6 +330,16 @@ extension HTTP2ClientTransport.Posix.Config { return config } + /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted: + /// - `certificateChain` equals `[]` + /// - `privateKey` equals `nil` + /// - `serverCertificateVerification` equals `fullVerification` + /// - `trustRoots` equals `systemDefault` + /// - `serverHostname` equals `nil` + public static var defaults: Self { + Self.defaults() + } + /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match /// the requirements of mTLS: /// - `trustRoots` equals `systemDefault` @@ -349,7 +359,8 @@ extension HTTP2ClientTransport.Posix.Config { certificateChain: certificateChain, privateKey: privateKey, serverCertificateVerification: .fullVerification, - trustRoots: .systemDefault + trustRoots: .systemDefault, + serverHostname: nil ) configure(&config) return config diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOPosixTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOPosixTests.swift index d094151..79e1439 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOPosixTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportNIOPosixTests.swift @@ -410,7 +410,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } func testClientTLSConfig_Defaults() throws { - let grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults() + let grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) XCTAssertEqual(nioSSLTLSConfig.certificateChain, []) @@ -422,7 +422,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } func testClientTLSConfig_CustomCertificateChainAndPrivateKey() throws { - var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults() + var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults grpcTLSConfig.certificateChain = [ .bytes(Array(Self.samplePemCert.utf8), format: .pem) ] @@ -451,7 +451,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } func testClientTLSConfig_CustomTrustRoots() throws { - var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults() + var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults grpcTLSConfig.trustRoots = .certificates([.bytes(Array(Self.samplePemCert.utf8), format: .pem)]) let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) @@ -467,7 +467,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase { } func testClientTLSConfig_CustomCertificateVerification() throws { - var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults() + var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults grpcTLSConfig.serverCertificateVerification = .noHostnameVerification let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig) diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift index 0f6b0e0..5d4a8ea 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift @@ -24,67 +24,319 @@ import X509 @Suite("HTTP/2 transport E2E tests with TLS enabled") struct HTTP2TransportTLSEnabledTests { + // - MARK: Tests + + @Test( + "When using defaults, server does not perform client verification", + arguments: [TransportSecurity.posix], + [TransportSecurity.posix] + ) + func testRPC_Defaults_OK( + clientTransport: TransportSecurity, + serverTransport: TransportSecurity + ) async throws { + let certificateKeyPairs = try SelfSignedCertificateKeyPairs() + let clientTransportConfig = self.makeDefaultClientTLSConfig( + for: clientTransport, + certificateKeyPairs: certificateKeyPairs + ) + let serverTransportConfig = self.makeDefaultServerTLSConfig( + for: serverTransport, + certificateKeyPairs: certificateKeyPairs + ) + + try await self.withClientAndServer( + clientTransportSecurity: clientTransportConfig, + serverTransportSecurity: serverTransportConfig + ) { control in + await #expect( + throws: Never.self, + performing: { + try await self.executeUnaryRPC(control: control) + } + ) + } + } + + @Test( + "When using mTLS defaults, both client and server verify each others' certificates", + arguments: [TransportSecurity.posix], + [TransportSecurity.posix] + ) + func testRPC_mTLS_OK( + clientTransport: TransportSecurity, + serverTransport: TransportSecurity + ) async throws { + let certificateKeyPairs = try SelfSignedCertificateKeyPairs() + let clientTransportConfig = self.makeMTLSClientTLSConfig( + for: clientTransport, + certificateKeyPairs: certificateKeyPairs, + serverHostname: "localhost" + ) + let serverTransportConfig = self.makeMTLSServerTLSConfig( + for: serverTransport, + certificateKeyPairs: certificateKeyPairs, + includeClientCertificateInTrustRoots: true + ) + + try await self.withClientAndServer( + clientTransportSecurity: clientTransportConfig, + serverTransportSecurity: serverTransportConfig + ) { control in + await #expect( + throws: Never.self, + performing: { + try await self.executeUnaryRPC(control: control) + } + ) + } + } + + @Test( + "Error is surfaced when client fails server verification", + arguments: [TransportSecurity.posix], + [TransportSecurity.posix] + ) + // Verification should fail because the custom hostname is missing on the client. + func testClientFailsServerValidation( + clientTransport: TransportSecurity, + serverTransport: TransportSecurity + ) async throws { + let certificateKeyPairs = try SelfSignedCertificateKeyPairs() + let clientTransportConfig = self.makeMTLSClientTLSConfig( + for: clientTransport, + certificateKeyPairs: certificateKeyPairs, + serverHostname: nil + ) + let serverTransportConfig = self.makeMTLSServerTLSConfig( + for: serverTransport, + certificateKeyPairs: certificateKeyPairs, + includeClientCertificateInTrustRoots: true + ) + + try await self.withClientAndServer( + clientTransportSecurity: clientTransportConfig, + serverTransportSecurity: serverTransportConfig + ) { control in + await #expect( + performing: { + try await self.executeUnaryRPC(control: control) + }, + throws: { error in + guard let rootError = error as? RPCError else { + Issue.record("Should be an RPC error") + return false + } + #expect(rootError.code == .unavailable) + #expect( + rootError.message + == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." + ) + + guard + let sslError = rootError.cause as? NIOSSLExtraError, + case .failedToValidateHostname = sslError + else { + Issue.record( + "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))" + ) + return false + } + + return true + } + ) + } + } + + @Test( + "Error is surfaced when server fails client verification", + arguments: [TransportSecurity.posix], + [TransportSecurity.posix] + ) + // Verification should fail because the server does not have trust roots containing the client cert. + func testServerFailsClientValidation( + clientTransport: TransportSecurity, + serverTransport: TransportSecurity + ) async throws { + let certificateKeyPairs = try SelfSignedCertificateKeyPairs() + let clientTransportConfig = self.makeMTLSClientTLSConfig( + for: clientTransport, + certificateKeyPairs: certificateKeyPairs, + serverHostname: "localhost" + ) + let serverTransportConfig = self.makeMTLSServerTLSConfig( + for: serverTransport, + certificateKeyPairs: certificateKeyPairs, + includeClientCertificateInTrustRoots: false + ) + + try await self.withClientAndServer( + clientTransportSecurity: clientTransportConfig, + serverTransportSecurity: serverTransportConfig + ) { control in + await #expect( + performing: { + try await self.executeUnaryRPC(control: control) + }, + throws: { error in + guard let rootError = error as? RPCError else { + Issue.record("Should be an RPC error") + return false + } + #expect(rootError.code == .unavailable) + #expect( + rootError.message + == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." + ) + + guard + let sslError = rootError.cause as? NIOSSL.BoringSSLError, + case .sslError = sslError + else { + Issue.record( + "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))" + ) + return false + } + + return true + } + ) + } + } + // - MARK: Test Utilities - // A combination of client and server transport kinds. - struct Transport: Sendable { - var server: ServerKind - var client: ClientKind + enum TransportSecurity: Sendable { + case posix + } - enum ClientKind: Sendable { + enum TLSConfig { + enum Client { case posix(HTTP2ClientTransport.Posix.Config.TransportSecurity) } - enum ServerKind: Sendable { + enum Server { case posix(HTTP2ServerTransport.Posix.Config.TransportSecurity) } } - func executeUnaryRPCForEachTransportPair( - transportProvider: (TestSecurity) -> [Transport] - ) async throws { - let security = try TestSecurity() - for pair in transportProvider(security) { - try await withThrowingTaskGroup(of: Void.self) { group in - let (server, address) = try await self.runServer( - in: &group, - kind: pair.server + func makeDefaultClientTLSConfig( + for transportSecurity: TransportSecurity, + certificateKeyPairs: SelfSignedCertificateKeyPairs + ) -> TLSConfig.Client { + switch transportSecurity { + case .posix: + return .posix( + .tls( + .defaults { + $0.trustRoots = .certificates([ + .bytes(certificateKeyPairs.server.certificate, format: .der) + ]) + $0.serverHostname = "localhost" + } ) + ) + } + } - let target: any ResolvableTarget - if let ipv4 = address.ipv4 { - target = .ipv4(host: ipv4.host, port: ipv4.port) - } else if let ipv6 = address.ipv6 { - target = .ipv6(host: ipv6.host, port: ipv6.port) - } else if let uds = address.unixDomainSocket { - target = .unixDomainSocket(path: uds.path) - } else { - Issue.record("Unexpected address to connect to") - return - } + func makeMTLSClientTLSConfig( + for transportSecurity: TransportSecurity, + certificateKeyPairs: SelfSignedCertificateKeyPairs, + serverHostname: String? + ) -> TLSConfig.Client { + switch transportSecurity { + case .posix: + return .posix( + .tls( + .mTLS( + certificateChain: [.bytes(certificateKeyPairs.client.certificate, format: .der)], + privateKey: .bytes(certificateKeyPairs.client.key, format: .der) + ) { + $0.trustRoots = .certificates([ + .bytes(certificateKeyPairs.server.certificate, format: .der) + ]) + $0.serverHostname = serverHostname + } + ) + ) + } + } - let client = try self.makeClient( - kind: pair.client, - target: target + func makeDefaultServerTLSConfig( + for transportSecurity: TransportSecurity, + certificateKeyPairs: SelfSignedCertificateKeyPairs + ) -> TLSConfig.Server { + switch transportSecurity { + case .posix: + return .posix( + .tls( + .defaults( + certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)], + privateKey: .bytes(certificateKeyPairs.server.key, format: .der) + ) ) + ) + } + } - group.addTask { - try await client.run() - } + func makeMTLSServerTLSConfig( + for transportSecurity: TransportSecurity, + certificateKeyPairs: SelfSignedCertificateKeyPairs, + includeClientCertificateInTrustRoots: Bool + ) -> TLSConfig.Server { + switch transportSecurity { + case .posix: + return .posix( + .tls( + .mTLS( + certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)], + privateKey: .bytes(certificateKeyPairs.server.key, format: .der) + ) { + if includeClientCertificateInTrustRoots { + $0.trustRoots = .certificates([ + .bytes(certificateKeyPairs.client.certificate, format: .der) + ]) + } + } + ) + ) + } + } - let control = ControlClient(wrapping: client) - try await self.executeUnaryRPC(control: control, pair: pair) + func withClientAndServer( + clientTransportSecurity: TLSConfig.Client, + serverTransportSecurity: TLSConfig.Server, + _ test: (ControlClient) async throws -> Void + ) async throws { + try await withThrowingDiscardingTaskGroup { group in + let server = self.makeServer(kind: serverTransportSecurity) - server.beginGracefulShutdown() - client.beginGracefulShutdown() + group.addTask { + try await server.serve() + } + + guard let address = try await server.listeningAddress?.ipv4 else { + Issue.record("Unexpected address to connect to") + return + } + let target: any ResolvableTarget = .ipv4(host: address.host, port: address.port) + let client = try self.makeClient(kind: clientTransportSecurity, target: target) + + group.addTask { + try await client.run() } + + let control = ControlClient(wrapping: client) + try await test(control) + + server.beginGracefulShutdown() + client.beginGracefulShutdown() } } - private func runServer( - in group: inout ThrowingTaskGroup, - kind: Transport.ServerKind - ) async throws -> (GRPCServer, GRPCNIOTransportHTTP2Posix.SocketAddress) { + private func makeServer(kind: TLSConfig.Server) -> GRPCServer { let services = [ControlService()] switch kind { @@ -97,25 +349,18 @@ struct HTTP2TransportTLSEnabledTests { services: services ) - group.addTask { - try await server.serve() - } - - let address = try await server.listeningAddress! - return (server, address) + return server } } private func makeClient( - kind: Transport.ClientKind, + kind: TLSConfig.Client, target: any ResolvableTarget ) throws -> GRPCClient { let transport: any ClientTransport switch kind { case .posix(let transportSecurity): - var serviceConfig = ServiceConfig() - serviceConfig.loadBalancingConfig = [.roundRobin] transport = try HTTP2ClientTransport.Posix( target: target, config: .defaults(transportSecurity: transportSecurity) { config in @@ -123,243 +368,40 @@ struct HTTP2TransportTLSEnabledTests { config.backoff.multiplier = 1 config.backoff.jitter = 0 }, - serviceConfig: serviceConfig + serviceConfig: ServiceConfig() ) } return GRPCClient(transport: transport) } - private func executeUnaryRPC(control: ControlClient, pair: Transport) async throws { - let input = ControlInput.with { - $0.echoMetadataInHeaders = true - $0.echoMetadataInTrailers = true - $0.numberOfMessages = 1 - $0.payloadParameters = .with { - $0.content = 0 - $0.size = 1024 - } - } - - let metadata: Metadata = ["test-key": "test-value"] - let request = ClientRequest(message: input, metadata: metadata) - + private func executeUnaryRPC(control: ControlClient) async throws { + let input = ControlInput.with { $0.numberOfMessages = 1 } + let request = ClientRequest(message: input) try await control.unary(request: request) { response in - let message = try response.message - #expect(message.payload == Data(repeating: 0, count: 1024)) - - let initial = response.metadata - #expect(Array(initial["echo-test-key"]) == ["test-value"]) - - let trailing = response.trailingMetadata - #expect(Array(trailing["echo-test-key"]) == ["test-value"]) + #expect(throws: Never.self) { try response.message } } } - - // - MARK: Tests - - @Test("When using defaults, server does not perform client verification") - func testRPC_Defaults_OK() async throws { - try await self.executeUnaryRPCForEachTransportPair { security in - [ - HTTP2TransportTLSEnabledTests.Transport( - server: .posix( - .tls( - .defaults( - certificateChain: [.bytes(security.server.certificate, format: .der)], - privateKey: .bytes(security.server.key, format: .der) - ) - ) - ), - client: .posix( - .tls( - .defaults { - $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) - $0.serverHostname = "localhost" - } - ) - ) - ) - ] - } - } - - @Test("When using mTLS defaults, both client and server verify each others' certificates") - func testRPC_mTLS_OK() async throws { - try await self.executeUnaryRPCForEachTransportPair { security in - [ - HTTP2TransportTLSEnabledTests.Transport( - server: .posix( - .tls( - .mTLS( - certificateChain: [.bytes(security.server.certificate, format: .der)], - privateKey: .bytes(security.server.key, format: .der) - ) { - $0.trustRoots = .certificates([.bytes(security.client.certificate, format: .der)]) - } - ) - ), - client: .posix( - .tls( - .mTLS( - certificateChain: [.bytes(security.client.certificate, format: .der)], - privateKey: .bytes(security.client.key, format: .der) - ) { - $0.trustRoots = .certificates([.bytes(security.server.certificate, format: .der)]) - $0.serverHostname = "localhost" - } - ) - ) - ) - ] - } - } - - @Test("Error is surfaced when client fails server verification") - // Verification should fail because the custom hostname is missing on the client. - func testClientFailsServerValidation() async throws { - await #expect( - performing: { - try await self.executeUnaryRPCForEachTransportPair { security in - [ - HTTP2TransportTLSEnabledTests.Transport( - server: .posix( - .tls( - .mTLS( - certificateChain: [.bytes(security.server.certificate, format: .der)], - privateKey: .bytes(security.server.key, format: .der) - ) { - $0.trustRoots = .certificates([ - .bytes(security.client.certificate, format: .der) - ]) - } - ) - ), - client: .posix( - .tls( - .mTLS( - certificateChain: [.bytes(security.client.certificate, format: .der)], - privateKey: .bytes(security.client.key, format: .der) - ) { - $0.trustRoots = .certificates([ - .bytes(security.server.certificate, format: .der) - ]) - } - ) - ) - ) - ] - } - }, - throws: { error in - guard let rootError = error as? RPCError else { - Issue.record("Should be an RPC error") - return false - } - #expect(rootError.code == .unavailable) - #expect( - rootError.message - == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." - ) - - guard - let sslError = rootError.cause as? NIOSSLExtraError, - case .failedToValidateHostname = sslError - else { - Issue.record( - "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))" - ) - return false - } - - return true - } - ) - } - - @Test("Error is surfaced when server fails client verification") - // Verification should fail because the server does not have trust roots containing the client cert. - func testServerFailsClientValidation() async throws { - await #expect( - performing: { - try await self.executeUnaryRPCForEachTransportPair { security in - [ - HTTP2TransportTLSEnabledTests.Transport( - server: .posix( - .tls( - .mTLS( - certificateChain: [.bytes(security.server.certificate, format: .der)], - privateKey: .bytes(security.server.key, format: .der) - ) - ) - ), - client: .posix( - .tls( - .mTLS( - certificateChain: [.bytes(security.client.certificate, format: .der)], - privateKey: .bytes(security.client.key, format: .der) - ) { - $0.trustRoots = .certificates([ - .bytes(security.server.certificate, format: .der) - ]) - $0.serverHostname = "localhost" - } - ) - ) - ) - ] - } - }, - throws: { error in - guard let rootError = error as? RPCError else { - Issue.record("Should be an RPC error") - return false - } - #expect(rootError.code == .unavailable) - #expect( - rootError.message - == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." - ) - - guard - let sslError = rootError.cause as? NIOSSL.BoringSSLError, - case .sslError = sslError - else { - Issue.record( - "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))" - ) - return false - } - - return true - } - ) - } } -struct TestSecurity { - struct Server { - let certificate: [UInt8] - let key: [UInt8] - } - - struct Client { +struct SelfSignedCertificateKeyPairs { + struct CertificateKeyPair { let certificate: [UInt8] let key: [UInt8] } - let server: Server - let client: Client + let server: CertificateKeyPair + let client: CertificateKeyPair init() throws { - let server = try Self.createSelfSignedDERCertificateAndPrivateKey(name: "Server Certificate") - let client = try Self.createSelfSignedDERCertificateAndPrivateKey(name: "Client Certificate") + let server = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Server Certificate") + let client = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Client Certificate") - self.server = Server(certificate: server.cert, key: server.key) - self.client = Client(certificate: client.cert, key: client.key) + self.server = CertificateKeyPair(certificate: server.cert, key: server.key) + self.client = CertificateKeyPair(certificate: client.cert, key: client.key) } - private static func createSelfSignedDERCertificateAndPrivateKey( + private static func makeSelfSignedDERCertificateAndPrivateKey( name: String ) throws -> (cert: [UInt8], key: [UInt8]) { let swiftCryptoKey = P256.Signing.PrivateKey() From 5b1151f2f47d8dca23619149cdd513b0f9507f1d Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Tue, 15 Oct 2024 10:06:47 +0000 Subject: [PATCH 6/6] PR changes --- .../HTTP2TransportTLSEnabledTests.swift | 196 ++++++++---------- 1 file changed, 92 insertions(+), 104 deletions(-) diff --git a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift index 5d4a8ea..7992826 100644 --- a/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift +++ b/Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift @@ -28,12 +28,12 @@ struct HTTP2TransportTLSEnabledTests { @Test( "When using defaults, server does not perform client verification", - arguments: [TransportSecurity.posix], - [TransportSecurity.posix] + arguments: [TransportKind.posix], + [TransportKind.posix] ) func testRPC_Defaults_OK( - clientTransport: TransportSecurity, - serverTransport: TransportSecurity + clientTransport: TransportKind, + serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() let clientTransportConfig = self.makeDefaultClientTLSConfig( @@ -46,26 +46,23 @@ struct HTTP2TransportTLSEnabledTests { ) try await self.withClientAndServer( - clientTransportSecurity: clientTransportConfig, - serverTransportSecurity: serverTransportConfig + clientTLSConfig: clientTransportConfig, + serverTLSConfig: serverTransportConfig ) { control in - await #expect( - throws: Never.self, - performing: { - try await self.executeUnaryRPC(control: control) - } - ) + await #expect(throws: Never.self) { + try await self.executeUnaryRPC(control: control) + } } } @Test( "When using mTLS defaults, both client and server verify each others' certificates", - arguments: [TransportSecurity.posix], - [TransportSecurity.posix] + arguments: [TransportKind.posix], + [TransportKind.posix] ) func testRPC_mTLS_OK( - clientTransport: TransportSecurity, - serverTransport: TransportSecurity + clientTransport: TransportKind, + serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() let clientTransportConfig = self.makeMTLSClientTLSConfig( @@ -80,27 +77,24 @@ struct HTTP2TransportTLSEnabledTests { ) try await self.withClientAndServer( - clientTransportSecurity: clientTransportConfig, - serverTransportSecurity: serverTransportConfig + clientTLSConfig: clientTransportConfig, + serverTLSConfig: serverTransportConfig ) { control in - await #expect( - throws: Never.self, - performing: { - try await self.executeUnaryRPC(control: control) - } - ) + await #expect(throws: Never.self) { + try await self.executeUnaryRPC(control: control) + } } } @Test( "Error is surfaced when client fails server verification", - arguments: [TransportSecurity.posix], - [TransportSecurity.posix] + arguments: [TransportKind.posix], + [TransportKind.posix] ) // Verification should fail because the custom hostname is missing on the client. func testClientFailsServerValidation( - clientTransport: TransportSecurity, - serverTransport: TransportSecurity + clientTransport: TransportKind, + serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() let clientTransportConfig = self.makeMTLSClientTLSConfig( @@ -115,49 +109,46 @@ struct HTTP2TransportTLSEnabledTests { ) try await self.withClientAndServer( - clientTransportSecurity: clientTransportConfig, - serverTransportSecurity: serverTransportConfig + clientTLSConfig: clientTransportConfig, + serverTLSConfig: serverTransportConfig ) { control in - await #expect( - performing: { - try await self.executeUnaryRPC(control: control) - }, - throws: { error in - guard let rootError = error as? RPCError else { - Issue.record("Should be an RPC error") - return false - } - #expect(rootError.code == .unavailable) - #expect( - rootError.message - == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." - ) - - guard - let sslError = rootError.cause as? NIOSSLExtraError, - case .failedToValidateHostname = sslError - else { - Issue.record( - "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))" - ) - return false - } + await #expect { + try await self.executeUnaryRPC(control: control) + } throws: { error in + guard let rootError = error as? RPCError else { + Issue.record("Should be an RPC error") + return false + } + #expect(rootError.code == .unavailable) + #expect( + rootError.message + == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." + ) - return true + guard + let sslError = rootError.cause as? NIOSSLExtraError, + case .failedToValidateHostname = sslError + else { + Issue.record( + "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))" + ) + return false } - ) + + return true + } } } @Test( "Error is surfaced when server fails client verification", - arguments: [TransportSecurity.posix], - [TransportSecurity.posix] + arguments: [TransportKind.posix], + [TransportKind.posix] ) // Verification should fail because the server does not have trust roots containing the client cert. func testServerFailsClientValidation( - clientTransport: TransportSecurity, - serverTransport: TransportSecurity + clientTransport: TransportKind, + serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() let clientTransportConfig = self.makeMTLSClientTLSConfig( @@ -172,43 +163,40 @@ struct HTTP2TransportTLSEnabledTests { ) try await self.withClientAndServer( - clientTransportSecurity: clientTransportConfig, - serverTransportSecurity: serverTransportConfig + clientTLSConfig: clientTransportConfig, + serverTLSConfig: serverTransportConfig ) { control in - await #expect( - performing: { - try await self.executeUnaryRPC(control: control) - }, - throws: { error in - guard let rootError = error as? RPCError else { - Issue.record("Should be an RPC error") - return false - } - #expect(rootError.code == .unavailable) - #expect( - rootError.message - == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." - ) - - guard - let sslError = rootError.cause as? NIOSSL.BoringSSLError, - case .sslError = sslError - else { - Issue.record( - "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))" - ) - return false - } + await #expect { + try await self.executeUnaryRPC(control: control) + } throws: { error in + guard let rootError = error as? RPCError else { + Issue.record("Should be an RPC error") + return false + } + #expect(rootError.code == .unavailable) + #expect( + rootError.message + == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." + ) - return true + guard + let sslError = rootError.cause as? NIOSSL.BoringSSLError, + case .sslError = sslError + else { + Issue.record( + "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))" + ) + return false } - ) + + return true + } } } // - MARK: Test Utilities - enum TransportSecurity: Sendable { + enum TransportKind: Sendable { case posix } @@ -223,7 +211,7 @@ struct HTTP2TransportTLSEnabledTests { } func makeDefaultClientTLSConfig( - for transportSecurity: TransportSecurity, + for transportSecurity: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs ) -> TLSConfig.Client { switch transportSecurity { @@ -242,11 +230,11 @@ struct HTTP2TransportTLSEnabledTests { } func makeMTLSClientTLSConfig( - for transportSecurity: TransportSecurity, + for transportKind: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs, serverHostname: String? ) -> TLSConfig.Client { - switch transportSecurity { + switch transportKind { case .posix: return .posix( .tls( @@ -265,10 +253,10 @@ struct HTTP2TransportTLSEnabledTests { } func makeDefaultServerTLSConfig( - for transportSecurity: TransportSecurity, + for transportKind: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs ) -> TLSConfig.Server { - switch transportSecurity { + switch transportKind { case .posix: return .posix( .tls( @@ -282,11 +270,11 @@ struct HTTP2TransportTLSEnabledTests { } func makeMTLSServerTLSConfig( - for transportSecurity: TransportSecurity, + for transportKind: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs, includeClientCertificateInTrustRoots: Bool ) -> TLSConfig.Server { - switch transportSecurity { + switch transportKind { case .posix: return .posix( .tls( @@ -306,12 +294,12 @@ struct HTTP2TransportTLSEnabledTests { } func withClientAndServer( - clientTransportSecurity: TLSConfig.Client, - serverTransportSecurity: TLSConfig.Server, + clientTLSConfig: TLSConfig.Client, + serverTLSConfig: TLSConfig.Server, _ test: (ControlClient) async throws -> Void ) async throws { try await withThrowingDiscardingTaskGroup { group in - let server = self.makeServer(kind: serverTransportSecurity) + let server = self.makeServer(tlsConfig: serverTLSConfig) group.addTask { try await server.serve() @@ -322,7 +310,7 @@ struct HTTP2TransportTLSEnabledTests { return } let target: any ResolvableTarget = .ipv4(host: address.host, port: address.port) - let client = try self.makeClient(kind: clientTransportSecurity, target: target) + let client = try self.makeClient(tlsConfig: clientTLSConfig, target: target) group.addTask { try await client.run() @@ -336,10 +324,10 @@ struct HTTP2TransportTLSEnabledTests { } } - private func makeServer(kind: TLSConfig.Server) -> GRPCServer { + private func makeServer(tlsConfig: TLSConfig.Server) -> GRPCServer { let services = [ControlService()] - switch kind { + switch tlsConfig { case .posix(let transportSecurity): let server = GRPCServer( transport: .http2NIOPosix( @@ -354,12 +342,12 @@ struct HTTP2TransportTLSEnabledTests { } private func makeClient( - kind: TLSConfig.Client, + tlsConfig: TLSConfig.Client, target: any ResolvableTarget ) throws -> GRPCClient { let transport: any ClientTransport - switch kind { + switch tlsConfig { case .posix(let transportSecurity): transport = try HTTP2ClientTransport.Posix( target: target,