Skip to content

Commit e9021d1

Browse files
AndrewBarbaLukasa
andauthored
Implement SSL_CTX_set_cert_cb (#456)
This is a first pass at reviving #311 in order to fix #310 Credit to @TechnikEmpire, I did my best to address the comments in the original PR from @Lukasa If the approach looks okay I'll move to testing --------- Co-authored-by: Cory Benfield <[email protected]>
1 parent d1088eb commit e9021d1

10 files changed

+601
-172
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ For example:
1919
```swift
2020
let configuration = TLSConfiguration.makeServerConfiguration(
2121
certificateChain: try NIOSSLCertificate.fromPEMFile("cert.pem").map { .certificate($0) },
22-
privateKey: .file("key.pem")
22+
privateKey: try .privateKey(.init(file: "key.pem", format: .pem))
2323
)
2424
let sslContext = try NIOSSLContext(configuration: configuration)
2525

Sources/NIOSSL/Docs.docc/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ For example:
2121
```swift
2222
let configuration = TLSConfiguration.makeServerConfiguration(
2323
certificateChain: try NIOSSLCertificate.fromPEMFile("cert.pem").map { .certificate($0) },
24-
privateKey: .file("key.pem")
24+
privateKey: try .privateKey(.init(file: "key.pem", format: .pem))
2525
)
2626
let sslContext = try NIOSSLContext(configuration: configuration)
2727

Sources/NIOSSL/NIOSSLHandler.swift

+5
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,11 @@ public class NIOSSLHandler : ChannelInboundHandler, ChannelOutboundHandler, Remo
402402
context.fireErrorCaught(privateKeyError)
403403
}
404404

405+
// If there's a failed custom context operation, we fire both errors.
406+
if let customContextError = self.connection.parentContext.customContextManager?.loadContextError {
407+
context.fireErrorCaught(customContextError)
408+
}
409+
405410
context.fireErrorCaught(NIOSSLError.handshakeFailed(err))
406411
channelClose(context: context, reason: NIOSSLError.handshakeFailed(err))
407412
}

Sources/NIOSSL/SSLCallbacks.swift

+136-2
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ public struct PSKServerContext: Sendable, Hashable {
153153
public let clientIdentity: String
154154
/// Maximum length of the returned PSK.
155155
public let maxPSKLength: Int
156-
156+
157157
/// Constructs a ``PSKServerContext``.
158158
///
159159
/// - parameter hint: Optional identity hint provided to the client.
@@ -172,7 +172,7 @@ public struct PSKClientContext: Sendable, Hashable {
172172
public let hint: String?
173173
/// Maximum length of the returned PSK.
174174
public let maxPSKLength: Int
175-
175+
176176
/// Constructs a ``PSKClientContext``.
177177
///
178178
/// - parameter hint: Optional identity hint provided by the server.
@@ -213,6 +213,140 @@ public struct PSKClientIdentityResponse: Sendable {
213213
}
214214
}
215215

216+
/// A structure representing values from client extensions in the SSL/TLS handshake.
217+
///
218+
/// This struct contains values obtained from the client hello message extensions during the TLS handshake process and
219+
/// can be manipulated or introspected by the `NIOSSLContextCallback` to alter the TLS handshake behaviour dynamically
220+
/// based on these values.
221+
public struct NIOSSLClientExtensionValues: Hashable, Sendable {
222+
223+
/// The hostname value from the Server Name Indication (SNI) extension.
224+
///
225+
/// This value, if available, indicates the requested server hostname by the client.
226+
/// In a context where a service is handling multiple hostnames (virtual hosts, for example),
227+
/// this value could be used to decide which SSLContext to use for the handshake.
228+
public var serverHostname: String?
229+
230+
/// Initializes a new `NIOSSLClientExtensionValues` struct.
231+
///
232+
/// - parameter serverHostname: The hostname value from the SNI extension.
233+
public init(serverHostname: String?) {
234+
self.serverHostname = serverHostname
235+
}
236+
}
237+
238+
/// A structure representing changes to the SSL/TLS configuration that can be applied
239+
/// after the client hello message extensions have been processed.
240+
public struct NIOSSLContextConfigurationOverride: Sendable {
241+
242+
/// The new certificate chain to use for the handshake.
243+
public var certificateChain: [NIOSSLCertificateSource]?
244+
245+
/// The new private key to use for the handshake.
246+
public var privateKey: NIOSSLPrivateKeySource?
247+
248+
public init() {}
249+
}
250+
251+
extension NIOSSLContextConfigurationOverride {
252+
253+
/// Return inside `NIOSSLContextCallback` when there are no changes to be made
254+
public static let noChanges = Self()
255+
}
256+
257+
/// A callback that can used to support multiple or dynamic TLS hosts.
258+
///
259+
/// When set, this callback will be invoked once per TLS hello. The provided `NIOSSLClientExtensionValues` will contain the
260+
/// host name indicated in the TLS client hello.
261+
///
262+
/// Within this callback, the user can create and return a new `NIOSSLContextConfigurationOverride` for the given host,
263+
/// and the delta will be applied to the current handshake configuration.
264+
///
265+
public typealias NIOSSLContextCallback = @Sendable (NIOSSLClientExtensionValues, EventLoopPromise<NIOSSLContextConfigurationOverride>) -> Void
266+
267+
/// A struct that provides helpers for working with a NIOSSLContextCallback.
268+
internal struct CustomContextManager: Sendable {
269+
private let callback: NIOSSLContextCallback
270+
271+
private var state: State
272+
273+
init(callback: @escaping NIOSSLContextCallback) {
274+
self.callback = callback
275+
self.state = .notStarted
276+
}
277+
}
278+
279+
extension CustomContextManager {
280+
private enum State {
281+
case notStarted
282+
283+
case pendingResult
284+
285+
case complete(Result<NIOSSLContextConfigurationOverride, Error>)
286+
}
287+
}
288+
289+
extension CustomContextManager {
290+
internal var loadContextError: (any Error)? {
291+
switch self.state {
292+
case .complete(.failure(let error)):
293+
return error
294+
default:
295+
return nil
296+
}
297+
}
298+
}
299+
300+
extension CustomContextManager {
301+
mutating func loadContext(ssl: OpaquePointer) -> Result<NIOSSLContextConfigurationOverride, Error>? {
302+
switch state {
303+
case .pendingResult:
304+
// In the pending case we return nil
305+
return nil
306+
case .complete(let result):
307+
// In the complete we can return our result
308+
return result
309+
case .notStarted:
310+
// Load the attached connection so we can resume handshake when future resolves
311+
let connection = SSLConnection.loadConnectionFromSSL(ssl)
312+
313+
guard let eventLoop = connection.eventLoop else {
314+
preconditionFailure("""
315+
SSL_CTX_set_cert_cb was executed without an event loop assigned to the connection.
316+
This should not be possible, please file an issue.
317+
""")
318+
}
319+
320+
// Construct extension values to be passed to callback
321+
let cServerHostname = CNIOBoringSSL_SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name)
322+
let serverHostname = cServerHostname.map { String(cString: $0) }
323+
let values = NIOSSLClientExtensionValues(serverHostname: serverHostname)
324+
325+
// Before invoking the user callback we can update our state to pending
326+
self.state = .pendingResult
327+
328+
// We're responsible for creating the promise and the user provided callback will fulfill it
329+
let promise = eventLoop.makePromise(of: NIOSSLContextConfigurationOverride.self)
330+
self.callback(values, promise)
331+
332+
promise.futureResult.whenComplete { result in
333+
// Ensure we execute any completion on the next event loop tick
334+
// This ensures that we suspend before calling resume
335+
eventLoop.execute {
336+
connection.parentContext.customContextManager?.state = .complete(result)
337+
connection.parentHandler?.resumeHandshake()
338+
}
339+
}
340+
341+
return nil
342+
}
343+
}
344+
345+
mutating func setLoadContextError(_ error: any Error) {
346+
self.state = .complete(.failure(error))
347+
}
348+
}
349+
216350
/// The callback used for providing a PSK on the client side.
217351
///
218352
/// The callback is invoked on the event loop with the PSK hint. This callback must complete synchronously: it cannot return a future.

Sources/NIOSSL/SSLConnection.swift

+6-3
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,18 @@ internal final class SSLConnection {
215215
CNIOBoringSSL_ERR_clear_error()
216216
let rc = CNIOBoringSSL_SSL_do_handshake(ssl)
217217

218-
if (rc == 1) { return .complete(rc) }
219-
218+
if rc == 1 {
219+
return .complete(rc)
220+
}
221+
220222
let result = CNIOBoringSSL_SSL_get_error(ssl, rc)
221223
let error = BoringSSLError.fromSSLGetErrorResult(result)!
222224

223225
switch error {
224226
case .wantRead,
225227
.wantWrite,
226-
.wantCertificateVerify:
228+
.wantCertificateVerify,
229+
.wantX509Lookup:
227230
return .incomplete
228231
default:
229232
return .failed(error)

0 commit comments

Comments
 (0)