Przeglądaj źródła

Expose the peerCertificate from mTLS to ServerContext (#97)

Motivation:

When using gRPC, it's useful to pull the peerCertificate from a
successful mTLS connection for authn and/or authz.

Modifications:

These changes make use of the new transportSpecific field in the
ServerContext. After a successful hanshake has completed, it uses the
provided handler to retrieve the verified peer certificate and populate
the Server Context.

There's also a simple happy path test that's been added for validation
purposes.

Temporarily, this commit also references specific updates in
swift-nio-ssl and grpc-swift.

Result:

This commit allows access to the swift Certificate object in an
interceptor for the Posix HTTP2 transport. It also adds a mechanism for
other transports to allow their own transport specific data.

---------

Co-authored-by: George Barnett <gbarnett@apple.com>
Jeff Davey 6 miesięcy temu
rodzic
commit
5d0c90b182

+ 8 - 2
Package.swift

@@ -35,7 +35,7 @@ let products: [Product] = [
 let dependencies: [Package.Dependency] = [
   .package(
     url: "https://github.com/grpc/grpc-swift.git",
-    from: "2.0.0"
+    from: "2.2.0"
   ),
   .package(
     url: "https://github.com/apple/swift-nio.git",
@@ -51,7 +51,7 @@ let dependencies: [Package.Dependency] = [
   ),
   .package(
     url: "https://github.com/apple/swift-nio-ssl.git",
-    from: "2.29.0"
+    from: "2.31.0"
   ),
   .package(
     url: "https://github.com/apple/swift-nio-extras.git",
@@ -61,6 +61,10 @@ let dependencies: [Package.Dependency] = [
     url: "https://github.com/apple/swift-certificates.git",
     from: "1.5.0"
   ),
+  .package(
+    url: "https://github.com/apple/swift-asn1.git",
+    from: "1.0.0"
+  ),
 ]
 
 let defaultSwiftSettings: [SwiftSetting] = [
@@ -111,6 +115,8 @@ let targets: [Target] = [
       .product(name: "GRPCCore", package: "grpc-swift"),
       .product(name: "NIOPosix", package: "swift-nio"),
       .product(name: "NIOSSL", package: "swift-nio-ssl"),
+      .product(name: "X509", package: "swift-certificates"),
+      .product(name: "SwiftASN1", package: "swift-asn1"),
     ],
     swiftSettings: defaultSwiftSettings
   ),

+ 15 - 2
Sources/GRPCNIOTransportCore/Server/CommonHTTP2ServerTransport.swift

@@ -33,6 +33,10 @@ package final class CommonHTTP2ServerTransport<
   private let listeningAddressState: Mutex<State>
   private let serverQuiescingHelper: ServerQuiescingHelper
   private let factory: ListenerFactory
+  private let transportSpecificContext:
+    (
+      @Sendable (any Channel) async -> any ServerContext.TransportSpecific
+    )?
 
   private enum State {
     case idle(EventLoopPromise<SocketAddress>)
@@ -132,7 +136,10 @@ package final class CommonHTTP2ServerTransport<
     address: SocketAddress,
     eventLoopGroup: any EventLoopGroup,
     quiescingHelper: ServerQuiescingHelper,
-    listenerFactory: ListenerFactory
+    listenerFactory: ListenerFactory,
+    transportSpecificContext: (
+      @Sendable (any Channel) async -> any ServerContext.TransportSpecific
+    )? = nil
   ) {
     self.eventLoopGroup = eventLoopGroup
     self.address = address
@@ -142,6 +149,7 @@ package final class CommonHTTP2ServerTransport<
 
     self.factory = listenerFactory
     self.serverQuiescingHelper = quiescingHelper
+    self.transportSpecificContext = transportSpecificContext
   }
 
   package func listen(
@@ -219,6 +227,7 @@ package final class CommonHTTP2ServerTransport<
             group.addTask {
               await self.handleStream(
                 stream,
+                connection,
                 handler: streamHandler,
                 descriptor: descriptor,
                 remotePeer: remotePeer,
@@ -235,6 +244,7 @@ package final class CommonHTTP2ServerTransport<
 
   private func handleStream(
     _ stream: NIOAsyncChannel<RPCRequestPart<Bytes>, RPCResponsePart<Bytes>>,
+    _ connection: NIOAsyncChannel<HTTP2Frame, HTTP2Frame>,
     handler streamHandler: @escaping @Sendable (
       _ stream: RPCStream<Inbound, Outbound>,
       _ context: ServerContext
@@ -279,12 +289,15 @@ package final class CommonHTTP2ServerTransport<
           )
         )
 
-        let context = ServerContext(
+        var context = ServerContext(
           descriptor: descriptor,
           remotePeer: remotePeer,
           localPeer: localPeer,
           cancellation: handle
         )
+        if let transportSpecificContext = self.transportSpecificContext {
+          context.transportSpecific = await transportSpecificContext(connection.channel)
+        }
         await streamHandler(rpcStream, context)
       }
     }

+ 23 - 1
Sources/GRPCNIOTransportHTTP2Posix/HTTP2ServerTransport+Posix.swift

@@ -21,7 +21,9 @@ internal import NIOExtras
 internal import NIOHTTP2
 public import NIOPosix  // has to be public because of default argument value in init
 private import NIOSSL
+private import SwiftASN1
 private import Synchronization
+public import X509
 
 extension HTTP2ServerTransport {
   /// A `ServerTransport` using HTTP/2 built on top of `NIOPosix`.
@@ -167,7 +169,18 @@ extension HTTP2ServerTransport {
         eventLoopGroup: eventLoopGroup,
         quiescingHelper: helper,
         listenerFactory: factory
-      )
+      ) { channel in
+        var context = HTTP2ServerTransport.Posix.Context()
+        do {
+          if let peerCert = try await channel.nioSSL_peerCertificate().get() {
+            let serialized = try peerCert.toDERBytes()
+            let swiftCert = try Certificate(derEncoded: serialized)
+            context.peerCertificate = swiftCert
+          }
+        } catch {}
+
+        return context
+      }
     }
 
     public func listen(
@@ -186,6 +199,15 @@ extension HTTP2ServerTransport {
 }
 
 extension HTTP2ServerTransport.Posix {
+  /// Context for Posix TransportSpecific
+  public struct Context: ServerContext.TransportSpecific {
+    /// The peer certificate (if any) from the mTLS handshake
+    public var peerCertificate: Certificate?
+
+    public init() {
+    }
+  }
+
   /// Config for the `Posix` transport.
   public struct Config: Sendable {
     /// Compression configuration.

+ 59 - 1
Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift

@@ -19,6 +19,7 @@ import GRPCCore
 import GRPCNIOTransportHTTP2Posix
 import GRPCNIOTransportHTTP2TransportServices
 import NIOSSL
+import SwiftASN1
 import Testing
 
 #if canImport(Network)
@@ -58,6 +59,58 @@ struct HTTP2TransportTLSEnabledTests {
     }
   }
 
+  final class TransportSpecificInterceptor: ServerInterceptor {
+    let clientCert: [UInt8]
+    init(_ clientCert: [UInt8]) {
+      self.clientCert = clientCert
+    }
+    func intercept<Input, Output>(
+      request: GRPCCore.StreamingServerRequest<Input>,
+      context: GRPCCore.ServerContext,
+      next: @Sendable (GRPCCore.StreamingServerRequest<Input>, GRPCCore.ServerContext) async throws
+        -> GRPCCore.StreamingServerResponse<Output>
+    ) async throws -> GRPCCore.StreamingServerResponse<Output>
+    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]
+  )
+  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.supported,
@@ -473,6 +526,7 @@ struct HTTP2TransportTLSEnabledTests {
   func withClientAndServer(
     clientConfig: ClientConfig,
     serverConfig: ServerConfig,
+    interceptors: [any ServerInterceptor] = [],
     _ test: (ControlClient<NIOClientTransport>) async throws -> Void
   ) async throws {
     let serverTransport: NIOServerTransport
@@ -497,7 +551,11 @@ struct HTTP2TransportTLSEnabledTests {
     #endif
     }
 
-    try await withGRPCServer(transport: serverTransport, services: [ControlService()]) { server in
+    try await withGRPCServer(
+      transport: serverTransport,
+      services: [ControlService()],
+      interceptors: interceptors
+    ) { server in
       guard let address = try await server.listeningAddress?.ipv4 else {
         throw TLSEnabledTestsError.unexpectedListeningAddress
       }