/* * 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 Foundation import GRPCCore import GRPCNIOTransportHTTP2Posix import GRPCNIOTransportHTTP2TransportServices import NIOSSL import SwiftASN1 import Testing #if canImport(Network) import Network #endif @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.clientsWithTLS, TransportKind.serversWithTLS ) @available(gRPCSwiftNIOTransport 1.0, *) func testRPC_Defaults_OK( clientTransport: TransportKind, serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() let clientConfig = self.makeDefaultTLSClientConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs ) let serverConfig = self.makeDefaultTLSServerConfig( for: serverTransport, certificateKeyPairs: certificateKeyPairs ) try await self.withClientAndServer( clientConfig: clientConfig, serverConfig: serverConfig ) { control in await #expect(throws: Never.self) { try await self.executeUnaryRPC(control: control) } } } @available(gRPCSwiftNIOTransport 1.2, *) final class TransportSpecificInterceptor: ServerInterceptor { let clientCert: [UInt8] init(_ clientCert: [UInt8]) { self.clientCert = clientCert } func intercept( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext, next: @Sendable (GRPCCore.StreamingServerRequest, GRPCCore.ServerContext) async throws -> GRPCCore.StreamingServerResponse ) async throws -> GRPCCore.StreamingServerResponse where Input: Sendable, Output: Sendable { let transportSpecific = context.transportSpecific let transportSpecificAsPosixContext = try #require( transportSpecific as? HTTP2ServerTransport.Posix.Context ) let peerCertificate = try #require(transportSpecificAsPosixContext.peerCertificate) var derSerializer = DER.Serializer() try peerCertificate.serialize(into: &derSerializer) #expect(derSerializer.serializedBytes == self.clientCert) return try await next(request, context) } } @Test( "Using the mTLS defaults, and with Posix transport, validate we get the peer cert on the server", arguments: [TransportKind.posix] ) @available(gRPCSwiftNIOTransport 1.2, *) func testRPC_mTLS_TransportContext_OK(supportedTransport: TransportKind) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() let clientConfig = self.makeMTLSClientConfig( for: supportedTransport, certificateKeyPairs: certificateKeyPairs, serverHostname: "localhost" ) let serverConfig = self.makeMTLSServerConfig( for: supportedTransport, certificateKeyPairs: certificateKeyPairs, includeClientCertificateInTrustRoots: true ) try await self.withClientAndServer( clientConfig: clientConfig, serverConfig: serverConfig, interceptors: [TransportSpecificInterceptor(certificateKeyPairs.client.certificate)] ) { 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.clientsWithTLS, TransportKind.clientsWithTLS ) @available(gRPCSwiftNIOTransport 1.0, *) func testRPC_mTLS_OK( clientTransport: TransportKind, serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() let clientConfig = self.makeMTLSClientConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs, serverHostname: "localhost" ) let serverConfig = self.makeMTLSServerConfig( for: serverTransport, certificateKeyPairs: certificateKeyPairs, includeClientCertificateInTrustRoots: true ) try await self.withClientAndServer( clientConfig: clientConfig, serverConfig: serverConfig ) { control in await #expect(throws: Never.self) { try await self.executeUnaryRPC(control: control) } } } @Test( "Error is surfaced when client fails server verification", arguments: TransportKind.clientsWithTLS, TransportKind.clientsWithTLS ) @available(gRPCSwiftNIOTransport 1.0, *) // 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.makeDefaultTLSClientConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs, authority: "wrong-hostname" ) let serverTransportConfig = self.makeDefaultTLSServerConfig( for: serverTransport, certificateKeyPairs: certificateKeyPairs ) await #expect { try await self.withClientAndServer( clientConfig: clientTransportConfig, serverConfig: serverTransportConfig ) { control in try await self.executeUnaryRPC(control: control) } } throws: { error in let rootError = try #require(error as? RPCError) #expect(rootError.code == .unavailable) switch clientTransport { case .posix: #expect( rootError.message == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." ) let sslError = try #require(rootError.cause as? NIOSSLExtraError) guard sslError == .failedToValidateHostname else { Issue.record( "Should be a NIOSSLExtraError.failedToValidateHostname error, but was: \(String(describing: rootError.cause))" ) return false } #if canImport(Network) case .transportServices: #expect(rootError.message.starts(with: "Could not establish a connection to")) let nwError = try #require(rootError.cause as? NWError) guard case .tls(Security.errSSLBadCert) = nwError else { Issue.record( "Should be a NWError.tls(-9808/errSSLBadCert) error, but was: \(String(describing: rootError.cause))" ) return false } #endif case .wrappedChannel: fatalError("Unsupported") } return true } } @Test( "Error is surfaced when server fails client verification", arguments: TransportKind.clientsWithTLS, TransportKind.clientsWithTLS ) @available(gRPCSwiftNIOTransport 1.0, *) // Verification should fail because the client does not offer a cert that // the server can use for mutual verification. func testServerFailsClientValidation( clientTransport: TransportKind, serverTransport: TransportKind ) async throws { let certificateKeyPairs = try SelfSignedCertificateKeyPairs() let clientTransportConfig = self.makeDefaultTLSClientConfig( for: clientTransport, certificateKeyPairs: certificateKeyPairs ) let serverTransportConfig = self.makeMTLSServerConfig( for: serverTransport, certificateKeyPairs: certificateKeyPairs, includeClientCertificateInTrustRoots: true ) await #expect { try await self.withClientAndServer( clientConfig: clientTransportConfig, serverConfig: serverTransportConfig ) { control in try await self.executeUnaryRPC(control: control) } } throws: { error in let rootError = try #require(error as? RPCError) #expect(rootError.code == .unavailable) #expect( rootError.message == "The server accepted the TCP connection but closed the connection before completing the HTTP/2 connection preface." ) switch clientTransport { case .posix: let sslError = try #require(rootError.cause as? NIOSSL.BoringSSLError) guard case .sslError = sslError else { Issue.record( "Should be a NIOSSL.sslError error, but was: \(String(describing: rootError.cause))" ) return false } #if canImport(Network) case .transportServices: let nwError = try #require(rootError.cause as? NWError) guard case .tls(Security.errSSLPeerCertUnknown) = nwError else { // When the TLS handshake fails, the connection will be closed from the client. // Network.framework will generally surface the right SSL error (in this case, an "unknown // certificate" from the server), but it will sometimes instead return the broken pipe // error caused by the underlying TLS handshake handler closing the connection: // we should tolerate this. if case .posix(POSIXErrorCode.EPIPE) = nwError { return true } Issue.record( "Should be a NWError.tls(-9829/errSSLPeerCertUnknown) error, but was: \(String(describing: rootError.cause))" ) return false } #endif case .wrappedChannel: fatalError("Unsupported") } return true } } // - MARK: Test Utilities enum TLSEnabledTestsError: Error { case failedToImportPKCS12 case unexpectedListeningAddress } struct Config { var security: Security var transport: Transport } @available(gRPCSwiftNIOTransport 1.0, *) enum ClientConfig { typealias Posix = Config< HTTP2ClientTransport.Posix.Config, HTTP2ClientTransport.Posix.TransportSecurity > case posix(Posix) #if canImport(Network) typealias TransportServices = Config< HTTP2ClientTransport.TransportServices.Config, HTTP2ClientTransport.TransportServices.TransportSecurity > case transportServices(TransportServices) #endif } @available(gRPCSwiftNIOTransport 1.0, *) enum ServerConfig { typealias Posix = Config< HTTP2ServerTransport.Posix.Config, HTTP2ServerTransport.Posix.TransportSecurity > case posix(Posix) #if canImport(Network) typealias TransportServices = Config< HTTP2ServerTransport.TransportServices.Config, HTTP2ServerTransport.TransportServices.TransportSecurity > case transportServices(TransportServices) #endif } @available(gRPCSwiftNIOTransport 1.0, *) private func makeDefaultPlaintextPosixClientConfig() -> ClientConfig.Posix { ClientConfig.Posix( security: .plaintext, transport: .defaults { config in config.backoff.initial = .milliseconds(100) config.backoff.multiplier = 1 config.backoff.jitter = 0 } ) } #if canImport(Network) @available(gRPCSwiftNIOTransport 1.0, *) private func makeDefaultPlaintextTSClientConfig() -> ClientConfig.TransportServices { ClientConfig.TransportServices( security: .plaintext, transport: .defaults { config in config.backoff.initial = .milliseconds(100) config.backoff.multiplier = 1 config.backoff.jitter = 0 } ) } #endif @available(gRPCSwiftNIOTransport 1.0, *) private func makeDefaultTLSClientConfig( for transportSecurity: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs, authority: String? = "localhost" ) -> ClientConfig { switch transportSecurity { case .posix: var config = self.makeDefaultPlaintextPosixClientConfig() config.security = .tls { $0.trustRoots = .certificates([ .bytes(certificateKeyPairs.server.certificate, format: .der) ]) } config.transport.http2.authority = authority return .posix(config) #if canImport(Network) case .transportServices: var config = self.makeDefaultPlaintextTSClientConfig() config.security = .tls { $0.trustRoots = .certificates([ .bytes(certificateKeyPairs.server.certificate, format: .der) ]) } config.transport.http2.authority = authority return .transportServices(config) #endif case .wrappedChannel: fatalError("Unsupported") } } #if canImport(Network) @available(gRPCSwiftNIOTransport 1.0, *) private func makeSecIdentityProvider( certificateBytes: [UInt8], privateKeyBytes: [UInt8] ) throws -> SecIdentity { let password = "somepassword" let bundle = NIOSSLPKCS12Bundle( certificateChain: [try NIOSSLCertificate(bytes: certificateBytes, format: .der)], privateKey: try NIOSSLPrivateKey(bytes: privateKeyBytes, format: .der) ) let pkcs12Bytes = try bundle.serialize(passphrase: password.utf8) let options = [ kSecImportExportPassphrase as String: password, kSecImportToMemoryOnly: kCFBooleanTrue!, ] as [AnyHashable: Any] var rawItems: CFArray? let status = SecPKCS12Import( Data(pkcs12Bytes) as CFData, options as CFDictionary, &rawItems ) guard status == errSecSuccess else { Issue.record("Failed to import PKCS12 bundle: status \(status).") throw TLSEnabledTestsError.failedToImportPKCS12 } let items = rawItems! as! [[String: Any]] let firstItem = items[0] let identity = firstItem[kSecImportItemIdentity as String] as! SecIdentity return identity } #endif @available(gRPCSwiftNIOTransport 1.0, *) private func makeMTLSClientConfig( for transportKind: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs, serverHostname: String? ) -> ClientConfig { switch transportKind { case .posix: var config = self.makeDefaultPlaintextPosixClientConfig() config.security = .mTLS( certificateChain: [.bytes(certificateKeyPairs.client.certificate, format: .der)], privateKey: .bytes(certificateKeyPairs.client.key, format: .der) ) { $0.trustRoots = .certificates([ .bytes(certificateKeyPairs.server.certificate, format: .der) ]) } config.transport.http2.authority = serverHostname return .posix(config) #if canImport(Network) case .transportServices: var config = self.makeDefaultPlaintextTSClientConfig() config.security = .mTLS { try self.makeSecIdentityProvider( certificateBytes: certificateKeyPairs.client.certificate, privateKeyBytes: certificateKeyPairs.client.key ) } configure: { $0.trustRoots = .certificates([ .bytes(certificateKeyPairs.server.certificate, format: .der) ]) } config.transport.http2.authority = serverHostname return .transportServices(config) #endif case .wrappedChannel: fatalError("Unsupported") } } @available(gRPCSwiftNIOTransport 1.0, *) private func makeDefaultPlaintextPosixServerConfig() -> ServerConfig.Posix { ServerConfig.Posix(security: .plaintext, transport: .defaults) } #if canImport(Network) @available(gRPCSwiftNIOTransport 1.0, *) private func makeDefaultPlaintextTSServerConfig() -> ServerConfig.TransportServices { ServerConfig.TransportServices(security: .plaintext, transport: .defaults) } #endif @available(gRPCSwiftNIOTransport 1.0, *) private func makeDefaultTLSServerConfig( for transportKind: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs ) -> ServerConfig { switch transportKind { case .posix: var config = self.makeDefaultPlaintextPosixServerConfig() config.security = .tls( certificateChain: [.bytes(certificateKeyPairs.server.certificate, format: .der)], privateKey: .bytes(certificateKeyPairs.server.key, format: .der) ) return .posix(config) #if canImport(Network) case .transportServices: var config = self.makeDefaultPlaintextTSServerConfig() config.security = .tls { try self.makeSecIdentityProvider( certificateBytes: certificateKeyPairs.server.certificate, privateKeyBytes: certificateKeyPairs.server.key ) } return .transportServices(config) #endif case .wrappedChannel: fatalError("Unsupported") } } @available(gRPCSwiftNIOTransport 1.0, *) private func makeMTLSServerConfig( for transportKind: TransportKind, certificateKeyPairs: SelfSignedCertificateKeyPairs, includeClientCertificateInTrustRoots: Bool ) -> ServerConfig { switch transportKind { case .posix: var config = self.makeDefaultPlaintextPosixServerConfig() config.security = .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) ]) } } return .posix(config) #if canImport(Network) case .transportServices: var config = self.makeDefaultPlaintextTSServerConfig() config.security = .mTLS { try self.makeSecIdentityProvider( certificateBytes: certificateKeyPairs.server.certificate, privateKeyBytes: certificateKeyPairs.server.key ) } configure: { if includeClientCertificateInTrustRoots { $0.trustRoots = .certificates([ .bytes(certificateKeyPairs.client.certificate, format: .der) ]) } } return .transportServices(config) #endif case .wrappedChannel: fatalError("Unsupported") } } @available(gRPCSwiftNIOTransport 1.0, *) func withClientAndServer( clientConfig: ClientConfig, serverConfig: ServerConfig, interceptors: [any ServerInterceptor] = [], _ test: (ControlClient) async throws -> Void ) async throws { let serverTransport: NIOServerTransport switch serverConfig { case .posix(let posix): serverTransport = NIOServerTransport( .http2NIOPosix( address: .ipv4(host: "127.0.0.1", port: 0), transportSecurity: posix.security, config: posix.transport ) ) #if canImport(Network) case .transportServices(let config): serverTransport = NIOServerTransport( .http2NIOTS( address: .ipv4(host: "127.0.0.1", port: 0), transportSecurity: config.security, config: config.transport ) ) #endif } try await withGRPCServer( transport: serverTransport, services: [ControlService()], interceptors: interceptors ) { server in guard let address = try await server.listeningAddress?.ipv4 else { throw TLSEnabledTestsError.unexpectedListeningAddress } let target: any ResolvableTarget = .ipv4(host: address.host, port: address.port) let clientTransport: NIOClientTransport switch clientConfig { case .posix(let config): clientTransport = try NIOClientTransport( .http2NIOPosix( target: target, transportSecurity: config.security, config: config.transport ) ) #if canImport(Network) case .transportServices(let config): clientTransport = try NIOClientTransport( .http2NIOTS(target: target, transportSecurity: config.security, config: config.transport) ) #endif } try await withGRPCClient(transport: clientTransport) { client in let control = ControlClient(wrapping: client) try await test(control) } } } @available(gRPCSwiftNIOTransport 1.0, *) 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 } } } }