Skip to content

Commit 5d0c90b

Browse files
jtdaveyglbrntt
andauthored
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>
1 parent bb97afe commit 5d0c90b

File tree

4 files changed

+105
-6
lines changed

4 files changed

+105
-6
lines changed

Package.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ let products: [Product] = [
3535
let dependencies: [Package.Dependency] = [
3636
.package(
3737
url: "https://github.com/grpc/grpc-swift.git",
38-
from: "2.0.0"
38+
from: "2.2.0"
3939
),
4040
.package(
4141
url: "https://github.com/apple/swift-nio.git",
@@ -51,7 +51,7 @@ let dependencies: [Package.Dependency] = [
5151
),
5252
.package(
5353
url: "https://github.com/apple/swift-nio-ssl.git",
54-
from: "2.29.0"
54+
from: "2.31.0"
5555
),
5656
.package(
5757
url: "https://github.com/apple/swift-nio-extras.git",
@@ -61,6 +61,10 @@ let dependencies: [Package.Dependency] = [
6161
url: "https://github.com/apple/swift-certificates.git",
6262
from: "1.5.0"
6363
),
64+
.package(
65+
url: "https://github.com/apple/swift-asn1.git",
66+
from: "1.0.0"
67+
),
6468
]
6569

6670
let defaultSwiftSettings: [SwiftSetting] = [
@@ -111,6 +115,8 @@ let targets: [Target] = [
111115
.product(name: "GRPCCore", package: "grpc-swift"),
112116
.product(name: "NIOPosix", package: "swift-nio"),
113117
.product(name: "NIOSSL", package: "swift-nio-ssl"),
118+
.product(name: "X509", package: "swift-certificates"),
119+
.product(name: "SwiftASN1", package: "swift-asn1"),
114120
],
115121
swiftSettings: defaultSwiftSettings
116122
),

Sources/GRPCNIOTransportCore/Server/CommonHTTP2ServerTransport.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ package final class CommonHTTP2ServerTransport<
3333
private let listeningAddressState: Mutex<State>
3434
private let serverQuiescingHelper: ServerQuiescingHelper
3535
private let factory: ListenerFactory
36+
private let transportSpecificContext:
37+
(
38+
@Sendable (any Channel) async -> any ServerContext.TransportSpecific
39+
)?
3640

3741
private enum State {
3842
case idle(EventLoopPromise<SocketAddress>)
@@ -132,7 +136,10 @@ package final class CommonHTTP2ServerTransport<
132136
address: SocketAddress,
133137
eventLoopGroup: any EventLoopGroup,
134138
quiescingHelper: ServerQuiescingHelper,
135-
listenerFactory: ListenerFactory
139+
listenerFactory: ListenerFactory,
140+
transportSpecificContext: (
141+
@Sendable (any Channel) async -> any ServerContext.TransportSpecific
142+
)? = nil
136143
) {
137144
self.eventLoopGroup = eventLoopGroup
138145
self.address = address
@@ -142,6 +149,7 @@ package final class CommonHTTP2ServerTransport<
142149

143150
self.factory = listenerFactory
144151
self.serverQuiescingHelper = quiescingHelper
152+
self.transportSpecificContext = transportSpecificContext
145153
}
146154

147155
package func listen(
@@ -219,6 +227,7 @@ package final class CommonHTTP2ServerTransport<
219227
group.addTask {
220228
await self.handleStream(
221229
stream,
230+
connection,
222231
handler: streamHandler,
223232
descriptor: descriptor,
224233
remotePeer: remotePeer,
@@ -235,6 +244,7 @@ package final class CommonHTTP2ServerTransport<
235244

236245
private func handleStream(
237246
_ stream: NIOAsyncChannel<RPCRequestPart<Bytes>, RPCResponsePart<Bytes>>,
247+
_ connection: NIOAsyncChannel<HTTP2Frame, HTTP2Frame>,
238248
handler streamHandler: @escaping @Sendable (
239249
_ stream: RPCStream<Inbound, Outbound>,
240250
_ context: ServerContext
@@ -279,12 +289,15 @@ package final class CommonHTTP2ServerTransport<
279289
)
280290
)
281291

282-
let context = ServerContext(
292+
var context = ServerContext(
283293
descriptor: descriptor,
284294
remotePeer: remotePeer,
285295
localPeer: localPeer,
286296
cancellation: handle
287297
)
298+
if let transportSpecificContext = self.transportSpecificContext {
299+
context.transportSpecific = await transportSpecificContext(connection.channel)
300+
}
288301
await streamHandler(rpcStream, context)
289302
}
290303
}

Sources/GRPCNIOTransportHTTP2Posix/HTTP2ServerTransport+Posix.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ internal import NIOExtras
2121
internal import NIOHTTP2
2222
public import NIOPosix // has to be public because of default argument value in init
2323
private import NIOSSL
24+
private import SwiftASN1
2425
private import Synchronization
26+
public import X509
2527

2628
extension HTTP2ServerTransport {
2729
/// A `ServerTransport` using HTTP/2 built on top of `NIOPosix`.
@@ -167,7 +169,18 @@ extension HTTP2ServerTransport {
167169
eventLoopGroup: eventLoopGroup,
168170
quiescingHelper: helper,
169171
listenerFactory: factory
170-
)
172+
) { channel in
173+
var context = HTTP2ServerTransport.Posix.Context()
174+
do {
175+
if let peerCert = try await channel.nioSSL_peerCertificate().get() {
176+
let serialized = try peerCert.toDERBytes()
177+
let swiftCert = try Certificate(derEncoded: serialized)
178+
context.peerCertificate = swiftCert
179+
}
180+
} catch {}
181+
182+
return context
183+
}
171184
}
172185

173186
public func listen(
@@ -186,6 +199,15 @@ extension HTTP2ServerTransport {
186199
}
187200

188201
extension HTTP2ServerTransport.Posix {
202+
/// Context for Posix TransportSpecific
203+
public struct Context: ServerContext.TransportSpecific {
204+
/// The peer certificate (if any) from the mTLS handshake
205+
public var peerCertificate: Certificate?
206+
207+
public init() {
208+
}
209+
}
210+
189211
/// Config for the `Posix` transport.
190212
public struct Config: Sendable {
191213
/// Compression configuration.

Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import GRPCCore
1919
import GRPCNIOTransportHTTP2Posix
2020
import GRPCNIOTransportHTTP2TransportServices
2121
import NIOSSL
22+
import SwiftASN1
2223
import Testing
2324

2425
#if canImport(Network)
@@ -58,6 +59,58 @@ struct HTTP2TransportTLSEnabledTests {
5859
}
5960
}
6061

62+
final class TransportSpecificInterceptor: ServerInterceptor {
63+
let clientCert: [UInt8]
64+
init(_ clientCert: [UInt8]) {
65+
self.clientCert = clientCert
66+
}
67+
func intercept<Input, Output>(
68+
request: GRPCCore.StreamingServerRequest<Input>,
69+
context: GRPCCore.ServerContext,
70+
next: @Sendable (GRPCCore.StreamingServerRequest<Input>, GRPCCore.ServerContext) async throws
71+
-> GRPCCore.StreamingServerResponse<Output>
72+
) async throws -> GRPCCore.StreamingServerResponse<Output>
73+
where Input: Sendable, Output: Sendable {
74+
let transportSpecific = context.transportSpecific
75+
let transportSpecificAsPosixContext = try #require(
76+
transportSpecific as? HTTP2ServerTransport.Posix.Context
77+
)
78+
let peerCertificate = try #require(transportSpecificAsPosixContext.peerCertificate)
79+
var derSerializer = DER.Serializer()
80+
try peerCertificate.serialize(into: &derSerializer)
81+
#expect(derSerializer.serializedBytes == self.clientCert)
82+
return try await next(request, context)
83+
}
84+
}
85+
86+
@Test(
87+
"Using the mTLS defaults, and with Posix transport, validate we get the peer cert on the server",
88+
arguments: [TransportKind.posix]
89+
)
90+
func testRPC_mTLS_TransportContext_OK(supportedTransport: TransportKind) async throws {
91+
let certificateKeyPairs = try SelfSignedCertificateKeyPairs()
92+
let clientConfig = self.makeMTLSClientConfig(
93+
for: supportedTransport,
94+
certificateKeyPairs: certificateKeyPairs,
95+
serverHostname: "localhost"
96+
)
97+
let serverConfig = self.makeMTLSServerConfig(
98+
for: supportedTransport,
99+
certificateKeyPairs: certificateKeyPairs,
100+
includeClientCertificateInTrustRoots: true
101+
)
102+
103+
try await self.withClientAndServer(
104+
clientConfig: clientConfig,
105+
serverConfig: serverConfig,
106+
interceptors: [TransportSpecificInterceptor(certificateKeyPairs.client.certificate)]
107+
) { control in
108+
await #expect(throws: Never.self) {
109+
try await self.executeUnaryRPC(control: control)
110+
}
111+
}
112+
}
113+
61114
@Test(
62115
"When using mTLS defaults, both client and server verify each others' certificates",
63116
arguments: TransportKind.supported,
@@ -473,6 +526,7 @@ struct HTTP2TransportTLSEnabledTests {
473526
func withClientAndServer(
474527
clientConfig: ClientConfig,
475528
serverConfig: ServerConfig,
529+
interceptors: [any ServerInterceptor] = [],
476530
_ test: (ControlClient<NIOClientTransport>) async throws -> Void
477531
) async throws {
478532
let serverTransport: NIOServerTransport
@@ -497,7 +551,11 @@ struct HTTP2TransportTLSEnabledTests {
497551
#endif
498552
}
499553

500-
try await withGRPCServer(transport: serverTransport, services: [ControlService()]) { server in
554+
try await withGRPCServer(
555+
transport: serverTransport,
556+
services: [ControlService()],
557+
interceptors: interceptors
558+
) { server in
501559
guard let address = try await server.listeningAddress?.ipv4 else {
502560
throw TLSEnabledTestsError.unexpectedListeningAddress
503561
}

0 commit comments

Comments
 (0)