@@ -242,7 +242,24 @@ class TLSConfigurationTest: XCTestCase {
242242 ) throws {
243243 let clientContext = try assertNoThrowWithValue ( NIOSSLContext ( configuration: clientConfig) )
244244 let serverContext = try assertNoThrowWithValue ( NIOSSLContext ( configuration: serverConfig) )
245+ try self . assertHandshakeSucceededInMemory (
246+ withClientContext: clientContext,
247+ andServerContext: serverContext,
248+ file: file,
249+ line: line
250+ )
251+ }
245252
253+ /// Performs a connection in memory and validates that the handshake was successful.
254+ ///
255+ /// - NOTE: This function should only be used when you know that there is no custom verification
256+ /// callback in use, otherwise it will not be thread-safe.
257+ func assertHandshakeSucceededInMemory(
258+ withClientContext clientContext: NIOSSLContext ,
259+ andServerContext serverContext: NIOSSLContext ,
260+ file: StaticString = #filePath,
261+ line: UInt = #line
262+ ) throws {
246263 let serverChannel = EmbeddedChannel ( )
247264 let clientChannel = EmbeddedChannel ( )
248265
@@ -294,7 +311,23 @@ class TLSConfigurationTest: XCTestCase {
294311 file: file,
295312 line: line
296313 )
314+ try self . assertHandshakeSucceededEventLoop (
315+ withClientContext: clientContext,
316+ andServerContext: serverContext,
317+ file: file,
318+ line: line
319+ )
320+ }
297321
322+ /// Performs a connection using a real event loop and validates that the handshake was successful.
323+ ///
324+ /// This function is thread-safe in the presence of custom verification callbacks.
325+ func assertHandshakeSucceededEventLoop(
326+ withClientContext clientContext: NIOSSLContext ,
327+ andServerContext serverContext: NIOSSLContext ,
328+ file: StaticString = #filePath,
329+ line: UInt = #line
330+ ) throws {
298331 let group = MultiThreadedEventLoopGroup ( numberOfThreads: 1 )
299332 defer {
300333 XCTAssertNoThrow ( try group. syncShutdownGracefully ( ) )
@@ -358,6 +391,31 @@ class TLSConfigurationTest: XCTestCase {
358391 #endif
359392 }
360393
394+ func assertHandshakeSucceeded(
395+ withClientContext clientContext: NIOSSLContext ,
396+ andServerContext serverContext: NIOSSLContext ,
397+ file: StaticString = #filePath,
398+ line: UInt = #line
399+ ) throws {
400+ // The only use of a custom callback is on Darwin...
401+ #if os(Linux)
402+ return try self . assertHandshakeSucceededInMemory (
403+ withClientContext: clientContext,
404+ andServerContext: serverContext,
405+ file: file,
406+ line: line
407+ )
408+
409+ #else
410+ return try self . assertHandshakeSucceededEventLoop (
411+ withClientContext: clientContext,
412+ andServerContext: serverContext,
413+ file: file,
414+ line: line
415+ )
416+ #endif
417+ }
418+
361419 func setupTLSLeafandClientIdentitiesFromCustomCARoot( ) throws -> (
362420 leafCert: NIOSSLCertificate , leafKey: NIOSSLPrivateKey ,
363421 clientCert: NIOSSLCertificate , clientKey: NIOSSLPrivateKey
@@ -2062,6 +2120,73 @@ class TLSConfigurationTest: XCTestCase {
20622120 errorTextContains: " TLSV1_ALERT_INTERNAL_ERROR "
20632121 )
20642122 }
2123+
2124+ func testClientSideCertSelection_eachConnectionSelectsAgain( ) throws {
2125+ let callbackCount = NIOLockedValueBox ( 0 )
2126+ var clientConfig = TLSConfiguration . makeClientConfiguration ( )
2127+ clientConfig. certificateVerification = . noHostnameVerification
2128+ clientConfig. trustRoots = . certificates( [ TLSConfigurationTest . cert1] )
2129+ clientConfig. sslContextCallback = { _, promise in
2130+ callbackCount. withLockedValue { $0 += 1 }
2131+
2132+ var `override` = NIOSSLContextConfigurationOverride ( )
2133+ override. certificateChain = [ . certificate( TLSConfigurationTest . cert2) ]
2134+ override. privateKey = . privateKey( TLSConfigurationTest . key2)
2135+ promise. succeed ( override)
2136+ }
2137+
2138+ var serverConfig = TLSConfiguration . makeServerConfiguration (
2139+ certificateChain: [ . certificate( TLSConfigurationTest . cert1) ] ,
2140+ privateKey: . privateKey( TLSConfigurationTest . key1)
2141+ )
2142+ serverConfig. certificateVerification = . noHostnameVerification
2143+ serverConfig. trustRoots = . certificates( [ TLSConfigurationTest . cert2] )
2144+
2145+ let clientContext = try assertNoThrowWithValue (
2146+ NIOSSLContext ( configuration: clientConfig)
2147+ )
2148+ let serverContext = try assertNoThrowWithValue (
2149+ NIOSSLContext ( configuration: serverConfig)
2150+ )
2151+
2152+ for _ in 0 ..< 5 {
2153+ try assertHandshakeSucceeded ( withClientContext: clientContext, andServerContext: serverContext)
2154+ }
2155+
2156+ XCTAssertEqual ( callbackCount. withLockedValue { $0 } , 5 )
2157+ }
2158+
2159+ func testServerSideCertSelection_eachConnectionSelectsAgain( ) throws {
2160+ let callbackCount = NIOLockedValueBox ( 0 )
2161+ var clientConfig = TLSConfiguration . makeClientConfiguration ( )
2162+ clientConfig. certificateVerification = . noHostnameVerification
2163+ clientConfig. trustRoots = . certificates( [ TLSConfigurationTest . cert1] )
2164+
2165+ var serverConfig = TLSConfiguration . makeServerConfiguration (
2166+ certificateChain: [ . certificate( TLSConfigurationTest . cert2) ] ,
2167+ privateKey: . privateKey( TLSConfigurationTest . key2)
2168+ )
2169+ serverConfig. sslContextCallback = { _, promise in
2170+ var `override` = NIOSSLContextConfigurationOverride ( )
2171+ override. certificateChain = [ . certificate( TLSConfigurationTest . cert1) ]
2172+ override. privateKey = . privateKey( TLSConfigurationTest . key1)
2173+ callbackCount. withLockedValue { $0 += 1 }
2174+ promise. succeed ( override)
2175+ }
2176+
2177+ let clientContext = try assertNoThrowWithValue (
2178+ NIOSSLContext ( configuration: clientConfig)
2179+ )
2180+ let serverContext = try assertNoThrowWithValue (
2181+ NIOSSLContext ( configuration: serverConfig)
2182+ )
2183+
2184+ for _ in 0 ..< 5 {
2185+ try assertHandshakeSucceeded ( withClientContext: clientContext, andServerContext: serverContext)
2186+ }
2187+
2188+ XCTAssertEqual ( callbackCount. withLockedValue { $0 } , 5 )
2189+ }
20652190}
20662191
20672192extension EmbeddedChannel {
0 commit comments