Browse Source

Add TLS support for NIOPosix H2 client (#2036)

## Motivation
We currently have a `NIOPosix` client transport implementation in gRPC
v2, but it doesn't support TLS.

## Modifications
This PR adds support for TLS in the NIOPosix-backed HTTP/2
implementation of the client transport for gRPC v2.

## Result
We now support TLS when using the NIOPosix client transport in gRPC V2.
Gus Cairo 1 year ago
parent
commit
62b7f850a6

+ 1 - 1
Examples/v2/echo/Subcommands/Collect.swift

@@ -32,7 +32,7 @@ struct Collect: AsyncParsableCommand {
     let client = GRPCClient(
       transport: try .http2NIOPosix(
         target: self.arguments.target,
-        config: .defaults()
+        config: .defaults(transportSecurity: .plaintext)
       )
     )
 

+ 1 - 1
Examples/v2/echo/Subcommands/Expand.swift

@@ -32,7 +32,7 @@ struct Expand: AsyncParsableCommand {
     let client = GRPCClient(
       transport: try .http2NIOPosix(
         target: self.arguments.target,
-        config: .defaults()
+        config: .defaults(transportSecurity: .plaintext)
       )
     )
 

+ 1 - 1
Examples/v2/echo/Subcommands/Get.swift

@@ -30,7 +30,7 @@ struct Get: AsyncParsableCommand {
     let client = GRPCClient(
       transport: try .http2NIOPosix(
         target: self.arguments.target,
-        config: .defaults()
+        config: .defaults(transportSecurity: .plaintext)
       )
     )
 

+ 1 - 1
Examples/v2/echo/Subcommands/Update.swift

@@ -32,7 +32,7 @@ struct Update: AsyncParsableCommand {
     let client = GRPCClient(
       transport: try .http2NIOPosix(
         target: self.arguments.target,
-        config: .defaults()
+        config: .defaults(transportSecurity: .plaintext)
       )
     )
 

+ 1 - 1
Examples/v2/hello-world/Subcommands/Greet.swift

@@ -33,7 +33,7 @@ struct Greet: AsyncParsableCommand {
       let client = GRPCClient(
         transport: try .http2NIOPosix(
           target: .ipv4(host: "127.0.0.1", port: self.port),
-          config: .defaults()
+          config: .defaults(transportSecurity: .plaintext)
         )
       )
 

+ 1 - 1
Examples/v2/route-guide/Subcommands/GetFeature.swift

@@ -39,7 +39,7 @@ struct GetFeature: AsyncParsableCommand {
   func run() async throws {
     let transport = try HTTP2ClientTransport.Posix(
       target: .ipv4(host: "127.0.0.1", port: self.port),
-      config: .defaults()
+      config: .defaults(transportSecurity: .plaintext)
     )
     let client = GRPCClient(transport: transport)
 

+ 1 - 1
Examples/v2/route-guide/Subcommands/ListFeatures.swift

@@ -53,7 +53,7 @@ struct ListFeatures: AsyncParsableCommand {
   func run() async throws {
     let transport = try HTTP2ClientTransport.Posix(
       target: .ipv4(host: "127.0.0.1", port: self.port),
-      config: .defaults()
+      config: .defaults(transportSecurity: .plaintext)
     )
     let client = GRPCClient(transport: transport)
 

+ 1 - 1
Examples/v2/route-guide/Subcommands/RecordRoute.swift

@@ -32,7 +32,7 @@ struct RecordRoute: AsyncParsableCommand {
   func run() async throws {
     let transport = try HTTP2ClientTransport.Posix(
       target: .ipv4(host: "127.0.0.1", port: self.port),
-      config: .defaults()
+      config: .defaults(transportSecurity: .plaintext)
     )
     let client = GRPCClient(transport: transport)
 

+ 1 - 1
Examples/v2/route-guide/Subcommands/RouteChat.swift

@@ -32,7 +32,7 @@ struct RouteChat: AsyncParsableCommand {
   func run() async throws {
     let transport = try HTTP2ClientTransport.Posix(
       target: .ipv4(host: "127.0.0.1", port: self.port),
-      config: .defaults()
+      config: .defaults(transportSecurity: .plaintext)
     )
     let client = GRPCClient(transport: transport)
 

+ 72 - 13
Sources/GRPCHTTP2TransportNIOPosix/HTTP2ClientTransport+Posix.swift

@@ -19,14 +19,18 @@ public import GRPCHTTP2Core  // should be @usableFromInline
 public import NIOCore
 public import NIOPosix  // has to be public because of default argument value in init
 
+#if canImport(NIOSSL)
+import NIOSSL
+#endif
+
 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
 extension HTTP2ClientTransport {
-  /// A `ClientTransport` using HTTP/2 built on top of `NIOPosix`.
+  /// A ``GRPCCore/ClientTransport`` using HTTP/2 built on top of `NIOPosix`.
   ///
   /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use
   /// on Linux and Darwin based platform (macOS, iOS, etc.) However, it's *strongly* recommended
   /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of
-  /// the `HTTP2ClientTransport`.
+  /// the ``GRPCHTTP2Core/HTTP2ClientTransport``.
   ///
   /// To use this transport you need to provide a 'target' to connect to which will be resolved
   /// by an appropriate resolver from the resolver registry. By default the resolver registry can
@@ -34,16 +38,19 @@ extension HTTP2ClientTransport {
   /// targets. If you use a custom target you must also provide an appropriately configured
   /// registry.
   ///
-  /// You can control various aspects of connection creation, management and RPC behavior via the
-  /// ``Config``. Load balancing policies and other RPC specific behavior can be configured via
+  /// You can control various aspects of connection creation, management, security and RPC behavior via
+  /// the ``Config``. Load balancing policies and other RPC specific behavior can be configured via
   /// the ``ServiceConfig`` (if it isn't provided by a resolver).
   ///
   /// Beyond creating the transport you don't need to interact with it directly, instead, pass it
   /// to a `GRPCClient`:
   ///
   /// ```swift
-  /// try await withThrowingDiscardingTaskGroup {
-  ///   let transport = try HTTP2ClientTransport.Posix(target: .dns(host: "example.com"))
+  /// try await withThrowingDiscardingTaskGroup { group in
+  ///   let transport = try HTTP2ClientTransport.Posix(
+  ///     target: .ipv4(host: "example.com"),
+  ///     config: .defaults(transportSecurity: .plaintext)
+  ///   )
   ///   let client = GRPCClient(transport: transport)
   ///   group.addTask {
   ///     try await client.run()
@@ -87,7 +94,7 @@ extension HTTP2ClientTransport {
       // Configure a connector.
       self.channel = GRPCChannel(
         resolver: resolver,
-        connector: Connector(eventLoopGroup: eventLoopGroup, config: config),
+        connector: try Connector(eventLoopGroup: eventLoopGroup, config: config),
         config: GRPCChannel.Config(posix: config),
         defaultServiceConfig: serviceConfig
       )
@@ -125,9 +132,33 @@ extension HTTP2ClientTransport.Posix {
     private let config: HTTP2ClientTransport.Posix.Config
     private let eventLoopGroup: any EventLoopGroup
 
-    init(eventLoopGroup: any EventLoopGroup, config: HTTP2ClientTransport.Posix.Config) {
+    #if canImport(NIOSSL)
+    private let nioSSLContext: NIOSSLContext?
+    private let serverHostname: String?
+    #endif
+
+    init(eventLoopGroup: any EventLoopGroup, config: HTTP2ClientTransport.Posix.Config) throws {
       self.eventLoopGroup = eventLoopGroup
       self.config = config
+
+      #if canImport(NIOSSL)
+      switch self.config.transportSecurity.wrapped {
+      case .plaintext:
+        self.nioSSLContext = nil
+        self.serverHostname = nil
+      case .tls(let tlsConfig):
+        do {
+          self.nioSSLContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig))
+          self.serverHostname = tlsConfig.serverHostname
+        } catch {
+          throw RuntimeError(
+            code: .transportError,
+            message: "Couldn't create SSL context, check your TLS configuration.",
+            cause: error
+          )
+        }
+      }
+      #endif
     }
 
     func establishConnection(
@@ -137,7 +168,18 @@ extension HTTP2ClientTransport.Posix {
         group: self.eventLoopGroup
       ).connect(to: address) { channel in
         channel.eventLoop.makeCompletedFuture {
-          try channel.pipeline.syncOperations.configureGRPCClientPipeline(
+          #if canImport(NIOSSL)
+          if let nioSSLContext = self.nioSSLContext {
+            try channel.pipeline.syncOperations.addHandler(
+              NIOSSLClientHandler(
+                context: nioSSLContext,
+                serverHostname: self.serverHostname
+              )
+            )
+          }
+          #endif
+
+          return try channel.pipeline.syncOperations.configureGRPCClientPipeline(
             channel: channel,
             config: GRPCChannel.Config(posix: self.config)
           )
@@ -164,31 +206,48 @@ extension HTTP2ClientTransport.Posix {
     /// Compression configuration.
     public var compression: HTTP2ClientTransport.Config.Compression
 
+    /// The transport's security.
+    public var transportSecurity: TransportSecurity
+
     /// Creates a new connection configuration.
     ///
-    /// See also ``defaults``.
+    /// - Parameters:
+    ///   - http2: HTTP2 configuration.
+    ///   - backoff: Backoff configuration.
+    ///   - connection: Connection configuration.
+    ///   - compression: Compression configuration.
+    ///   - transportSecurity: The transport's security configuration.
+    ///
+    /// - SeeAlso: ``defaults(_:)``.
     public init(
       http2: HTTP2ClientTransport.Config.HTTP2,
       backoff: HTTP2ClientTransport.Config.Backoff,
       connection: HTTP2ClientTransport.Config.Connection,
-      compression: HTTP2ClientTransport.Config.Compression
+      compression: HTTP2ClientTransport.Config.Compression,
+      transportSecurity: TransportSecurity
     ) {
       self.http2 = http2
       self.connection = connection
       self.backoff = backoff
       self.compression = compression
+      self.transportSecurity = transportSecurity
     }
 
     /// Default values.
     ///
     /// - Parameters:
+    ///   - transportSecurity: The security settings applied to the transport.
     ///   - configure: A closure which allows you to modify the defaults before returning them.
-    public static func defaults(_ configure: (_ config: inout Self) -> Void = { _ in }) -> Self {
+    public static func defaults(
+      transportSecurity: TransportSecurity,
+      configure: (_ config: inout Self) -> Void = { _ in }
+    ) -> Self {
       var config = Self(
         http2: .defaults,
         backoff: .defaults,
         connection: .defaults,
-        compression: .defaults
+        compression: .defaults,
+        transportSecurity: transportSecurity
       )
       configure(&config)
       return config

+ 41 - 8
Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift

@@ -15,7 +15,7 @@
  */
 
 public import GRPCCore
-public import GRPCHTTP2Core
+public import GRPCHTTP2Core  // should be @usableFromInline
 internal import NIOCore
 internal import NIOExtras
 internal import NIOHTTP2
@@ -27,7 +27,33 @@ import NIOSSL
 #endif
 
 extension HTTP2ServerTransport {
-  /// A NIOPosix-backed implementation of a server transport.
+  /// A ``GRPCCore/ServerTransport`` using HTTP/2 built on top of `NIOPosix`.
+  ///
+  /// This transport builds on top of SwiftNIO's Posix networking layer and is suitable for use
+  /// on Linux and Darwin based platform (macOS, iOS, etc.) However, it's *strongly* recommended
+  /// that if you are targeting Darwin platforms then you should use the `NIOTS` variant of
+  /// the ``GRPCHTTP2Core/HTTP2ServerTransport``.
+  ///
+  /// You can control various aspects of connection creation, management, security and RPC behavior via
+  /// the ``Config``.
+  ///
+  /// Beyond creating the transport you don't need to interact with it directly, instead, pass it
+  /// to a `GRPCServer`:
+  ///
+  /// ```swift
+  /// try await withThrowingDiscardingTaskGroup { group in
+  ///   let transport = HTTP2ServerTransport.Posix(
+  ///     address: .ipv4(host: "127.0.0.1", port: 0),
+  ///     config: .defaults(transportSecurity: .plaintext)
+  ///   )
+  ///   let server = GRPCServer(transport: transport, services: someServices)
+  ///   group.addTask {
+  ///     try await server.serve()
+  ///   }
+  ///
+  ///   // ...
+  /// }
+  /// ```
   @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
   public final class Posix: ServerTransport, ListeningServerTransport {
     private let address: GRPCHTTP2Core.SocketAddress
@@ -340,27 +366,34 @@ extension HTTP2ServerTransport.Posix {
   public struct Config: Sendable {
     /// Compression configuration.
     public var compression: HTTP2ServerTransport.Config.Compression
+
     /// Connection configuration.
     public var connection: HTTP2ServerTransport.Config.Connection
+
     /// HTTP2 configuration.
     public var http2: HTTP2ServerTransport.Config.HTTP2
+
     /// RPC configuration.
     public var rpc: HTTP2ServerTransport.Config.RPC
+
     /// The transport's security.
     public var transportSecurity: TransportSecurity
 
     /// Construct a new `Config`.
+    ///
     /// - Parameters:
-    ///   - compression: Compression configuration.
-    ///   - connection: Connection configuration.
     ///   - http2: HTTP2 configuration.
     ///   - rpc: RPC configuration.
+    ///   - connection: Connection configuration.
+    ///   - compression: Compression configuration.
     ///   - transportSecurity: The transport's security configuration.
+    ///
+    /// - SeeAlso: ``defaults(transportSecurity:configure:)``
     public init(
-      compression: HTTP2ServerTransport.Config.Compression,
-      connection: HTTP2ServerTransport.Config.Connection,
       http2: HTTP2ServerTransport.Config.HTTP2,
       rpc: HTTP2ServerTransport.Config.RPC,
+      connection: HTTP2ServerTransport.Config.Connection,
+      compression: HTTP2ServerTransport.Config.Compression,
       transportSecurity: TransportSecurity
     ) {
       self.compression = compression
@@ -380,10 +413,10 @@ extension HTTP2ServerTransport.Posix {
       configure: (_ config: inout Self) -> Void = { _ in }
     ) -> Self {
       var config = Self(
-        compression: .defaults,
-        connection: .defaults,
         http2: .defaults,
         rpc: .defaults,
+        connection: .defaults,
+        compression: .defaults,
         transportSecurity: transportSecurity
       )
       configure(&config)

+ 22 - 6
Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift

@@ -131,18 +131,34 @@ extension TLSConfiguration {
     let certificateChain = try tlsConfig.certificateChain.sslCertificateSources()
     let privateKey = try NIOSSLPrivateKey(privateKey: tlsConfig.privateKey)
 
-    var tlsConfiguration = TLSConfiguration.makeServerConfiguration(
+    self = TLSConfiguration.makeServerConfiguration(
       certificateChain: certificateChain,
       privateKey: .privateKey(privateKey)
     )
-    tlsConfiguration.minimumTLSVersion = .tlsv12
-    tlsConfiguration.certificateVerification = CertificateVerification(
+    self.minimumTLSVersion = .tlsv12
+    self.certificateVerification = CertificateVerification(
       tlsConfig.clientCertificateVerification
     )
-    tlsConfiguration.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots)
-    tlsConfiguration.applicationProtocols = ["grpc-exp", "h2"]
+    self.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots)
+    self.applicationProtocols = ["grpc-exp", "h2"]
+  }
+
+  @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+  package init(_ tlsConfig: HTTP2ClientTransport.Posix.Config.TLS) throws {
+    self = TLSConfiguration.makeClientConfiguration()
+    self.certificateChain = try tlsConfig.certificateChain.sslCertificateSources()
 
-    self = tlsConfiguration
+    if let privateKey = tlsConfig.privateKey {
+      let privateKeySource = try NIOSSLPrivateKey(privateKey: privateKey)
+      self.privateKey = .privateKey(privateKeySource)
+    }
+
+    self.minimumTLSVersion = .tlsv12
+    self.certificateVerification = CertificateVerification(
+      tlsConfig.serverCertificateVerification
+    )
+    self.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots)
+    self.applicationProtocols = ["grpc-exp", "h2"]
   }
 }
 #endif

+ 82 - 2
Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift

@@ -147,10 +147,12 @@ extension HTTP2ServerTransport.Posix.Config {
     /// This connection is plaintext: no encryption will take place.
     public static let plaintext = Self(wrapped: .plaintext)
 
+    #if canImport(NIOSSL)
     /// This connection will use TLS.
     public static func tls(_ tls: TLS) -> Self {
       Self(wrapped: .tls(tls))
     }
+    #endif
   }
 
   public struct TLS: Sendable {
@@ -184,7 +186,7 @@ extension HTTP2ServerTransport.Posix.Config {
       certificateChain: [TLSConfig.CertificateSource],
       privateKey: TLSConfig.PrivateKeySource
     ) -> Self {
-      Self.init(
+      Self(
         certificateChain: certificateChain,
         privateKey: privateKey,
         clientCertificateVerification: .noVerification,
@@ -207,7 +209,7 @@ extension HTTP2ServerTransport.Posix.Config {
       certificateChain: [TLSConfig.CertificateSource],
       privateKey: TLSConfig.PrivateKeySource
     ) -> Self {
-      Self.init(
+      Self(
         certificateChain: certificateChain,
         privateKey: privateKey,
         clientCertificateVerification: .noHostnameVerification,
@@ -217,3 +219,81 @@ extension HTTP2ServerTransport.Posix.Config {
     }
   }
 }
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension HTTP2ClientTransport.Posix.Config {
+  /// The security configuration for this connection.
+  public struct TransportSecurity: Sendable {
+    package enum Wrapped: Sendable {
+      case plaintext
+      case tls(TLS)
+    }
+
+    package let wrapped: Wrapped
+
+    /// This connection is plaintext: no encryption will take place.
+    public static let plaintext = Self(wrapped: .plaintext)
+
+    #if canImport(NIOSSL)
+    /// This connection will use TLS.
+    public static func tls(_ tls: TLS) -> Self {
+      Self(wrapped: .tls(tls))
+    }
+    #endif
+  }
+
+  public struct TLS: Sendable {
+    /// The certificates the client will offer during negotiation.
+    public var certificateChain: [TLSConfig.CertificateSource]
+
+    /// The private key associated with the leaf certificate.
+    public var privateKey: TLSConfig.PrivateKeySource?
+
+    /// How to verify the server certificate, if one is presented.
+    public var serverCertificateVerification: TLSConfig.CertificateVerification
+
+    /// The trust roots to be used when verifying server certificates.
+    public var trustRoots: TLSConfig.TrustRootsSource
+
+    /// An optional server hostname to use when verifying certificates.
+    public var serverHostname: String?
+
+    /// 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`
+    ///
+    /// - Returns: A new HTTP2 NIO Posix transport TLS config.
+    public static var defaults: Self {
+      Self(
+        certificateChain: [],
+        privateKey: nil,
+        serverCertificateVerification: .fullVerification,
+        trustRoots: .systemDefault,
+        serverHostname: nil
+      )
+    }
+
+    /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match
+    /// the requirements of mTLS:
+    /// - `trustRoots` equals `systemDefault`
+    ///
+    /// - Parameters:
+    ///   - certificateChain: The certificates the client will offer during negotiation.
+    ///   - privateKey: The private key associated with the leaf certificate.
+    /// - Returns: A new HTTP2 NIO Posix transport TLS config.
+    public static func mTLS(
+      certificateChain: [TLSConfig.CertificateSource],
+      privateKey: TLSConfig.PrivateKeySource
+    ) -> Self {
+      Self(
+        certificateChain: certificateChain,
+        privateKey: privateKey,
+        serverCertificateVerification: .fullVerification,
+        trustRoots: .systemDefault
+      )
+    }
+  }
+}

+ 1 - 1
Sources/interoperability-tests/InteroperabilityTestsExecutable.swift

@@ -106,7 +106,7 @@ struct InteroperabilityTestsExecutable: AsyncParsableCommand {
       return GRPCClient(
         transport: try .http2NIOPosix(
           target: .ipv4(host: host, port: port),
-          config: .defaults {
+          config: .defaults(transportSecurity: .plaintext) {
             $0.compression.enabledAlgorithms = .all
           },
           serviceConfig: serviceConfig

+ 1 - 1
Sources/performance-worker/WorkerService.swift

@@ -457,7 +457,7 @@ extension WorkerService {
         client: GRPCClient(
           transport: try .http2NIOPosix(
             target: target,
-            config: .defaults()
+            config: .defaults(transportSecurity: .plaintext)
           )
         ),
         concurrentRPCs: Int(config.outstandingRpcsPerChannel),

+ 74 - 4
Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift

@@ -263,7 +263,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
     -----END RSA PRIVATE KEY-----
     """
 
-  func testTLSConfig_Defaults() throws {
+  func testServerTLSConfig_Defaults() throws {
     let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults(
       certificateChain: [
         .bytes(Array(Self.samplePemCert.utf8), format: .pem)
@@ -293,7 +293,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
     XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
   }
 
-  func testTLSConfig_mTLS() throws {
+  func testServerTLSConfig_mTLS() throws {
     let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.mTLS(
       certificateChain: [
         .bytes(Array(Self.samplePemCert.utf8), format: .pem)
@@ -323,7 +323,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
     XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
   }
 
-  func testTLSConfig_FullVerifyClient() throws {
+  func testServerTLSConfig_FullVerifyClient() throws {
     var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults(
       certificateChain: [
         .bytes(Array(Self.samplePemCert.utf8), format: .pem)
@@ -354,7 +354,7 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
     XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
   }
 
-  func testTLSConfig_CustomTrustRoots() throws {
+  func testServerTLSConfig_CustomTrustRoots() throws {
     var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults(
       certificateChain: [
         .bytes(Array(Self.samplePemCert.utf8), format: .pem)
@@ -387,5 +387,75 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
     )
     XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
   }
+
+  func testClientTLSConfig_Defaults() throws {
+    let grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults
+    let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig)
+
+    XCTAssertEqual(nioSSLTLSConfig.certificateChain, [])
+    XCTAssertNil(nioSSLTLSConfig.privateKey)
+    XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12)
+    XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification)
+    XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default)
+    XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
+  }
+
+  func testClientTLSConfig_CustomCertificateChainAndPrivateKey() throws {
+    var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults
+    grpcTLSConfig.certificateChain = [
+      .bytes(Array(Self.samplePemCert.utf8), format: .pem)
+    ]
+    grpcTLSConfig.privateKey = .bytes(Array(Self.samplePemKey.utf8), format: .pem)
+    let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig)
+
+    XCTAssertEqual(
+      nioSSLTLSConfig.certificateChain,
+      [
+        .certificate(
+          try NIOSSLCertificate(
+            bytes: Array(Self.samplePemCert.utf8),
+            format: .pem
+          )
+        )
+      ]
+    )
+    XCTAssertEqual(
+      nioSSLTLSConfig.privateKey,
+      .privateKey(try NIOSSLPrivateKey(bytes: Array(Self.samplePemKey.utf8), format: .pem))
+    )
+    XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12)
+    XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification)
+    XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default)
+    XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
+  }
+
+  func testClientTLSConfig_CustomTrustRoots() throws {
+    var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults
+    grpcTLSConfig.trustRoots = .certificates([.bytes(Array(Self.samplePemCert.utf8), format: .pem)])
+    let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig)
+
+    XCTAssertEqual(nioSSLTLSConfig.certificateChain, [])
+    XCTAssertNil(nioSSLTLSConfig.privateKey)
+    XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12)
+    XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .fullVerification)
+    XCTAssertEqual(
+      nioSSLTLSConfig.trustRoots,
+      .certificates(try NIOSSLCertificate.fromPEMBytes(Array(Self.samplePemCert.utf8)))
+    )
+    XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
+  }
+
+  func testClientTLSConfig_CustomCertificateVerification() throws {
+    var grpcTLSConfig = HTTP2ClientTransport.Posix.Config.TLS.defaults
+    grpcTLSConfig.serverCertificateVerification = .noHostnameVerification
+    let nioSSLTLSConfig = try TLSConfiguration(grpcTLSConfig)
+
+    XCTAssertEqual(nioSSLTLSConfig.certificateChain, [])
+    XCTAssertNil(nioSSLTLSConfig.privateKey)
+    XCTAssertEqual(nioSSLTLSConfig.minimumTLSVersion, .tlsv12)
+    XCTAssertEqual(nioSSLTLSConfig.certificateVerification, .noHostnameVerification)
+    XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default)
+    XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
+  }
   #endif
 }

+ 1 - 1
Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift

@@ -199,7 +199,7 @@ final class HTTP2TransportTests: XCTestCase {
       serviceConfig.loadBalancingConfig = [.roundRobin]
       transport = try HTTP2ClientTransport.Posix(
         target: target,
-        config: .defaults {
+        config: .defaults(transportSecurity: .plaintext) {
           $0.compression.algorithm = compression
           $0.compression.enabledAlgorithms = enabledCompression
         },