|
|
@@ -0,0 +1,432 @@
|
|
|
+/*
|
|
|
+ * 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 Crypto
|
|
|
+import Foundation
|
|
|
+import GRPCNIOTransportHTTP2Posix
|
|
|
+import NIOSSL
|
|
|
+import SwiftASN1
|
|
|
+import Testing
|
|
|
+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: [TransportKind.posix],
|
|
|
+ [TransportKind.posix]
|
|
|
+ )
|
|
|
+ func testRPC_Defaults_OK(
|
|
|
+ clientTransport: TransportKind,
|
|
|
+ serverTransport: TransportKind
|
|
|
+ ) 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(
|
|
|
+ clientTLSConfig: clientTransportConfig,
|
|
|
+ serverTLSConfig: serverTransportConfig
|
|
|
+ ) { control in
|
|
|
+ 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: [TransportKind.posix],
|
|
|
+ [TransportKind.posix]
|
|
|
+ )
|
|
|
+ func testRPC_mTLS_OK(
|
|
|
+ clientTransport: TransportKind,
|
|
|
+ serverTransport: TransportKind
|
|
|
+ ) 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(
|
|
|
+ clientTLSConfig: clientTransportConfig,
|
|
|
+ serverTLSConfig: serverTransportConfig
|
|
|
+ ) { control in
|
|
|
+ await #expect(throws: Never.self) {
|
|
|
+ try await self.executeUnaryRPC(control: control)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test(
|
|
|
+ "Error is surfaced when client fails server verification",
|
|
|
+ arguments: [TransportKind.posix],
|
|
|
+ [TransportKind.posix]
|
|
|
+ )
|
|
|
+ // Verification should fail because the custom hostname is missing on the client.
|
|
|
+ func testClientFailsServerValidation(
|
|
|
+ clientTransport: TransportKind,
|
|
|
+ serverTransport: TransportKind
|
|
|
+ ) 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(
|
|
|
+ clientTLSConfig: clientTransportConfig,
|
|
|
+ serverTLSConfig: serverTransportConfig
|
|
|
+ ) { control in
|
|
|
+ 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."
|
|
|
+ )
|
|
|
+
|
|
|
+ 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: [TransportKind.posix],
|
|
|
+ [TransportKind.posix]
|
|
|
+ )
|
|
|
+ // Verification should fail because the server does not have trust roots containing the client cert.
|
|
|
+ func testServerFailsClientValidation(
|
|
|
+ clientTransport: TransportKind,
|
|
|
+ serverTransport: TransportKind
|
|
|
+ ) 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(
|
|
|
+ clientTLSConfig: clientTransportConfig,
|
|
|
+ serverTLSConfig: serverTransportConfig
|
|
|
+ ) { control in
|
|
|
+ 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."
|
|
|
+ )
|
|
|
+
|
|
|
+ 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 TransportKind: Sendable {
|
|
|
+ case posix
|
|
|
+ }
|
|
|
+
|
|
|
+ enum TLSConfig {
|
|
|
+ enum Client {
|
|
|
+ case posix(HTTP2ClientTransport.Posix.Config.TransportSecurity)
|
|
|
+ }
|
|
|
+
|
|
|
+ enum Server {
|
|
|
+ case posix(HTTP2ServerTransport.Posix.Config.TransportSecurity)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func makeDefaultClientTLSConfig(
|
|
|
+ for transportSecurity: TransportKind,
|
|
|
+ certificateKeyPairs: SelfSignedCertificateKeyPairs
|
|
|
+ ) -> TLSConfig.Client {
|
|
|
+ switch transportSecurity {
|
|
|
+ case .posix:
|
|
|
+ return .posix(
|
|
|
+ .tls(
|
|
|
+ .defaults {
|
|
|
+ $0.trustRoots = .certificates([
|
|
|
+ .bytes(certificateKeyPairs.server.certificate, format: .der)
|
|
|
+ ])
|
|
|
+ $0.serverHostname = "localhost"
|
|
|
+ }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func makeMTLSClientTLSConfig(
|
|
|
+ for transportKind: TransportKind,
|
|
|
+ certificateKeyPairs: SelfSignedCertificateKeyPairs,
|
|
|
+ serverHostname: String?
|
|
|
+ ) -> TLSConfig.Client {
|
|
|
+ switch transportKind {
|
|
|
+ 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
|
|
|
+ }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func makeDefaultServerTLSConfig(
|
|
|
+ for transportKind: TransportKind,
|
|
|
+ certificateKeyPairs: SelfSignedCertificateKeyPairs
|
|
|
+ ) -> TLSConfig.Server {
|
|
|
+ switch transportKind {
|
|
|
+ case .posix:
|
|
|
+ return .posix(
|
|
|
+ .tls(
|
|
|
+ .defaults(
|
|
|
+ certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)],
|
|
|
+ privateKey: .bytes(certificateKeyPairs.server.key, format: .der)
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func makeMTLSServerTLSConfig(
|
|
|
+ for transportKind: TransportKind,
|
|
|
+ certificateKeyPairs: SelfSignedCertificateKeyPairs,
|
|
|
+ includeClientCertificateInTrustRoots: Bool
|
|
|
+ ) -> TLSConfig.Server {
|
|
|
+ switch transportKind {
|
|
|
+ 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)
|
|
|
+ ])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ func withClientAndServer(
|
|
|
+ clientTLSConfig: TLSConfig.Client,
|
|
|
+ serverTLSConfig: TLSConfig.Server,
|
|
|
+ _ test: (ControlClient) async throws -> Void
|
|
|
+ ) async throws {
|
|
|
+ try await withThrowingDiscardingTaskGroup { group in
|
|
|
+ let server = self.makeServer(tlsConfig: serverTLSConfig)
|
|
|
+
|
|
|
+ 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(tlsConfig: clientTLSConfig, target: target)
|
|
|
+
|
|
|
+ group.addTask {
|
|
|
+ try await client.run()
|
|
|
+ }
|
|
|
+
|
|
|
+ let control = ControlClient(wrapping: client)
|
|
|
+ try await test(control)
|
|
|
+
|
|
|
+ server.beginGracefulShutdown()
|
|
|
+ client.beginGracefulShutdown()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func makeServer(tlsConfig: TLSConfig.Server) -> GRPCServer {
|
|
|
+ let services = [ControlService()]
|
|
|
+
|
|
|
+ switch tlsConfig {
|
|
|
+ case .posix(let transportSecurity):
|
|
|
+ let server = GRPCServer(
|
|
|
+ transport: .http2NIOPosix(
|
|
|
+ address: .ipv4(host: "127.0.0.1", port: 0),
|
|
|
+ config: .defaults(transportSecurity: transportSecurity)
|
|
|
+ ),
|
|
|
+ services: services
|
|
|
+ )
|
|
|
+
|
|
|
+ return server
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func makeClient(
|
|
|
+ tlsConfig: TLSConfig.Client,
|
|
|
+ target: any ResolvableTarget
|
|
|
+ ) throws -> GRPCClient {
|
|
|
+ let transport: any ClientTransport
|
|
|
+
|
|
|
+ switch tlsConfig {
|
|
|
+ case .posix(let transportSecurity):
|
|
|
+ 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) async throws {
|
|
|
+ let input = ControlInput.with { $0.numberOfMessages = 1 }
|
|
|
+ let request = ClientRequest(message: input)
|
|
|
+ try await control.unary(request: request) { response in
|
|
|
+ #expect(throws: Never.self) { try response.message }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+struct SelfSignedCertificateKeyPairs {
|
|
|
+ struct CertificateKeyPair {
|
|
|
+ let certificate: [UInt8]
|
|
|
+ let key: [UInt8]
|
|
|
+ }
|
|
|
+
|
|
|
+ let server: CertificateKeyPair
|
|
|
+ let client: CertificateKeyPair
|
|
|
+
|
|
|
+ init() throws {
|
|
|
+ let server = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Server Certificate")
|
|
|
+ let client = try Self.makeSelfSignedDERCertificateAndPrivateKey(name: "Client Certificate")
|
|
|
+
|
|
|
+ self.server = CertificateKeyPair(certificate: server.cert, key: server.key)
|
|
|
+ self.client = CertificateKeyPair(certificate: client.cert, key: client.key)
|
|
|
+ }
|
|
|
+
|
|
|
+ private static func makeSelfSignedDERCertificateAndPrivateKey(
|
|
|
+ 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)
|
|
|
+ }
|
|
|
+}
|