Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ extension HTTPConnectionPool {
self.tlsConfiguration =
tlsConfiguration ?? clientConfiguration.tlsConfiguration ?? .makeClientConfiguration()
}

/// Builds a ``NIOSSLClientHandler`` for `context`/`serverHostname`, routing through
/// `clientConfiguration.tlsCustomVerification` when the caller has installed one — see that
/// property's documentation for what setting it implies for NIOSSL's own verification logic.
func makeNIOSSLClientHandler(context: NIOSSLContext, serverHostname: String?) throws -> NIOSSLClientHandler {
if let customVerification = self.clientConfiguration.tlsCustomVerification {
return try NIOSSLClientHandler(
context: context,
serverHostname: serverHostname,
customVerificationCallback: customVerification
)
}
return try NIOSSLClientHandler(context: context, serverHostname: serverHostname)
}
}
}

Expand Down Expand Up @@ -406,7 +420,7 @@ extension HTTPConnectionPool.ConnectionFactory {

return sslContextFuture.flatMap { sslContext -> EventLoopFuture<String?> in
do {
let sslHandler = try NIOSSLClientHandler(
let sslHandler = try self.makeNIOSSLClientHandler(
context: sslContext,
serverHostname: sslServerHostname
)
Expand Down Expand Up @@ -591,7 +605,8 @@ extension HTTPConnectionPool.ConnectionFactory {
let localAddr = self.key.localAddress
let bootstrapFuture = tlsConfig.getNWProtocolTLSOptions(
on: eventLoop,
serverNameIndicatorOverride: key.serverNameIndicatorOverride
serverNameIndicatorOverride: key.serverNameIndicatorOverride,
customVerification: self.clientConfiguration.tlsCustomVerificationNetworkFramework
).map {
options -> NIOClientTCPBootstrapProtocol in

Expand Down Expand Up @@ -665,7 +680,7 @@ extension HTTPConnectionPool.ConnectionFactory {
sslContextFuture.flatMap { sslContext -> EventLoopFuture<Void> in
do {
let sync = channel.pipeline.syncOperations
let sslHandler = try NIOSSLClientHandler(
let sslHandler = try self.makeNIOSSLClientHandler(
context: sslContext,
serverHostname: self.key.serverNameIndicator
)
Expand Down
39 changes: 39 additions & 0 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Tracing

#if canImport(Network)
import NIOTransportServices
import Security
#endif

#if canImport(FoundationEssentials)
Expand Down Expand Up @@ -945,6 +946,40 @@ public final class HTTPClient: Sendable {
/// Configuration how distributed traces are created and handled.
public var tracing: TracingConfiguration = .init()

/// A callback that can completely override peer certificate verification for connections that use
/// the NIOSSL TLS backend — every connection on non-Apple platforms, and on Apple platforms every
/// proxied connection plus any direct connection that isn't running on Network.framework (see
/// `tlsCustomVerificationNetworkFramework` for that case, on platforms where it's available).
///
/// The callback receives the certificate chain presented by the peer (leaf first) and an
/// `EventLoopPromise` that must be completed exactly once to signal the verification result.
///
/// - Warning: Setting this overrides *all* trust-chain verification logic NIOSSL provides. It
/// does **not**, on its own, disable hostname/SNI validation — that check is a separate NIOSSL
/// step gated purely by `TLSConfiguration.certificateVerification`, and runs whenever that is
/// `.fullVerification` regardless of whether this callback is set. A conforming implementation
/// that wants to own hostname matching too must also set `tlsConfiguration.certificateVerification`
/// to `.none` or `.noHostnameVerification`. See `NIOSSLCustomVerificationCallback` (from NIOSSL) for
/// the full contract a conforming implementation must uphold to remain secure.
public var tlsCustomVerification:
(@Sendable ([NIOSSLCertificate], EventLoopPromise<NIOSSLVerificationResult>) -> Void)?

#if canImport(Network)
/// A callback that can completely override peer certificate verification for direct (non-proxied)
/// connections on Apple platforms that use Network.framework instead of NIOSSL (see
/// ``tlsCustomVerification`` for the NIOSSL backend used everywhere else, including every proxied
/// connection regardless of platform).
///
/// The callback receives the peer's `SecTrust` and a completion handler that must be invoked
/// exactly once — with `true` to accept the connection, `false` to reject it. The completion
/// handler may be invoked asynchronously (e.g. after an OCSP lookup) from any thread.
///
/// - Warning: Setting this overrides *all* verification logic Network.framework provides,
/// including trust-root validation.
public var tlsCustomVerificationNetworkFramework:
(@Sendable (SecTrust, @escaping @Sendable (Bool) -> Void) -> Void)?
#endif

public init(
tlsConfiguration: TLSConfiguration? = nil,
redirectConfiguration: RedirectConfiguration? = nil,
Expand All @@ -964,6 +999,10 @@ public final class HTTPClient: Sendable {
self.networkFrameworkWaitForConnectivity = true
self.enableMultipath = false
self.localAddress = nil
self.tlsCustomVerification = nil
#if canImport(Network)
self.tlsCustomVerificationNetworkFramework = nil
#endif
}

public init(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ extension TLSVersion {
}
}

/// Wraps a non-`Sendable` value that is, in practice, safe to hand across threads — used to satisfy the
/// compiler when passing the C-provided `sec_protocol_verify_complete_t` completion handler into a
/// `@Sendable` closure. Network.framework's own contract for `sec_protocol_verify_block_t` guarantees
/// this handler may be invoked from any queue.
private struct UncheckedSendableBox<Value>: @unchecked Sendable {
let value: Value
}

@available(macOS 10.14, iOS 12.0, tvOS 12.0, watchOS 5.0, *)
extension TLSConfiguration {
/// Dispatch queue used by Network framework TLS to control certificate verification
Expand All @@ -70,15 +78,22 @@ extension TLSConfiguration {
/// create NWProtocolTLS.Options for use with NIOTransportServices from the NIOSSL TLSConfiguration
///
/// - Parameter eventLoop: EventLoop to wait for creation of options on
/// - Parameter customVerification: When non-nil, overrides *all* of Network.framework's certificate
/// verification (including trust-root validation) with this callback — see
/// ``HTTPClient/Configuration/tlsCustomVerificationNetworkFramework``.
/// - Returns: Future holding NWProtocolTLS Options
func getNWProtocolTLSOptions(
on eventLoop: EventLoop,
serverNameIndicatorOverride: String?
serverNameIndicatorOverride: String?,
customVerification: (@Sendable (SecTrust, @escaping @Sendable (Bool) -> Void) -> Void)? = nil
) -> EventLoopFuture<NWProtocolTLS.Options> {
let promise = eventLoop.makePromise(of: NWProtocolTLS.Options.self)
Self.tlsDispatchQueue.async {
do {
let options = try self.getNWProtocolTLSOptions(serverNameIndicatorOverride: serverNameIndicatorOverride)
let options = try self.getNWProtocolTLSOptions(
serverNameIndicatorOverride: serverNameIndicatorOverride,
customVerification: customVerification
)
promise.succeed(options)
} catch {
promise.fail(error)
Expand All @@ -89,8 +104,14 @@ extension TLSConfiguration {

/// create NWProtocolTLS.Options for use with NIOTransportServices from the NIOSSL TLSConfiguration
///
/// - Parameter customVerification: When non-nil, overrides *all* of Network.framework's certificate
/// verification (including trust-root validation) with this callback — see
/// ``HTTPClient/Configuration/tlsCustomVerificationNetworkFramework``.
/// - Returns: Equivalent NWProtocolTLS Options
func getNWProtocolTLSOptions(serverNameIndicatorOverride: String?) throws -> NWProtocolTLS.Options {
func getNWProtocolTLSOptions(
serverNameIndicatorOverride: String?,
customVerification: (@Sendable (SecTrust, @escaping @Sendable (Bool) -> Void) -> Void)? = nil
) throws -> NWProtocolTLS.Options {
let options = NWProtocolTLS.Options()

let useMTELGExplainer = """
Expand Down Expand Up @@ -178,12 +199,29 @@ extension TLSConfiguration {
break
}

// A custom verification callback takes over the accept/reject decision entirely, so the
// limitations around the built-in trust-root/hostname logic below no longer apply.
precondition(
self.certificateVerification != .noHostnameVerification,
customVerification != nil || self.certificateVerification != .noHostnameVerification,
"TLSConfiguration.certificateVerification = .noHostnameVerification is not supported. \(useMTELGExplainer)"
)

if certificateVerification != .fullVerification || trustRoots != nil {
if let customVerification {
// A caller-supplied callback overrides all of Network.framework's verification logic,
// including trust-root validation — same contract as NIOSSLCustomVerificationCallback on
// the NIOSSL backend. The callback owns the accept/reject decision entirely.
sec_protocol_options_set_verify_block(
options.securityProtocolOptions,
{ _, sec_trust, sec_protocol_verify_complete in
let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
let completeBox = UncheckedSendableBox(value: sec_protocol_verify_complete)
customVerification(trust) { accepted in
completeBox.value(accepted)
}
},
Self.tlsDispatchQueue
)
} else if certificateVerification != .fullVerification || trustRoots != nil {
// add verify block to control certificate verification
sec_protocol_options_set_verify_block(
options.securityProtocolOptions,
Expand Down
Loading
Loading