Skip to content

Commit cad5ca2

Browse files
committed
Add support for specifying a custom client certificate verification callback
Motivation: `NIOSSL` supports specifying a custom verification callback for client certificate verification. When such a callback is provided to `NIOSSLServerHandler`, it will use the callback to _replace_ its default client certificate verification logic, and surface the peer's validated chain of trust (derived and returned from the custom callback). This can be useful for mTLS implementations. `NIOHTTPServer` currently does not support propagating a custom verification callback to `NIOSSLServerHandler`. This change adds support for that and also surfaces the peer's validated certificate chain that then becomes available as a result. Modifications: - Updated `HTTPServerConfiguration` to also include a `customCertificateVerificationCallback` argument for the `mTLS` and `reloadingMTLS` cases of `TransportSecurity`. - Note: This exposes `NIO` types at the `HTTPServerConfiguration` level (in particular, the arguments of the callback are [`NIOSSLCertificate`] and an `EventLoopPromise` that must be fulfilled within the callback). We currently do not have a `NIOHTTPServer`-specific configuration, so let us discuss in the comments what the best way to allow users to specify a custom verification callback would be. - Updated the `serveSecureUpgrade` method in `NIOHTTPServer` to (1) propagate the custom verification callback into the underlying `NIOSSLServerHandler`, (2) extract the peer certificate chain `EventLoopFuture` per connection, and expose that future in a new type in `NIOHTTPServer` named `Context`. - The `Context` type is accessed from a task-local property `context`. Users can await the result of the promise from their route handlers by calling `NIOHTTPServer.context.peerCertificateChain()`. - The name of this type, its properties, the task-local approach, etc. are all very much open for discussion. - Added end-to-end tests for this functionality for both HTTP/1.1 and HTTP2. - Other changes: - Added `NIOSSL+X509` containing some convenience conversions between `NIOSSL` and `X509` types. - Added documentation to the `TransportSecurity` methods as they were missing. Result: Users can now specify a custom verification callback and access the peer's validated certificate chain from the request handler.
1 parent dae143c commit cad5ca2

File tree

9 files changed

+566
-118
lines changed

9 files changed

+566
-118
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ let package = Package(
2424
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
2525
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"),
2626
.package(url: "https://github.com/apple/swift-distributed-tracing.git", from: "1.0.0"),
27-
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.0.4"),
27+
.package(url: "https://github.com/apple/swift-certificates.git", from: "1.16.0"),
2828
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
2929
.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),
3030
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"),

Sources/HTTPServer/HTTPServerConfiguration.swift

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
//
1212
//===----------------------------------------------------------------------===//
1313

14-
public import X509
1514
public import NIOCertificateReloading
16-
import NIOSSL
15+
public import NIOCore
16+
public import NIOSSL
17+
public import X509
1718

1819
/// Configuration settings for the HTTP server.
1920
///
@@ -53,6 +54,26 @@ public struct HTTPServerConfiguration: Sendable {
5354
/// Provides options for running the server with or without TLS encryption.
5455
/// When using TLS, you must either provide a certificate chain and private key, or a `CertificateReloader`.
5556
public struct TransportSecurity: Sendable {
57+
/// A callback that replaces `NIOSSL`'s default certificate verification with custom verification logic.
58+
///
59+
/// This is just a `Sendable` version of `NIOSSLCustomVerificationCallbackWithMetadata`.
60+
///
61+
/// ## Usage
62+
///
63+
/// The callback receives:
64+
/// - **certificates**: The certificates presented by the peer. You are responsible for building and validating
65+
/// a chain of trust from these certificates.
66+
/// - **promise**: A promise that must be completed. Call `promise.succeed(...)` with the subset of certificates
67+
/// that formed the validated chain of trust, or `promise.fail()` if verification fails.
68+
///
69+
/// - Warning: This callback completely replaces NIOSSL's certificate verification logic and must be used with
70+
/// caution.
71+
public typealias CustomCertificateVerificationCallback =
72+
@Sendable (
73+
[NIOSSLCertificate],
74+
EventLoopPromise<NIOSSLVerificationResultWithMetadata>
75+
) -> Void
76+
5677
enum Backing {
5778
case plaintext
5879
case tls(
@@ -63,18 +84,24 @@ public struct HTTPServerConfiguration: Sendable {
6384
case mTLS(
6485
certificateChain: [Certificate],
6586
privateKey: Certificate.PrivateKey,
66-
trustRoots: [Certificate]?
87+
trustRoots: [Certificate]?,
88+
customCertificateVerificationCallback: CustomCertificateVerificationCallback? = nil
6789
)
6890
case reloadingMTLS(
6991
certificateReloader: any CertificateReloader,
70-
trustRoots: [Certificate]?
92+
trustRoots: [Certificate]?,
93+
customCertificateVerificationCallback: CustomCertificateVerificationCallback? = nil
7194
)
7295
}
7396

7497
let backing: Backing
7598

7699
public static let plaintext: Self = Self(backing: .plaintext)
77100

101+
/// Enables TLS.
102+
/// - Parameters:
103+
/// - certificateChain: The certificate chain to present during negotiation.
104+
/// - privateKey: The private key corresponding to the leaf certificate in `certificateChain`.
78105
public static func tls(
79106
certificateChain: [Certificate],
80107
privateKey: Certificate.PrivateKey
@@ -87,32 +114,60 @@ public struct HTTPServerConfiguration: Sendable {
87114
)
88115
}
89116

117+
/// Enables TLS with automatic certificate reloading.
118+
/// - Parameters:
119+
/// - certificateReloader: The certificate reloader instance.
90120
public static func tls(certificateReloader: any CertificateReloader) throws -> Self {
91121
Self(backing: .reloadingTLS(certificateReloader: certificateReloader))
92122
}
93123

124+
/// Enables mTLS. Optionally provide a custom verification callback to override the default verification logic
125+
/// used to verify client certificates, and control the derivation of a validated chain of trust from the
126+
/// certificates presented by the peer.
127+
///
128+
/// - Parameters:
129+
/// - certificateChain: The certificate chain to present during negotiation.
130+
/// - privateKey: The private key corresponding to the leaf certificate in `certificateChain`.
131+
/// - trustRoots: The root certificates to trust when verifying client certificates.
132+
/// - customCertificateVerificationCallback: A custom certificate verification callback. This will override
133+
/// NIOSSL's default certificate verification logic.
94134
public static func mTLS(
95135
certificateChain: [Certificate],
96136
privateKey: Certificate.PrivateKey,
97-
trustRoots: [Certificate]?
137+
trustRoots: [Certificate]?,
138+
customCertificateVerificationCallback: CustomCertificateVerificationCallback? = nil
98139
) -> Self {
99140
Self(
100141
backing: .mTLS(
101142
certificateChain: certificateChain,
102143
privateKey: privateKey,
103-
trustRoots: trustRoots
144+
trustRoots: trustRoots,
145+
customCertificateVerificationCallback: customCertificateVerificationCallback
104146
)
105147
)
106148
}
107149

150+
/// Enables mTLS with certificate reloading. Optionally provide a custom verification callback to override the default verification logic
151+
/// used to verify client certificates, and control the derivation of a validated chain of trust from the
152+
/// certificates presented by the peer.
153+
///
154+
/// - Parameters:
155+
/// - certificateReloader: The certificate reloader instance.
156+
/// - trustRoots: The root certificates to trust when verifying client certificates.
157+
/// - customCertificateVerificationCallback: A custom certificate verification callback. This will override
158+
/// NIOSSL's default certificate verification logic.
108159
public static func mTLS(
109160
certificateReloader: any CertificateReloader,
110-
trustRoots: [Certificate]?
161+
trustRoots: [Certificate]?,
162+
customCertificateVerificationCallback: CustomCertificateVerificationCallback? = nil
111163
) throws -> Self {
112-
Self(backing: .reloadingMTLS(
113-
certificateReloader: certificateReloader,
114-
trustRoots: trustRoots
115-
))
164+
Self(
165+
backing: .reloadingMTLS(
166+
certificateReloader: certificateReloader,
167+
trustRoots: trustRoots,
168+
customCertificateVerificationCallback: customCertificateVerificationCallback
169+
)
170+
)
116171
}
117172
}
118173

@@ -148,7 +203,7 @@ public struct HTTPServerConfiguration: Sendable {
148203
maxConcurrentStreams: nil
149204
)
150205
}
151-
}
206+
}
152207

153208
/// Configuration for the backpressure strategy to use when reading requests and writing back responses.
154209
public struct BackPressureStrategy: Sendable {
@@ -187,7 +242,7 @@ public struct HTTPServerConfiguration: Sendable {
187242
/// Create a new configuration.
188243
/// - Parameters:
189244
/// - bindTarget: A ``BindTarget``.
190-
/// - tlsConfiguration: A ``TLSConfiguration``. Defaults to ``TLSConfiguration/insecure``.
245+
/// - transportSecurity: A ``TransportSecurity``. Defaults to ``TransportSecurity/plaintext``.
191246
/// - backpressureStrategy: A ``BackPressureStrategy``.
192247
/// Defaults to ``BackPressureStrategy/watermark(low:high:)`` with a low watermark of 2 and a high of 10.
193248
/// - http2: A ``HTTP2``. Defaults to ``HTTP2/defaults``.

0 commit comments

Comments
 (0)