Эх сурвалжийг харах

Add support for TLS configuration in H2 Posix server transport (#1999)

Motivation:

We currently have a NIOPosix server 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 server transport for gRPC v2.
It also adds support for ALPN, to validate that the negotiated protocol,
if required, is HTTP2 or `grpc-exp`. If it's not, an error will be
fired/the channel will be closed, since we don't support H1.

Result:

We now support TLS/ALPN when using the NIOPosix server transport in gRPC
V2.
Gustavo Cairo 1 жил өмнө
parent
commit
f7ecfedc8c

+ 5 - 3
Sources/GRPCHTTP2Core/Internal/NIOChannelPipeline+GRPC.swift

@@ -32,7 +32,8 @@ extension ChannelPipeline.SynchronousOperations {
     connectionConfig: HTTP2ServerTransport.Config.Connection,
     http2Config: HTTP2ServerTransport.Config.HTTP2,
     rpcConfig: HTTP2ServerTransport.Config.RPC,
-    useTLS: Bool
+    requireALPN: Bool,
+    scheme: Scheme
   ) throws -> (HTTP2ConnectionChannel, HTTP2StreamMultiplexer) {
     let serverConnectionHandler = ServerConnectionManagementHandler(
       eventLoop: self.eventLoop,
@@ -44,7 +45,8 @@ extension ChannelPipeline.SynchronousOperations {
       allowKeepaliveWithoutCalls: connectionConfig.keepalive.clientBehavior.allowWithoutCalls,
       minPingIntervalWithoutCalls: TimeAmount(
         connectionConfig.keepalive.clientBehavior.minPingIntervalWithoutCalls
-      )
+      ),
+      requireALPN: requireALPN
     )
     let flushNotificationHandler = GRPCServerFlushNotificationHandler(
       serverConnectionManagementHandler: serverConnectionHandler
@@ -81,7 +83,7 @@ extension ChannelPipeline.SynchronousOperations {
       return streamChannel.eventLoop.makeCompletedFuture {
         let methodDescriptorPromise = streamChannel.eventLoop.makePromise(of: MethodDescriptor.self)
         let streamHandler = GRPCServerStreamHandler(
-          scheme: useTLS ? .https : .http,
+          scheme: scheme,
           acceptedEncodings: compressionConfig.enabledAlgorithms,
           maximumPayloadSize: rpcConfig.maxRequestPayloadSize,
           methodDescriptorPromise: methodDescriptorPromise

+ 23 - 0
Sources/GRPCHTTP2Core/Server/Connection/ServerConnectionManagementHandler.swift

@@ -14,8 +14,10 @@
  * limitations under the License.
  */
 
+internal import GRPCCore
 internal import NIOCore
 internal import NIOHTTP2
+internal import NIOTLS
 
 /// A `ChannelHandler` which manages the lifecycle of a gRPC connection over HTTP/2.
 ///
@@ -71,6 +73,7 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler {
 
   /// Whether a flush is pending.
   private var flushPending: Bool
+
   /// Whether `channelRead` has been called and `channelReadComplete` hasn't yet been called.
   /// Resets once `channelReadComplete` returns.
   private var inReadLoop: Bool
@@ -84,6 +87,11 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler {
   /// The clock.
   private let clock: Clock
 
+  /// Whether ALPN is required.
+  /// If it is but the TLS handshake finished without negotiating a protocol, an error will be fired down the
+  /// pipeline and the channel will be closed.
+  private let requireALPN: Bool
+
   /// A clock providing the current time.
   ///
   /// This is necessary for testing where a manual clock can be used and advanced from the test.
@@ -209,6 +217,7 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler {
     keepaliveTimeout: TimeAmount?,
     allowKeepaliveWithoutCalls: Bool,
     minPingIntervalWithoutCalls: TimeAmount,
+    requireALPN: Bool,
     clock: Clock = .nio
   ) {
     self.eventLoop = eventLoop
@@ -235,6 +244,8 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler {
     self.inReadLoop = false
     self.clock = clock
     self.frameStats = FrameStats()
+
+    self.requireALPN = requireALPN
   }
 
   func handlerAdded(context: ChannelHandlerContext) {
@@ -284,6 +295,18 @@ final class ServerConnectionManagementHandler: ChannelDuplexHandler {
     case is ChannelShouldQuiesceEvent:
       self.initiateGracefulShutdown(context: context)
 
+    case TLSUserEvent.handshakeCompleted(let negotiatedProtocol):
+      if negotiatedProtocol == nil, self.requireALPN {
+        // No ALPN protocol negotiated but it was required: fire an error and close the channel.
+        context.fireErrorCaught(
+          RPCError(
+            code: .internalError,
+            message: "ALPN resulted in no protocol being negotiated, but it was required."
+          )
+        )
+        context.close(mode: .all, promise: nil)
+      }
+
     default:
       ()
     }

+ 55 - 6
Sources/GRPCHTTP2TransportNIOPosix/HTTP2ServerTransport+Posix.swift

@@ -21,6 +21,10 @@ internal import NIOExtras
 internal import NIOHTTP2
 public import NIOPosix  // has to be public because of default argument value in init
 
+#if canImport(NIOSSL)
+import NIOSSL
+#endif
+
 extension HTTP2ServerTransport {
   /// A NIOPosix-backed implementation of a server transport.
   @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
@@ -150,7 +154,7 @@ extension HTTP2ServerTransport {
     ///   - eventLoopGroup: The ELG from which to get ELs to run this transport.
     public init(
       address: GRPCHTTP2Core.SocketAddress,
-      config: Config = .defaults,
+      config: Config,
       eventLoopGroup: MultiThreadedEventLoopGroup = .singletonMultiThreadedEventLoopGroup
     ) {
       self.address = address
@@ -174,6 +178,24 @@ extension HTTP2ServerTransport {
         }
       }
 
+      #if canImport(NIOSSL)
+      let nioSSLContext: NIOSSLContext?
+      switch self.config.transportSecurity.wrapped {
+      case .plaintext:
+        nioSSLContext = nil
+      case .tls(let tlsConfig):
+        do {
+          nioSSLContext = try NIOSSLContext(configuration: TLSConfiguration(tlsConfig))
+        } catch {
+          throw RuntimeError(
+            code: .transportError,
+            message: "Couldn't create SSL context, check your TLS configuration.",
+            cause: error
+          )
+        }
+      }
+      #endif
+
       let serverChannel = try await ServerBootstrap(group: self.eventLoopGroup)
         .serverChannelOption(
           ChannelOptions.socketOption(.so_reuseaddr),
@@ -187,13 +209,33 @@ extension HTTP2ServerTransport {
         }
         .bind(to: self.address) { channel in
           channel.eventLoop.makeCompletedFuture {
+            #if canImport(NIOSSL)
+            if let nioSSLContext {
+              try channel.pipeline.syncOperations.addHandler(
+                NIOSSLServerHandler(context: nioSSLContext)
+              )
+            }
+            #endif
+
+            let requireALPN: Bool
+            let scheme: Scheme
+            switch self.config.transportSecurity.wrapped {
+            case .plaintext:
+              requireALPN = false
+              scheme = .http
+            case .tls(let tlsConfig):
+              requireALPN = tlsConfig.requireALPN
+              scheme = .https
+            }
+
             return try channel.pipeline.syncOperations.configureGRPCServerPipeline(
               channel: channel,
               compressionConfig: self.config.compression,
               connectionConfig: self.config.connection,
               http2Config: self.config.http2,
               rpcConfig: self.config.rpc,
-              useTLS: false
+              requireALPN: requireALPN,
+              scheme: scheme
             )
           }
         }
@@ -289,7 +331,6 @@ extension HTTP2ServerTransport {
       self.serverQuiescingHelper.initiateShutdown(promise: nil)
     }
   }
-
 }
 
 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
@@ -304,6 +345,8 @@ extension HTTP2ServerTransport.Posix {
     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:
@@ -311,25 +354,31 @@ extension HTTP2ServerTransport.Posix {
     ///   - connection: Connection configuration.
     ///   - http2: HTTP2 configuration.
     ///   - rpc: RPC configuration.
+    ///   - transportSecurity: The transport's security configuration.
     public init(
       compression: HTTP2ServerTransport.Config.Compression,
       connection: HTTP2ServerTransport.Config.Connection,
       http2: HTTP2ServerTransport.Config.HTTP2,
-      rpc: HTTP2ServerTransport.Config.RPC
+      rpc: HTTP2ServerTransport.Config.RPC,
+      transportSecurity: TransportSecurity
     ) {
       self.compression = compression
       self.connection = connection
       self.http2 = http2
       self.rpc = rpc
+      self.transportSecurity = transportSecurity
     }
 
     /// Default values for the different configurations.
-    public static var defaults: Self {
+    public static func defaults(
+      transportSecurity: TransportSecurity
+    ) -> Self {
       Self(
         compression: .defaults,
         connection: .defaults,
         http2: .defaults,
-        rpc: .defaults
+        rpc: .defaults,
+        transportSecurity: transportSecurity
       )
     }
   }

+ 148 - 0
Sources/GRPCHTTP2TransportNIOPosix/NIOSSL+GRPC.swift

@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+#if canImport(NIOSSL)
+import NIOSSL
+
+extension NIOSSLSerializationFormats {
+  fileprivate init(_ format: TLSConfig.SerializationFormat) {
+    switch format.wrapped {
+    case .pem:
+      self = .pem
+    case .der:
+      self = .der
+    }
+  }
+}
+
+extension Sequence<TLSConfig.CertificateSource> {
+  func sslCertificateSources() throws -> [NIOSSLCertificateSource] {
+    var certificateSources: [NIOSSLCertificateSource] = []
+    for source in self {
+      switch source.wrapped {
+      case .bytes(let bytes, let serializationFormat):
+        switch serializationFormat.wrapped {
+        case .der:
+          certificateSources.append(
+            .certificate(try NIOSSLCertificate(bytes: bytes, format: .der))
+          )
+
+        case .pem:
+          let certificates = try NIOSSLCertificate.fromPEMBytes(bytes).map {
+            NIOSSLCertificateSource.certificate($0)
+          }
+          certificateSources.append(contentsOf: certificates)
+        }
+
+      case .file(let path, let serializationFormat):
+        switch serializationFormat.wrapped {
+        case .der:
+          certificateSources.append(
+            .certificate(try NIOSSLCertificate(file: path, format: .der))
+          )
+
+        case .pem:
+          let certificates = try NIOSSLCertificate.fromPEMFile(path).map {
+            NIOSSLCertificateSource.certificate($0)
+          }
+          certificateSources.append(contentsOf: certificates)
+        }
+      }
+    }
+    return certificateSources
+  }
+}
+
+extension NIOSSLPrivateKey {
+  fileprivate convenience init(
+    privateKey source: TLSConfig.PrivateKeySource
+  ) throws {
+    switch source.wrapped {
+    case .file(let path, let serializationFormat):
+      try self.init(
+        file: path,
+        format: NIOSSLSerializationFormats(serializationFormat)
+      )
+    case .bytes(let bytes, let serializationFormat):
+      try self.init(
+        bytes: bytes,
+        format: NIOSSLSerializationFormats(serializationFormat)
+      )
+    }
+  }
+}
+
+extension NIOSSLTrustRoots {
+  fileprivate init(_ trustRoots: TLSConfig.TrustRootsSource) throws {
+    switch trustRoots.wrapped {
+    case .certificates(let certificateSources):
+      let certificates = try certificateSources.map { source in
+        switch source.wrapped {
+        case .bytes(let bytes, let serializationFormat):
+          return try NIOSSLCertificate(
+            bytes: bytes,
+            format: NIOSSLSerializationFormats(serializationFormat)
+          )
+        case .file(let path, let serializationFormat):
+          return try NIOSSLCertificate(
+            file: path,
+            format: NIOSSLSerializationFormats(serializationFormat)
+          )
+        }
+      }
+      self = .certificates(certificates)
+
+    case .systemDefault:
+      self = .default
+    }
+  }
+}
+
+extension CertificateVerification {
+  fileprivate init(
+    _ verificationMode: TLSConfig.CertificateVerification
+  ) {
+    switch verificationMode.wrapped {
+    case .doNotVerify:
+      self = .none
+    case .fullVerification:
+      self = .fullVerification
+    case .noHostnameVerification:
+      self = .noHostnameVerification
+    }
+  }
+}
+
+extension TLSConfiguration {
+  @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+  package init(_ tlsConfig: HTTP2ServerTransport.Posix.Config.TLS) throws {
+    let certificateChain = try tlsConfig.certificateChain.sslCertificateSources()
+    let privateKey = try NIOSSLPrivateKey(privateKey: tlsConfig.privateKey)
+
+    var tlsConfiguration = TLSConfiguration.makeServerConfiguration(
+      certificateChain: certificateChain,
+      privateKey: .privateKey(privateKey)
+    )
+    tlsConfiguration.minimumTLSVersion = .tlsv12
+    tlsConfiguration.certificateVerification = CertificateVerification(
+      tlsConfig.clientCertificateVerification
+    )
+    tlsConfiguration.trustRoots = try NIOSSLTrustRoots(tlsConfig.trustRoots)
+    tlsConfiguration.applicationProtocols = ["grpc-exp", "h2"]
+
+    self = tlsConfiguration
+  }
+}
+#endif

+ 219 - 0
Sources/GRPCHTTP2TransportNIOPosix/TLSConfig.swift

@@ -0,0 +1,219 @@
+/*
+ * 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.
+ */
+
+public enum TLSConfig: Sendable {
+  /// The serialization format of the provided certificates and private keys.
+  public struct SerializationFormat: Sendable, Equatable {
+    package enum Wrapped {
+      case pem
+      case der
+    }
+
+    package let wrapped: Wrapped
+
+    public static let pem = Self(wrapped: .pem)
+    public static let der = Self(wrapped: .der)
+  }
+
+  /// A description of where a certificate is coming from: either a byte array or a file.
+  /// The serialization format is specified by ``TLSConfig/SerializationFormat``.
+  public struct CertificateSource: Sendable {
+    package enum Wrapped {
+      case file(path: String, format: SerializationFormat)
+      case bytes(bytes: [UInt8], format: SerializationFormat)
+    }
+
+    package let wrapped: Wrapped
+
+    /// The certificate's source is a file.
+    /// - Parameters:
+    ///   - path: The file path containing the certificate.
+    ///   - format: The certificate's format, as a ``TLSConfig/SerializationFormat``.
+    /// - Returns: A source describing the certificate source is the given file.
+    public static func file(path: String, format: SerializationFormat) -> Self {
+      Self(wrapped: .file(path: path, format: format))
+    }
+
+    /// The certificate's source is an array of bytes.
+    /// - Parameters:
+    ///   - bytes: The array of bytes making up the certificate.
+    ///   - format: The certificate's format, as a ``TLSConfig/SerializationFormat``.
+    /// - Returns: A source describing the certificate source is the given bytes.
+    public static func bytes(_ bytes: [UInt8], format: SerializationFormat) -> Self {
+      Self(wrapped: .bytes(bytes: bytes, format: format))
+    }
+  }
+
+  /// A description of where the private key is coming from: either a byte array or a file.
+  /// The serialization format is specified by ``TLSConfig/SerializationFormat``.
+  public struct PrivateKeySource: Sendable {
+    package enum Wrapped {
+      case file(path: String, format: SerializationFormat)
+      case bytes(bytes: [UInt8], format: SerializationFormat)
+    }
+
+    package let wrapped: Wrapped
+
+    /// The private key's source is a file.
+    /// - Parameters:
+    ///   - path: The file path containing the private key.
+    ///   - format: The private key's format, as a ``TLSConfig/SerializationFormat``.
+    /// - Returns: A source describing the private key source is the given file.
+    public static func file(path: String, format: SerializationFormat) -> Self {
+      Self(wrapped: .file(path: path, format: format))
+    }
+
+    /// The private key's source is an array of bytes.
+    /// - Parameters:
+    ///   - bytes: The array of bytes making up the private key.
+    ///   - format: The private key's format, as a ``TLSConfig/SerializationFormat``.
+    /// - Returns: A source describing the private key source is the given bytes.
+    public static func bytes(
+      _ bytes: [UInt8],
+      format: SerializationFormat
+    ) -> Self {
+      Self(wrapped: .bytes(bytes: bytes, format: format))
+    }
+  }
+
+  /// A description of where the trust roots are coming from: either a custom certificate chain, or the system default trust store.
+  public struct TrustRootsSource: Sendable {
+    package enum Wrapped {
+      case certificates([CertificateSource])
+      case systemDefault
+    }
+
+    package let wrapped: Wrapped
+
+    /// A list of ``TLSConfig/CertificateSource``s making up the
+    /// chain of trust.
+    /// - Parameter certificateSources: The sources for the certificates that make up the chain of trust.
+    /// - Returns: A trust root for the given chain of trust.
+    public static func certificates(
+      _ certificateSources: [CertificateSource]
+    ) -> Self {
+      Self(wrapped: .certificates(certificateSources))
+    }
+
+    /// The system default trust store.
+    public static let systemDefault: Self = Self(wrapped: .systemDefault)
+  }
+
+  /// How to verify client certificates.
+  public struct CertificateVerification: Sendable {
+    package enum Wrapped {
+      case doNotVerify
+      case fullVerification
+      case noHostnameVerification
+    }
+
+    package let wrapped: Wrapped
+
+    /// All certificate verification disabled.
+    public static let noVerification: Self = Self(wrapped: .doNotVerify)
+
+    /// Certificates will be validated against the trust store, but will not be checked to see if they are valid for the given hostname.
+    public static let noHostnameVerification: Self = Self(wrapped: .noHostnameVerification)
+
+    /// Certificates will be validated against the trust store and checked against the hostname of the service we are contacting.
+    public static let fullVerification: Self = Self(wrapped: .fullVerification)
+  }
+}
+
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+extension HTTP2ServerTransport.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)
+
+    /// This connection will use TLS.
+    public static func tls(_ tls: TLS) -> Self {
+      Self(wrapped: .tls(tls))
+    }
+  }
+
+  public struct TLS: Sendable {
+    /// The certificates the server 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 client certificate, if one is presented.
+    public var clientCertificateVerification: TLSConfig.CertificateVerification
+
+    /// The trust roots to be used when verifying client certificates.
+    public var trustRoots: TLSConfig.TrustRootsSource
+
+    /// Whether ALPN is required.
+    ///
+    /// 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 transport TLS config, with some values defaulted:
+    /// - `clientCertificateVerificationMode` equals `doNotVerify`
+    /// - `trustRoots` equals `systemDefault`
+    /// - `requireALPN` equals `false`
+    ///
+    /// - Parameters:
+    ///   - certificateChain: The certificates the server 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 defaults(
+      certificateChain: [TLSConfig.CertificateSource],
+      privateKey: TLSConfig.PrivateKeySource
+    ) -> Self {
+      Self.init(
+        certificateChain: certificateChain,
+        privateKey: privateKey,
+        clientCertificateVerification: .noVerification,
+        trustRoots: .systemDefault,
+        requireALPN: false
+      )
+    }
+
+    /// Create a new HTTP2 NIO Posix transport TLS config, with some values defaulted to match
+    /// the requirements of mTLS:
+    /// - `clientCertificateVerificationMode` equals `noHostnameVerification`
+    /// - `trustRoots` equals `systemDefault`
+    /// - `requireALPN` equals `false`
+    ///
+    /// - Parameters:
+    ///   - certificateChain: The certificates the server 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.init(
+        certificateChain: certificateChain,
+        privateKey: privateKey,
+        clientCertificateVerification: .noHostnameVerification,
+        trustRoots: .systemDefault,
+        requireALPN: false
+      )
+    }
+  }
+}

+ 2 - 1
Sources/GRPCHTTP2TransportNIOTransportServices/HTTP2ServerTransport+TransportServices.swift

@@ -188,7 +188,8 @@ extension HTTP2ServerTransport {
               connectionConfig: self.config.connection,
               http2Config: self.config.http2,
               rpcConfig: self.config.rpc,
-              useTLS: false
+              requireALPN: false,
+              scheme: .http
             )
           }
         }

+ 1 - 0
Tests/GRPCHTTP2CoreTests/Server/Connection/ServerConnectionManagementHandlerTests.swift

@@ -361,6 +361,7 @@ extension ServerConnectionManagementHandlerTests {
         keepaliveTimeout: keepaliveTimeout,
         allowKeepaliveWithoutCalls: allowKeepaliveWithoutCalls,
         minPingIntervalWithoutCalls: minPingIntervalWithoutCalls,
+        requireALPN: false,
         clock: self.clock
       )
 

+ 235 - 6
Tests/GRPCHTTP2TransportTests/HTTP2TransportNIOPosixTests.swift

@@ -19,11 +19,16 @@ private import GRPCHTTP2Core
 private import GRPCHTTP2TransportNIOPosix
 internal import XCTest
 
+#if canImport(NIOSSL)
+private import NIOSSL
+#endif
+
 @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
 final class HTTP2TransportNIOPosixTests: XCTestCase {
   func testGetListeningAddress_IPv4() async throws {
     let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix(
-      address: .ipv4(host: "0.0.0.0", port: 0)
+      address: .ipv4(host: "0.0.0.0", port: 0),
+      config: .defaults(transportSecurity: .plaintext)
     )
 
     try await withThrowingDiscardingTaskGroup { group in
@@ -42,7 +47,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
 
   func testGetListeningAddress_IPv6() async throws {
     let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix(
-      address: .ipv6(host: "::1", port: 0)
+      address: .ipv6(host: "::1", port: 0),
+      config: .defaults(transportSecurity: .plaintext)
     )
 
     try await withThrowingDiscardingTaskGroup { group in
@@ -61,7 +67,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
 
   func testGetListeningAddress_UnixDomainSocket() async throws {
     let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix(
-      address: .unixDomainSocket(path: "/tmp/posix-uds-test")
+      address: .unixDomainSocket(path: "/tmp/posix-uds-test"),
+      config: .defaults(transportSecurity: .plaintext)
     )
 
     try await withThrowingDiscardingTaskGroup { group in
@@ -84,7 +91,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
     try XCTSkipUnless(self.vsockAvailable(), "Vsock unavailable")
 
     let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix(
-      address: .vsock(contextID: .any, port: .any)
+      address: .vsock(contextID: .any, port: .any),
+      config: .defaults(transportSecurity: .plaintext)
     )
 
     try await withThrowingDiscardingTaskGroup { group in
@@ -102,7 +110,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
 
   func testGetListeningAddress_InvalidAddress() async {
     let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix(
-      address: .unixDomainSocket(path: "/this/should/be/an/invalid/path")
+      address: .unixDomainSocket(path: "/this/should/be/an/invalid/path"),
+      config: .defaults(transportSecurity: .plaintext)
     )
 
     try? await withThrowingDiscardingTaskGroup { group in
@@ -130,7 +139,8 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
 
   func testGetListeningAddress_StoppedListening() async throws {
     let transport = GRPCHTTP2Core.HTTP2ServerTransport.Posix(
-      address: .ipv4(host: "0.0.0.0", port: 0)
+      address: .ipv4(host: "0.0.0.0", port: 0),
+      config: .defaults(transportSecurity: .plaintext)
     )
 
     try? await withThrowingDiscardingTaskGroup { group in
@@ -159,4 +169,223 @@ final class HTTP2TransportNIOPosixTests: XCTestCase {
       }
     }
   }
+
+  #if canImport(NIOSSL)
+  static let samplePemCert = """
+    -----BEGIN CERTIFICATE-----
+    MIIGGzCCBAOgAwIBAgIJAJ/X0Fo0ynmEMA0GCSqGSIb3DQEBCwUAMIGjMQswCQYD
+    VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5z
+    b2t5bzEuMCwGA1UECgwlU2FuIEZyYW5zb2t5byBJbnN0aXR1dGUgb2YgVGVjaG5v
+    bG9neTEVMBMGA1UECwwMUm9ib3RpY3MgTGFiMSAwHgYDVQQDDBdyb2JvdHMuc2Fu
+    ZnJhbnNva3lvLmVkdTAeFw0xNzEwMTYyMTAxMDJaFw00NzEwMDkyMTAxMDJaMIGj
+    MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu
+    IEZyYW5zb2t5bzEuMCwGA1UECgwlU2FuIEZyYW5zb2t5byBJbnN0aXR1dGUgb2Yg
+    VGVjaG5vbG9neTEVMBMGA1UECwwMUm9ib3RpY3MgTGFiMSAwHgYDVQQDDBdyb2Jv
+    dHMuc2FuZnJhbnNva3lvLmVkdTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
+    ggIBAO9rzJOOE8cmsIqAJMCrHDxkBAMgZhMsJ863MnWtVz5JIJK6CKI/Nu26tEzo
+    kHy3EI9565RwikvauheMsWaTFA4PD/P+s1DtxRCGIcK5x+SoTN7Drn5ZueoJNZRf
+    TYuN+gwyhprzrZrYjXpvEVPYuSIeUqK5XGrTyFA2uGj9wY3f9IF4rd7JT0ewRb1U
+    8OcR7xQbXKGjkY4iJE1TyfmIsBZboKaG/aYa9KbnWyTkDssaELWUIKrjwwuPgVgS
+    vlAYmo12MlsGEzkO9z78jvFmhUOsaEldM8Ua2AhOKW0oSYgauVuro/Ap/o5zn8PD
+    IDapl9g+5vjN2LucqX2a9utoFvxSKXT4NvfpL9fJvzdBNMM4xpqtHIkV0fkiMbWk
+    EW2FFlOXKnIJV8wT4a9iduuIDMg8O7oc+gt9pG9MHTWthXm4S29DARTqfZ48bW77
+    z8RrEURV03o05b/twuAJSRyyOCUi61yMo3YNytebjY2W3Pxqpq+YmT5qhqBZDLlT
+    LMptuFdISv6SQgg7JoFHGMWRXUavMj/sn5qZD4pQyZToHJ2Vtg5W/MI1pKwc3oKD
+    6M3/7Gf35r92V/ox6XT7+fnEsAH8AtQiZJkEbvzJ5lpUihSIaV3a/S+jnk7Lw8Tp
+    vjtpfjOg+wBblc38Oa9tk2WdXwYDbnvbeL26WmyHwQTUBi1jAgMBAAGjUDBOMB0G
+    A1UdDgQWBBToPRmTBQEF5F5LcPiUI5qBNPBU+DAfBgNVHSMEGDAWgBToPRmTBQEF
+    5F5LcPiUI5qBNPBU+DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCY
+    gxM5lufF2lTB9sH0s1E1VTERv37qoapNP+aw06oZkAD67QOTXFzbsM3JU1diY6rV
+    Y0g9CLzRO7gZY+kmi1WWnsYiMMSIGjIfsB8S+ot43LME+AJXPVeDZQnoZ6KQ/9r+
+    71Umi4AKLoZ9dInyUIM3EHg9pg5B0eEINrh4J+OPGtlC3NMiWxdmIkZwzfXa+64Z
+    8k5aX5piMTI+9BQSMWw5l7tFT/PISuI8b/Ln4IUBXKA0xkONXVnjPOmS0h7MBoc2
+    EipChDKnK+Mtm9GQewOCKdS2nsrCndGkIBnUix4ConUYIoywVzWGMD+9OzKNg76d
+    O6A7MxdjEdKhf1JDvklxInntDUDTlSFL4iEFELwyRseoTzj8vJE+cL6h6ClasYQ6
+    p0EeL3UpICYerfIvPhohftCivCH3k7Q1BSf0fq73cQ55nrFAHrqqYjD7HBeBS9hn
+    3L6bz9Eo6U9cuxX42k3l1N44BmgcDPin0+CRTirEmahUMb3gmvoSZqQ3Cz86GkIg
+    7cNJosc9NyevQlU9SX3ptEbv33tZtlB5GwgZ2hiGBTY0C3HaVFjLpQiSS5ygZLgI
+    /+AKtah7sTHIAtpUH1ZZEgKPl1Hg6J4x/dBkuk3wxPommNHaYaHREXF+fHMhBrSi
+    yH8agBmmECpa21SVnr7vrL+KSqfuF+GxwjSNsSR4SA==
+    -----END CERTIFICATE-----
+    """
+
+  static let samplePemKey = """
+    -----BEGIN RSA PRIVATE KEY-----
+    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
+    -----END RSA PRIVATE KEY-----
+    """
+
+  func testTLSConfig_Defaults() throws {
+    let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults(
+      certificateChain: [
+        .bytes(Array(Self.samplePemCert.utf8), format: .pem)
+      ],
+      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, .none)
+    XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default)
+    XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
+  }
+
+  func testTLSConfig_mTLS() throws {
+    let grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.mTLS(
+      certificateChain: [
+        .bytes(Array(Self.samplePemCert.utf8), format: .pem)
+      ],
+      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, .noHostnameVerification)
+    XCTAssertEqual(nioSSLTLSConfig.trustRoots, .default)
+    XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
+  }
+
+  func testTLSConfig_FullVerifyClient() throws {
+    var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults(
+      certificateChain: [
+        .bytes(Array(Self.samplePemCert.utf8), format: .pem)
+      ],
+      privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem)
+    )
+    grpcTLSConfig.clientCertificateVerification = .fullVerification
+    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 testTLSConfig_CustomTrustRoots() throws {
+    var grpcTLSConfig = HTTP2ServerTransport.Posix.Config.TLS.defaults(
+      certificateChain: [
+        .bytes(Array(Self.samplePemCert.utf8), format: .pem)
+      ],
+      privateKey: .bytes(Array(Self.samplePemKey.utf8), format: .pem)
+    )
+    grpcTLSConfig.trustRoots = .certificates([.bytes(Array(Self.samplePemCert.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, .none)
+    XCTAssertEqual(
+      nioSSLTLSConfig.trustRoots,
+      .certificates(try NIOSSLCertificate.fromPEMBytes(Array(Self.samplePemCert.utf8)))
+    )
+    XCTAssertEqual(nioSSLTLSConfig.applicationProtocols, ["grpc-exp", "h2"])
+  }
+  #endif
 }

+ 1 - 1
Tests/GRPCHTTP2TransportTests/HTTP2TransportTests.swift

@@ -144,7 +144,7 @@ final class HTTP2TransportTests: XCTestCase {
 
     switch kind {
     case .posix:
-      var config = HTTP2ServerTransport.Posix.Config.defaults
+      var config = HTTP2ServerTransport.Posix.Config.defaults(transportSecurity: .plaintext)
       config.compression.enabledAlgorithms = compression
       let transport = HTTP2ServerTransport.Posix(
         address: .ipv4(host: "127.0.0.1", port: 0),