Skip to content

Commit d6f372d

Browse files
dmonagleclaude
andcommitted
Honour certificate verification mode with custom trust roots (TransportServices)
With custom trustRoots, the TransportServices transports installed a verify block that anchored the custom roots and then evaluated the SecTrust exactly as Network.framework configured it — with the default SSL policy, including hostname matching. The configured CertificateVerification mode was never consulted, so .noHostnameVerification behaved like full verification (rejecting any certificate whose SANs don't cover the endpoint, e.g. private-CA deployments or IP-dialled peers), and .noVerification still evaluated and could fail the handshake. The verify block now honours the mode: .noHostnameVerification re-policies the trust for SSL use with no hostname before evaluating (chain validation against the custom roots is unchanged), and .noVerification installs no verify block at all, matching the Posix transport's NIOSSL semantics for both modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2ca31f0 commit d6f372d

4 files changed

Lines changed: 79 additions & 6 deletions

File tree

Sources/GRPCNIOTransportHTTP2TransportServices/Config+TLS.swift

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,22 @@ extension HTTP2ClientTransport.TransportServices {
324324

325325
@available(gRPCSwiftNIOTransport 2.0, *)
326326
extension NWProtocolTLS.Options {
327-
func setUpVerifyBlock(trustRootsSource: TLSConfig.TrustRootsSource) {
328-
if let (verifyQueue, verifyBlock) = trustRootsSource.makeTrustRootsConfig() {
327+
func setUpVerifyBlock(
328+
trustRootsSource: TLSConfig.TrustRootsSource,
329+
verification: TLSConfig.CertificateVerification,
330+
evaluatingServerCertificates: Bool
331+
) {
332+
// With verification disabled there is nothing to evaluate; installing
333+
// the block would evaluate the peer's chain anyway and could fail the
334+
// handshake despite verification being explicitly turned off.
335+
if verification == .noVerification {
336+
return
337+
}
338+
339+
if let (verifyQueue, verifyBlock) = trustRootsSource.makeTrustRootsConfig(
340+
verification: verification,
341+
evaluatingServerCertificates: evaluatingServerCertificates
342+
) {
329343
sec_protocol_options_set_verify_block(
330344
self.securityProtocolOptions,
331345
verifyBlock,
@@ -337,7 +351,10 @@ extension NWProtocolTLS.Options {
337351

338352
@available(gRPCSwiftNIOTransport 2.0, *)
339353
extension TLSConfig.TrustRootsSource {
340-
internal func makeTrustRootsConfig() -> (DispatchQueue, sec_protocol_verify_t)? {
354+
internal func makeTrustRootsConfig(
355+
verification: TLSConfig.CertificateVerification,
356+
evaluatingServerCertificates: Bool
357+
) -> (DispatchQueue, sec_protocol_verify_t)? {
341358
switch self.wrapped {
342359
case .certificates(let certificates):
343360
let verifyQueue = DispatchQueue(label: "io.grpc.CertificateVerification")
@@ -373,6 +390,16 @@ extension TLSConfig.TrustRootsSource {
373390
return
374391
}
375392

393+
// The trust arrives configured with the platform's default SSL
394+
// policy, which includes matching the peer's certificate against the
395+
// endpoint's hostname. `.noHostnameVerification` promises chain
396+
// validation without the hostname check, so re-policy the trust for
397+
// SSL use with no hostname before evaluating.
398+
if verification == .noHostnameVerification {
399+
let policy = SecPolicyCreateSSL(evaluatingServerCertificates, nil)
400+
SecTrustSetPolicies(actualTrust, policy)
401+
}
402+
376403
SecTrustSetAnchorCertificates(actualTrust, customAnchors as CFArray)
377404
SecTrustEvaluateAsyncWithError(actualTrust, verifyQueue) { _, trusted, _ in
378405
verifyCompleteCallback(trusted)

Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ClientTransport+TransportServices.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,11 @@ extension NWProtocolTLS.Options {
439439
)
440440
}
441441

442-
self.setUpVerifyBlock(trustRootsSource: tlsConfig.trustRoots)
442+
self.setUpVerifyBlock(
443+
trustRootsSource: tlsConfig.trustRoots,
444+
verification: tlsConfig.serverCertificateVerification,
445+
evaluatingServerCertificates: true
446+
)
443447
}
444448
}
445449
#endif

Sources/GRPCNIOTransportHTTP2TransportServices/HTTP2ServerTransport+TransportServices.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,11 @@ extension NWProtocolTLS.Options {
334334
)
335335
}
336336

337-
self.setUpVerifyBlock(trustRootsSource: tlsConfig.trustRoots)
337+
self.setUpVerifyBlock(
338+
trustRootsSource: tlsConfig.trustRoots,
339+
verification: tlsConfig.clientCertificateVerification,
340+
evaluatingServerCertificates: false
341+
)
338342
}
339343
}
340344
#endif

Tests/GRPCNIOTransportHTTP2Tests/HTTP2TransportTLSEnabledTests.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,41 @@ struct HTTP2TransportTLSEnabledTests {
500500
}
501501
}
502502

503+
@Test(
504+
"Client using noHostnameVerification connects despite a non-matching hostname",
505+
arguments: TransportKind.clientsWithTLS,
506+
TransportKind.serversWithTLS
507+
)
508+
@available(gRPCSwiftNIOTransport 2.0, *)
509+
// Like testClientFailsServerValidation, but with hostname verification
510+
// disabled the mismatched hostname must be ignored: the certificate chain
511+
// still validates against the custom trust roots, so the RPC succeeds.
512+
func testClientNoHostnameVerificationIgnoresHostname(
513+
clientTransport: TransportKind,
514+
serverTransport: TransportKind
515+
) async throws {
516+
let certificateKeyPairs = try SelfSignedCertificateKeyPairs()
517+
let clientTransportConfig = self.makeDefaultTLSClientConfig(
518+
for: clientTransport,
519+
certificateKeyPairs: certificateKeyPairs,
520+
authority: "wrong-hostname",
521+
serverCertificateVerification: .noHostnameVerification
522+
)
523+
let serverTransportConfig = self.makeDefaultTLSServerConfig(
524+
for: serverTransport,
525+
certificateKeyPairs: certificateKeyPairs
526+
)
527+
528+
try await self.withClientAndServer(
529+
clientConfig: clientTransportConfig,
530+
serverConfig: serverTransportConfig
531+
) { control in
532+
await #expect(throws: Never.self) {
533+
try await self.executeUnaryRPC(control: control)
534+
}
535+
}
536+
}
537+
503538
@Test(
504539
"Error is surfaced when server fails client verification",
505540
arguments: TransportKind.clientsWithTLS,
@@ -746,7 +781,8 @@ struct HTTP2TransportTLSEnabledTests {
746781
private func makeDefaultTLSClientConfig(
747782
for transportSecurity: TransportKind,
748783
certificateKeyPairs: SelfSignedCertificateKeyPairs,
749-
authority: String? = "localhost"
784+
authority: String? = "localhost",
785+
serverCertificateVerification: TLSConfig.CertificateVerification = .fullVerification
750786
) -> ClientConfig {
751787
switch transportSecurity {
752788
case .posix:
@@ -755,6 +791,7 @@ struct HTTP2TransportTLSEnabledTests {
755791
$0.trustRoots = .certificates([
756792
.bytes(certificateKeyPairs.server.certificate, format: .der)
757793
])
794+
$0.serverCertificateVerification = serverCertificateVerification
758795
}
759796
config.transport.http2.authority = authority
760797
return .posix(config)
@@ -766,6 +803,7 @@ struct HTTP2TransportTLSEnabledTests {
766803
$0.trustRoots = .certificates([
767804
.bytes(certificateKeyPairs.server.certificate, format: .der)
768805
])
806+
$0.serverCertificateVerification = serverCertificateVerification
769807
}
770808
config.transport.http2.authority = authority
771809
return .transportServices(config)

0 commit comments

Comments
 (0)