Skip to content

Commit 1b93013

Browse files
glbrnttWendellXY
authored andcommitted
Allow custom verification callback to be configured for servers (grpc#1595)
Motivation: NIOSSL allows users to override its certificate verification logic by setting a verification callback. gRPC allows clients to configure this but not servers. Modifications: - Add extra API to `GRPCTLSConfiguration` to allow custom verification callbacks to be set for servers. Result: Users can override the certificate verification logic on the server.
1 parent 3288a96 commit 1b93013

File tree

5 files changed

+148
-6
lines changed

5 files changed

+148
-6
lines changed

Sources/GRPC/GRPCTLSConfiguration.swift

+58-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616
#if canImport(NIOSSL)
17+
import NIOCore
1718
import NIOSSL
1819
#endif
1920

@@ -310,6 +311,38 @@ extension GRPCTLSConfiguration {
310311
trustRoots: NIOSSLTrustRoots = .default,
311312
certificateVerification: CertificateVerification = .none,
312313
requireALPN: Bool = true
314+
) -> GRPCTLSConfiguration {
315+
return Self.makeServerConfigurationBackedByNIOSSL(
316+
certificateChain: certificateChain,
317+
privateKey: privateKey,
318+
trustRoots: trustRoots,
319+
certificateVerification: certificateVerification,
320+
requireALPN: requireALPN,
321+
customVerificationCallback: nil
322+
)
323+
}
324+
325+
/// TLS Configuration with suitable defaults for servers.
326+
///
327+
/// This is a wrapper around `NIOSSL.TLSConfiguration` to restrict input to values which comply
328+
/// with the gRPC protocol.
329+
///
330+
/// - Parameter certificateChain: The certificate to offer during negotiation.
331+
/// - Parameter privateKey: The private key associated with the leaf certificate.
332+
/// - Parameter trustRoots: The trust roots to validate certificates, this defaults to using a
333+
/// root provided by the platform.
334+
/// - Parameter certificateVerification: Whether to verify the remote certificate. Defaults to
335+
/// `.none`.
336+
/// - Parameter requireALPN: Whether ALPN is required or not.
337+
/// - Parameter customVerificationCallback: A callback to provide to override the certificate verification logic,
338+
/// defaults to `nil`.
339+
public static func makeServerConfigurationBackedByNIOSSL(
340+
certificateChain: [NIOSSLCertificateSource],
341+
privateKey: NIOSSLPrivateKeySource,
342+
trustRoots: NIOSSLTrustRoots = .default,
343+
certificateVerification: CertificateVerification = .none,
344+
requireALPN: Bool = true,
345+
customVerificationCallback: NIOSSLCustomVerificationCallback? = nil
313346
) -> GRPCTLSConfiguration {
314347
var configuration = TLSConfiguration.makeServerConfiguration(
315348
certificateChain: certificateChain,
@@ -323,7 +356,8 @@ extension GRPCTLSConfiguration {
323356

324357
return GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL(
325358
configuration: configuration,
326-
requireALPN: requireALPN
359+
requireALPN: requireALPN,
360+
customVerificationCallback: customVerificationCallback
327361
)
328362
}
329363

@@ -338,6 +372,28 @@ extension GRPCTLSConfiguration {
338372
public static func makeServerConfigurationBackedByNIOSSL(
339373
configuration: TLSConfiguration,
340374
requireALPN: Bool = true
375+
) -> GRPCTLSConfiguration {
376+
return Self.makeServerConfigurationBackedByNIOSSL(
377+
configuration: configuration,
378+
requireALPN: requireALPN,
379+
customVerificationCallback: nil
380+
)
381+
}
382+
383+
/// Creates a gRPC TLS Configuration suitable for servers using the given
384+
/// `NIOSSL.TLSConfiguration`.
385+
///
386+
/// - Note: If no ALPN tokens are set in `configuration.applicationProtocols` then "grpc-exp",
387+
/// "h2", and "http/1.1" will be used.
388+
/// - Parameters:
389+
/// - configuration: The `NIOSSL.TLSConfiguration` to base this configuration on.
390+
/// - requiresALPN: Whether the server enforces ALPN. Defaults to `true`.
391+
/// - Parameter customVerificationCallback: A callback to provide to override the certificate verification logic,
392+
/// defaults to `nil`.
393+
public static func makeServerConfigurationBackedByNIOSSL(
394+
configuration: TLSConfiguration,
395+
requireALPN: Bool = true,
396+
customVerificationCallback: NIOSSLCustomVerificationCallback? = nil
341397
) -> GRPCTLSConfiguration {
342398
var configuration = configuration
343399

@@ -348,7 +404,7 @@ extension GRPCTLSConfiguration {
348404

349405
let nioConfiguration = NIOConfiguration(
350406
configuration: configuration,
351-
customVerificationCallback: nil,
407+
customVerificationCallback: customVerificationCallback,
352408
hostnameOverride: nil,
353409
requireALPN: requireALPN
354410
)

Sources/GRPC/Server.swift

+11-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,17 @@ public final class Server {
140140
let sync = channel.pipeline.syncOperations
141141
#if canImport(NIOSSL)
142142
if let sslContext = try sslContext?.get() {
143-
try sync.addHandler(NIOSSLServerHandler(context: sslContext))
143+
let sslHandler: NIOSSLServerHandler
144+
if let verify = configuration.tlsConfiguration?.nioSSLCustomVerificationCallback {
145+
sslHandler = NIOSSLServerHandler(
146+
context: sslContext,
147+
customVerificationCallback: verify
148+
)
149+
} else {
150+
sslHandler = NIOSSLServerHandler(context: sslContext)
151+
}
152+
153+
try sync.addHandler(sslHandler)
144154
}
145155
#endif // canImport(NIOSSL)
146156

Tests/GRPCTests/AsyncAwaitSupport/AsyncClientTests.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ final class AsyncClientCancellationTests: GRPCTestCase {
3333

3434
override func tearDown() async throws {
3535
if self.pool != nil {
36-
try self.pool.close().wait()
36+
try await self.pool.close().get()
3737
self.pool = nil
3838
}
3939

4040
if self.server != nil {
41-
try self.server.close().wait()
41+
try await self.server.close().get()
4242
self.server = nil
4343
}
4444

Tests/GRPCTests/ConnectionPool/ConnectionPoolTests.swift

-1
Original file line numberDiff line numberDiff line change
@@ -1245,7 +1245,6 @@ struct HTTP2FrameEncoder {
12451245
buf.writeInteger(Int32(frame.streamID))
12461246

12471247
// frame payload follows, which depends on the frame type itself
1248-
let payloadStart = buf.writerIndex
12491248
let extraFrameData: IOData?
12501249
let payloadSize: Int
12511250

Tests/GRPCTests/ServerTLSErrorTests.swift

+77
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,83 @@ class ServerTLSErrorTests: GRPCTestCase {
124124
XCTFail("Expected NIOSSLError.handshakeFailed(BoringSSL.sslError)")
125125
}
126126
}
127+
128+
func testServerCustomVerificationCallback() async throws {
129+
let verificationCallbackInvoked = self.serverEventLoopGroup.next().makePromise(of: Void.self)
130+
let configuration = GRPCTLSConfiguration.makeServerConfigurationBackedByNIOSSL(
131+
certificateChain: [.certificate(SampleCertificate.server.certificate)],
132+
privateKey: .privateKey(SamplePrivateKey.server),
133+
certificateVerification: .fullVerification,
134+
customVerificationCallback: { _, promise in
135+
verificationCallbackInvoked.succeed()
136+
promise.succeed(.failed)
137+
}
138+
)
139+
140+
let server = try await Server.usingTLS(with: configuration, on: self.serverEventLoopGroup)
141+
.withServiceProviders([EchoProvider()])
142+
.bind(host: "localhost", port: 0)
143+
.get()
144+
defer {
145+
XCTAssertNoThrow(try server.close().wait())
146+
}
147+
148+
let clientTLSConfiguration = GRPCTLSConfiguration.makeClientConfigurationBackedByNIOSSL(
149+
certificateChain: [.certificate(SampleCertificate.client.certificate)],
150+
privateKey: .privateKey(SamplePrivateKey.client),
151+
trustRoots: .certificates([SampleCertificate.ca.certificate]),
152+
certificateVerification: .noHostnameVerification,
153+
hostnameOverride: SampleCertificate.server.commonName
154+
)
155+
156+
let client = try GRPCChannelPool.with(
157+
target: .hostAndPort("localhost", server.channel.localAddress!.port!),
158+
transportSecurity: .tls(clientTLSConfiguration),
159+
eventLoopGroup: self.clientEventLoopGroup
160+
)
161+
defer {
162+
XCTAssertNoThrow(try client.close().wait())
163+
}
164+
165+
let echo = Echo_EchoAsyncClient(channel: client)
166+
167+
enum TaskResult {
168+
case rpcFailed
169+
case rpcSucceeded
170+
case verificationCallbackInvoked
171+
}
172+
173+
await withTaskGroup(of: TaskResult.self, returning: Void.self) { group in
174+
group.addTask {
175+
// Call the service to start an RPC.
176+
do {
177+
_ = try await echo.get(.with { $0.text = "foo" })
178+
return .rpcSucceeded
179+
} catch {
180+
return .rpcFailed
181+
}
182+
}
183+
184+
group.addTask {
185+
// '!' is okay, the promise is only ever succeeded.
186+
try! await verificationCallbackInvoked.futureResult.get()
187+
return .verificationCallbackInvoked
188+
}
189+
190+
while let next = await group.next() {
191+
switch next {
192+
case .verificationCallbackInvoked:
193+
// Expected.
194+
group.cancelAll()
195+
case .rpcFailed:
196+
// Expected, carry on.
197+
continue
198+
case .rpcSucceeded:
199+
XCTFail("RPC succeeded but shouldn't have")
200+
}
201+
}
202+
}
203+
}
127204
}
128205

129206
#endif // canImport(NIOSSL)

0 commit comments

Comments
 (0)