Skip to content

Commit 39b8e90

Browse files
authored
Honour certificate verification mode when custom trust roots are set (TransportServices) (#185)
Fixes #184. ## Motivation With `trustRoots: .certificates(...)`, the TransportServices transports install a verify block that anchors the custom roots and then calls `SecTrustEvaluateAsyncWithError` on the trust exactly as Network.framework configured it — with the default SSL policy, which includes hostname matching. The configured `CertificateVerification` mode is never consulted there: - `.noHostnameVerification` behaves like full verification: any certificate whose SANs don't cover the dialled endpoint is rejected (`errSSLBadCert`), even though the chain validates against the custom roots. This breaks private-CA deployments whose certs deliberately carry no DNS/IP SANs, and peers dialled by IP address. The identical configuration works with the Posix transport, where the mode maps to NIOSSL's `certificateVerification = .noHostnameVerification`. - `.noVerification` still installs the evaluating block and can fail the handshake despite verification being explicitly disabled. ## Modifications The verify block now receives the verification mode and whether it is evaluating server or client certificates: - `.noHostnameVerification`: the trust is re-policied with `SecPolicyCreateSSL(<role>, nil)` before evaluation — chain validation against the custom anchors is unchanged, hostname matching is skipped. - `.noVerification`: no verify block is installed (the callers already set `sec_protocol_options_set_peer_authentication_required(false)` for this mode), matching NIOSSL semantics. - `.fullVerification`: behaviour unchanged. ## Result `.noHostnameVerification` + custom trust roots connects when the chain is valid but the hostname doesn't match, on both client and server TransportServices transports, matching the Posix transport. New regression test (`testClientNoHostnameVerificationIgnoresHostname`) covers all client/server transport combinations; without the source change the two TransportServices-client cases fail with `NWError.tls(-9808)`. Existing TLS suites pass (`HTTP2TransportTLSEnabledTests`, `TLSConfigurationTests`).
1 parent 2ca31f0 commit 39b8e90

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)