-
Notifications
You must be signed in to change notification settings - Fork 0
Add support for custom client certificate verification #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
cad5ca2
bcab257
0bb4f4c
f7681db
c237a5e
32f8a84
a6be2ce
5ecc622
6686bbe
e8b6f32
0b9b203
27799c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,7 @@ import NIOPosix | |
| import NIOSSL | ||
| import SwiftASN1 | ||
| import Synchronization | ||
| import X509 | ||
| public import X509 | ||
|
|
||
| /// A generic HTTP server that can handle incoming HTTP requests. | ||
| /// | ||
|
|
@@ -40,7 +40,7 @@ import X509 | |
| /// ## Usage | ||
| /// | ||
| /// ```swift | ||
| /// let configuration = HTTPServerConfiguration( | ||
| /// let configuration = NIOHTTPServerConfiguration( | ||
| /// bindTarget: .hostAndPort(host: "localhost", port: 8080), | ||
| /// tlsConfiguration: .insecure() | ||
| /// ) | ||
|
|
@@ -81,17 +81,46 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| public typealias ResponseWriter = HTTPResponseConcludingAsyncWriter | ||
|
|
||
| private let logger: Logger | ||
| private let configuration: HTTPServerConfiguration | ||
| private let configuration: NIOHTTPServerConfiguration | ||
|
|
||
| var listeningAddressState: NIOLockedValueBox<State> | ||
|
|
||
| /// Task-local storage for connection-specific information accessible from request handlers. | ||
| /// | ||
| /// Use this to access data such as the peer's validated certificate chain. | ||
| @TaskLocal public static var connectionContext = ConnectionContext() | ||
|
|
||
| /// Connection-specific information available during request handling. | ||
| /// | ||
| /// Provides access to data such as the peer's validated certificate chain. | ||
| public struct ConnectionContext: Sendable { | ||
|
||
| var peerCertificateChainFuture: EventLoopFuture<NIOSSL.ValidatedCertificateChain?>? | ||
|
|
||
| init(_ peerCertificateChainFuture: EventLoopFuture<NIOSSL.ValidatedCertificateChain?>? = nil) { | ||
| self.peerCertificateChainFuture = peerCertificateChainFuture | ||
| } | ||
|
|
||
| /// The peer's validated certificate chain. This returns `nil` if a | ||
| /// ``NIOHTTPServerConfiguration/TransportSecurity/CustomCertificateVerificationCallback`` was not set in the | ||
| /// ``NIOHTTPServerConfiguration/TransportSecurity`` property of the server configuration, or if the peer did | ||
| /// not authenticate with certificates. | ||
| public var peerCertificateChain: X509.ValidatedCertificateChain? { | ||
| get async throws { | ||
| if let certs = try await self.peerCertificateChainFuture?.get() { | ||
| return .init(uncheckedCertificateChain: try certs.map { try Certificate($0) }) | ||
| } | ||
| return nil | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Create a new ``HTTPServer`` implemented over `SwiftNIO`. | ||
| /// - Parameters: | ||
| /// - logger: A logger instance for recording server events and debugging information. | ||
| /// - configuration: The server configuration including bind target and TLS settings. | ||
| public init( | ||
| logger: Logger, | ||
| configuration: HTTPServerConfiguration, | ||
| configuration: NIOHTTPServerConfiguration, | ||
| ) { | ||
| self.logger = logger | ||
| self.configuration = configuration | ||
|
|
@@ -171,20 +200,8 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| httpServerHTTP2Configuration: self.configuration.http2 | ||
| ) | ||
|
|
||
| let certificateChain = try certificateChain | ||
| .map { | ||
| try NIOSSLCertificate( | ||
| bytes: $0.serializeAsPEM().derBytes, | ||
| format: .der | ||
| ) | ||
| } | ||
| .map { NIOSSLCertificateSource.certificate($0) } | ||
| let privateKey = NIOSSLPrivateKeySource.privateKey( | ||
| try NIOSSLPrivateKey( | ||
| bytes: privateKey.serializeAsPEM().derBytes, | ||
| format: .der | ||
| ) | ||
| ) | ||
| let certificateChain = try certificateChain.map { try NIOSSLCertificateSource($0) } | ||
| let privateKey = try NIOSSLPrivateKeySource(privateKey) | ||
|
|
||
| var tlsConfiguration: TLSConfiguration = .makeServerConfiguration( | ||
| certificateChain: certificateChain, | ||
|
|
@@ -218,39 +235,14 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| http2Configuration: http2Config | ||
| ) | ||
|
|
||
| case .mTLS(let certificateChain, let privateKey, let trustRoots): | ||
| case .mTLS(let certificateChain, let privateKey, let trustRoots, let verificationCallback): | ||
| let http2Config = NIOHTTP2Handler.Configuration( | ||
| httpServerHTTP2Configuration: configuration.http2 | ||
| ) | ||
|
|
||
| let certificateChain = try certificateChain | ||
| .map { | ||
| try NIOSSLCertificate( | ||
| bytes: $0.serializeAsPEM().derBytes, | ||
| format: .der | ||
| ) | ||
| } | ||
| .map { NIOSSLCertificateSource.certificate($0) } | ||
| let privateKey = NIOSSLPrivateKeySource.privateKey( | ||
| try NIOSSLPrivateKey( | ||
| bytes: privateKey.serializeAsPEM().derBytes, | ||
| format: .der | ||
| ) | ||
| ) | ||
|
|
||
| let nioTrustRoots: NIOSSLTrustRoots | ||
| if let trustRoots { | ||
| nioTrustRoots = .certificates( | ||
| try trustRoots.map { | ||
| try NIOSSLCertificate( | ||
| bytes: $0.serializeAsPEM().derBytes, | ||
| format: .der | ||
| ) | ||
| } | ||
| ) | ||
| } else { | ||
| nioTrustRoots = .default | ||
| } | ||
| let certificateChain = try certificateChain.map { try NIOSSLCertificateSource($0) } | ||
| let privateKey = try NIOSSLPrivateKeySource(privateKey) | ||
| let nioTrustRoots = try NIOSSLTrustRoots(treatingNilAsSystemTrustRoots: trustRoots) | ||
|
|
||
| var tlsConfiguration: TLSConfiguration = .makeServerConfigurationWithMTLS( | ||
| certificateChain: certificateChain, | ||
|
|
@@ -264,27 +256,16 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| tlsConfiguration: tlsConfiguration, | ||
| handler: handler, | ||
| asyncChannelConfiguration: asyncChannelConfiguration, | ||
| http2Configuration: http2Config | ||
| http2Configuration: http2Config, | ||
| verificationCallback: verificationCallback | ||
| ) | ||
|
|
||
| case .reloadingMTLS(let certificateReloader, let trustRoots): | ||
| case .reloadingMTLS(let certificateReloader, let trustRoots, let verificationCallback): | ||
| let http2Config = NIOHTTP2Handler.Configuration( | ||
| httpServerHTTP2Configuration: configuration.http2 | ||
| ) | ||
|
|
||
| let nioTrustRoots: NIOSSLTrustRoots | ||
| if let trustRoots { | ||
| nioTrustRoots = .certificates( | ||
| try trustRoots.map { | ||
| try NIOSSLCertificate( | ||
| bytes: $0.serializeAsPEM().derBytes, | ||
| format: .der | ||
| ) | ||
| } | ||
| ) | ||
| } else { | ||
| nioTrustRoots = .default | ||
| } | ||
| let nioTrustRoots = try NIOSSLTrustRoots(treatingNilAsSystemTrustRoots: trustRoots) | ||
|
|
||
| var tlsConfiguration: TLSConfiguration = try .makeServerConfigurationWithMTLS( | ||
| certificateReloader: certificateReloader, | ||
|
|
@@ -297,13 +278,14 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| tlsConfiguration: tlsConfiguration, | ||
| handler: handler, | ||
| asyncChannelConfiguration: asyncChannelConfiguration, | ||
| http2Configuration: http2Config | ||
| http2Configuration: http2Config, | ||
| verificationCallback: verificationCallback | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private func serveInsecureHTTP1_1( | ||
| bindTarget: HTTPServerConfiguration.BindTarget, | ||
| bindTarget: NIOHTTPServerConfiguration.BindTarget, | ||
| handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>, | ||
| asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration | ||
| ) async throws { | ||
|
|
@@ -339,11 +321,12 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| } | ||
|
|
||
| private func serveSecureUpgrade( | ||
| bindTarget: HTTPServerConfiguration.BindTarget, | ||
| bindTarget: NIOHTTPServerConfiguration.BindTarget, | ||
| tlsConfiguration: TLSConfiguration, | ||
| handler: some HTTPServerRequestHandler<RequestReader, ResponseWriter>, | ||
| asyncChannelConfiguration: NIOAsyncChannel<HTTPRequestPart, HTTPResponsePart>.Configuration, | ||
| http2Configuration: NIOHTTP2Handler.Configuration | ||
| http2Configuration: NIOHTTP2Handler.Configuration, | ||
| verificationCallback: NIOHTTPServerConfiguration.TransportSecurity.CustomCertificateVerificationCallback? = nil | ||
| ) async throws { | ||
| switch bindTarget.backing { | ||
| case .hostAndPort(let host, let port): | ||
|
|
@@ -352,11 +335,7 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| .bind(host: host, port: port) { channel in | ||
| channel.eventLoop.makeCompletedFuture { | ||
| try channel.pipeline.syncOperations | ||
| .addHandler( | ||
| NIOSSLServerHandler( | ||
| context: .init(configuration: tlsConfiguration) | ||
| ) | ||
| ) | ||
| .addHandler(self.makeSSLServerHandler(tlsConfiguration, verificationCallback)) | ||
| }.flatMap { | ||
| channel.configureAsyncHTTPServerPipeline(http2Configuration: http2Configuration) { channel in | ||
| channel.eventLoop.makeCompletedFuture { | ||
|
|
@@ -368,7 +347,7 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| ) | ||
| } | ||
| } http2ConnectionInitializer: { channel in | ||
| channel.eventLoop.makeSucceededVoidFuture() | ||
| channel.eventLoop.makeCompletedFuture(.success(channel)) | ||
| } http2StreamInitializer: { channel in | ||
| channel.eventLoop.makeCompletedFuture { | ||
| try channel.pipeline.syncOperations | ||
|
|
@@ -395,20 +374,26 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| try await withThrowingDiscardingTaskGroup { connectionGroup in | ||
| switch try await upgradeResult.get() { | ||
| case .http1_1(let http1Channel): | ||
| connectionGroup.addTask { | ||
| try await self.handleRequestChannel( | ||
| channel: http1Channel, | ||
| handler: handler | ||
| ) | ||
| let chainFuture = http1Channel.channel.nioSSL_peerValidatedCertificateChain() | ||
| Self.$connectionContext.withValue(ConnectionContext(chainFuture)) { | ||
| connectionGroup.addTask { | ||
| try await self.handleRequestChannel( | ||
| channel: http1Channel, | ||
| handler: handler | ||
| ) | ||
| } | ||
| } | ||
| case .http2((_, let http2Multiplexer)): | ||
| case .http2((let http2Connection, let http2Multiplexer)): | ||
| do { | ||
| let chainFuture = http2Connection.nioSSL_peerValidatedCertificateChain() | ||
| for try await http2StreamChannel in http2Multiplexer.inbound { | ||
| connectionGroup.addTask { | ||
| try await self.handleRequestChannel( | ||
| channel: http2StreamChannel, | ||
| handler: handler | ||
| ) | ||
| Self.$connectionContext.withValue(ConnectionContext(chainFuture)) { | ||
|
||
| connectionGroup.addTask { | ||
| try await self.handleRequestChannel( | ||
| channel: http2StreamChannel, | ||
| handler: handler | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
|
|
@@ -507,9 +492,48 @@ public struct NIOHTTPServer: HTTPServerProtocol { | |
| } | ||
| } | ||
|
|
||
| @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) | ||
| extension NIOHTTPServer { | ||
| fileprivate func makeSSLServerHandler( | ||
| _ tlsConfiguration: TLSConfiguration, | ||
| _ customVerificationCallback: NIOHTTPServerConfiguration.TransportSecurity.CustomCertificateVerificationCallback? | ||
| ) throws -> NIOSSLServerHandler { | ||
| if let customVerificationCallback { | ||
| return try NIOSSLServerHandler( | ||
| context: .init(configuration: tlsConfiguration), | ||
| customVerificationCallbackWithMetadata: { certificates, promise in | ||
| promise.completeWithTask { | ||
| // Convert input [NIOSSLCertificate] to [X509.Certificate] | ||
| let x509Certs = try certificates.map { try Certificate($0) } | ||
|
|
||
| let callbackResult = try await customVerificationCallback(x509Certs) | ||
|
|
||
| switch callbackResult { | ||
| case .certificateVerified(let verificationMetadata): | ||
| guard let peerChain = verificationMetadata.validatedCertificateChain else { | ||
| return .certificateVerified(.init(nil)) | ||
| } | ||
|
|
||
| // Convert the result into [NIOSSLCertificate] | ||
| let nioSSLCerts = try peerChain.map { try NIOSSLCertificate($0) } | ||
| return .certificateVerified(.init(.init(nioSSLCerts))) | ||
|
|
||
| case .failed(let error): | ||
| self.logger.error("Custom certificate verification failed: \(error)") | ||
|
||
| return .failed | ||
| } | ||
| } | ||
| } | ||
| ) | ||
| } else { | ||
| return try NIOSSLServerHandler(context: .init(configuration: tlsConfiguration)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, visionOS 26.0, *) | ||
| extension NIOHTTP2Handler.Configuration { | ||
| init(httpServerHTTP2Configuration http2Config: HTTPServerConfiguration.HTTP2) { | ||
| init(httpServerHTTP2Configuration http2Config: NIOHTTPServerConfiguration.HTTP2) { | ||
| let clampedTargetWindowSize = Self.clampTargetWindowSize(http2Config.targetWindowSize) | ||
| let clampedMaxFrameSize = Self.clampMaxFrameSize(http2Config.maxFrameSize) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would remove this line and just link to the docs for
ConnectionContextinstead (you can use- SeeAlso: