| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- /*
- * 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)
- }
- }
|