@@ -24,7 +24,42 @@ internal static partial class OpenSsl
2424 private const string TlsCacheSizeCtxName = "System.Net.Security.TlsCacheSize" ;
2525 private const string TlsCacheSizeEnvironmentVariable = "DOTNET_SYSTEM_NET_SECURITY_TLSCACHESIZE" ;
2626 private const SslProtocols FakeAlpnSslProtocol = ( SslProtocols ) 1 ; // used to distinguish server sessions with ALPN
27- private static readonly ConcurrentDictionary < SslProtocols , SafeSslContextHandle > s_clientSslContexts = new ConcurrentDictionary < SslProtocols , SafeSslContextHandle > ( ) ;
27+
28+ private sealed class SafeSslContextCache : SafeHandleCache < SslContextCacheKey , SafeSslContextHandle > { }
29+
30+ private static readonly SafeSslContextCache s_clientSslContexts = new ( ) ;
31+
32+ internal readonly struct SslContextCacheKey : IEquatable < SslContextCacheKey >
33+ {
34+ public readonly byte [ ] ? CertificateThumbprint ;
35+ public readonly SslProtocols SslProtocols ;
36+
37+ public SslContextCacheKey ( SslProtocols sslProtocols , byte [ ] ? certificateThumbprint )
38+ {
39+ SslProtocols = sslProtocols ;
40+ CertificateThumbprint = certificateThumbprint ;
41+ }
42+
43+ public override bool Equals ( object ? obj ) => obj is SslContextCacheKey key && Equals ( key ) ;
44+
45+ public bool Equals ( SslContextCacheKey other ) =>
46+ SslProtocols == other . SslProtocols &&
47+ ( CertificateThumbprint == null && other . CertificateThumbprint == null ||
48+ CertificateThumbprint != null && other . CertificateThumbprint != null && CertificateThumbprint . AsSpan ( ) . SequenceEqual ( other . CertificateThumbprint ) ) ;
49+
50+ public override int GetHashCode ( )
51+ {
52+ HashCode hash = default ;
53+
54+ hash . Add ( SslProtocols ) ;
55+ if ( CertificateThumbprint != null )
56+ {
57+ hash . AddBytes ( CertificateThumbprint ) ;
58+ }
59+
60+ return hash . ToHashCode ( ) ;
61+ }
62+ }
2863
2964 #region internal methods
3065 internal static SafeChannelBindingHandle ? QueryChannelBinding ( SafeSslHandle context , ChannelBindingKind bindingType )
@@ -113,6 +148,54 @@ private static SslProtocols CalculateEffectiveProtocols(SslAuthenticationOptions
113148 return protocols ;
114149 }
115150
151+ internal static SafeSslContextHandle GetOrCreateSslContextHandle ( SslAuthenticationOptions sslAuthenticationOptions , bool allowCached )
152+ {
153+ SslProtocols protocols = CalculateEffectiveProtocols ( sslAuthenticationOptions ) ;
154+
155+ if ( ! allowCached )
156+ {
157+ return AllocateSslContext ( sslAuthenticationOptions , protocols , allowCached ) ;
158+ }
159+
160+ if ( sslAuthenticationOptions . IsClient )
161+ {
162+ var key = new SslContextCacheKey ( protocols , sslAuthenticationOptions . CertificateContext ? . TargetCertificate . GetCertHash ( HashAlgorithmName . SHA256 ) ) ;
163+
164+ return s_clientSslContexts . GetOrCreate ( key , static ( args ) =>
165+ {
166+ var ( sslAuthOptions , protocols , allowCached ) = args ;
167+ return AllocateSslContext ( sslAuthOptions , protocols , allowCached ) ;
168+ } , ( sslAuthenticationOptions , protocols , allowCached ) ) ;
169+ }
170+
171+ // cache in SslStreamCertificateContext is bounded and there is no eviction
172+ // so the handle should always be valid,
173+
174+ bool hasAlpn = sslAuthenticationOptions . ApplicationProtocols != null && sslAuthenticationOptions . ApplicationProtocols . Count != 0 ;
175+
176+ SafeSslContextHandle ? handle = AllocateSslContext ( sslAuthenticationOptions , protocols , allowCached ) ;
177+
178+ if ( ! sslAuthenticationOptions . CertificateContext ! . SslContexts ! . TryGetValue ( protocols | ( hasAlpn ? FakeAlpnSslProtocol : SslProtocols . None ) , out handle ) )
179+ {
180+ // not found in cache, create and insert
181+ handle = AllocateSslContext ( sslAuthenticationOptions , protocols , allowCached ) ;
182+
183+ SafeSslContextHandle cached = sslAuthenticationOptions . CertificateContext ! . SslContexts ! . GetOrAdd ( protocols | ( hasAlpn ? FakeAlpnSslProtocol : SslProtocols . None ) , handle ) ;
184+
185+ if ( handle != cached )
186+ {
187+ // lost the race, another thread created the SSL_CTX meanwhile, prefer the cached one
188+ handle . Dispose ( ) ;
189+ Debug . Assert ( handle . IsClosed ) ;
190+ handle = cached ;
191+ }
192+ }
193+
194+ Debug . Assert ( ! handle . IsClosed ) ;
195+ handle . TryAddRentCount ( ) ;
196+ return handle ;
197+ }
198+
116199 // This essentially wraps SSL_CTX* aka SSL_CTX_new + setting
117200 internal static unsafe SafeSslContextHandle AllocateSslContext ( SslAuthenticationOptions sslAuthenticationOptions , SslProtocols protocols , bool enableResume )
118201 {
@@ -188,7 +271,7 @@ internal static unsafe SafeSslContextHandle AllocateSslContext(SslAuthentication
188271 Interop . Ssl . SslCtxSetAlpnSelectCb ( sslCtx , & AlpnServerSelectCallback , IntPtr . Zero ) ;
189272 }
190273
191- if ( sslAuthenticationOptions . CertificateContext != null )
274+ if ( sslAuthenticationOptions . CertificateContext != null && sslAuthenticationOptions . IsServer )
192275 {
193276 SetSslCertificate ( sslCtx , sslAuthenticationOptions . CertificateContext . CertificateHandle , sslAuthenticationOptions . CertificateContext . KeyHandle ) ;
194277
@@ -257,10 +340,6 @@ internal static void UpdateClientCertificate(SafeSslHandle ssl, SslAuthenticatio
257340 internal static SafeSslHandle AllocateSslHandle ( SslAuthenticationOptions sslAuthenticationOptions )
258341 {
259342 SafeSslHandle ? sslHandle = null ;
260- SafeSslContextHandle ? sslCtxHandle = null ;
261- SafeSslContextHandle ? newCtxHandle = null ;
262- SslProtocols protocols = CalculateEffectiveProtocols ( sslAuthenticationOptions ) ;
263- bool hasAlpn = sslAuthenticationOptions . ApplicationProtocols != null && sslAuthenticationOptions . ApplicationProtocols . Count != 0 ;
264343 bool cacheSslContext = sslAuthenticationOptions . AllowTlsResume && ! SslStream . DisableTlsResume && sslAuthenticationOptions . EncryptionPolicy == EncryptionPolicy . RequireEncryption && sslAuthenticationOptions . CipherSuitesPolicy == null ;
265344
266345 if ( cacheSslContext )
@@ -269,13 +348,12 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
269348 {
270349 // We don't support client resume on old OpenSSL versions.
271350 // We don't want to try on empty TargetName since that is our key.
272- // And we don't want to mess up with client authentication. It may be possible
273- // but it seems safe to get full new session.
351+ // If we already have CertificateContext, then we know which cert the user wants to use and we can cache.
352+ // The only client auth scenario where we can't cache is when user provides a cert callback and we don't know
353+ // beforehand which cert will be used. and wan't to avoid resuming session created with different certificate.
274354 if ( ! Interop . Ssl . Capabilities . Tls13Supported ||
275355 string . IsNullOrEmpty ( sslAuthenticationOptions . TargetHost ) ||
276- sslAuthenticationOptions . CertificateContext != null ||
277- sslAuthenticationOptions . ClientCertificates ? . Count > 0 ||
278- sslAuthenticationOptions . CertSelectionDelegate != null )
356+ ( sslAuthenticationOptions . CertificateContext == null && sslAuthenticationOptions . CertSelectionDelegate != null ) )
279357 {
280358 cacheSslContext = false ;
281359 }
@@ -292,35 +370,14 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
292370 }
293371 }
294372
295- if ( cacheSslContext )
296- {
297- if ( sslAuthenticationOptions . IsServer )
298- {
299- sslAuthenticationOptions . CertificateContext ! . SslContexts ! . TryGetValue ( protocols | ( hasAlpn ? FakeAlpnSslProtocol : SslProtocols . None ) , out sslCtxHandle ) ;
300- }
301- else
302- {
303-
304- s_clientSslContexts . TryGetValue ( protocols , out sslCtxHandle ) ;
305- }
306- }
307-
308- if ( sslCtxHandle == null )
309- {
310- // We did not get SslContext from cache
311- sslCtxHandle = newCtxHandle = AllocateSslContext ( sslAuthenticationOptions , protocols , cacheSslContext ) ;
312-
313- if ( cacheSslContext )
314- {
315- bool added = sslAuthenticationOptions . IsServer ?
316- sslAuthenticationOptions . CertificateContext ! . SslContexts ! . TryAdd ( protocols | ( SslProtocols ) ( hasAlpn ? 1 : 0 ) , newCtxHandle ) :
317- s_clientSslContexts . TryAdd ( protocols , newCtxHandle ) ;
318- if ( added )
319- {
320- newCtxHandle = null ;
321- }
322- }
323- }
373+ // We do not touch the SSL_CTX after we create and configure SSL
374+ // objects, and SSL object created later in this function will keep an
375+ // outstanding up-ref on SSL_CTX.
376+ //
377+ // For uncached SafeSslContextHandles, the handle will be disposed and closed.
378+ // Cached SafeSslContextHandles are returned with increaset rent count so that
379+ // Dispose() here will not close the handle.
380+ using SafeSslContextHandle sslCtxHandle = GetOrCreateSslContextHandle ( sslAuthenticationOptions , cacheSslContext ) ;
324381
325382 GCHandle alpnHandle = default ;
326383 try
@@ -361,19 +418,25 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
361418 Crypto . ErrClearError ( ) ;
362419 }
363420
364-
365421 if ( cacheSslContext )
366422 {
367423 sslCtxHandle . TrySetSession ( sslHandle , sslAuthenticationOptions . TargetHost ) ;
368- bool ignored = false ;
369- sslCtxHandle . DangerousAddRef ( ref ignored ) ;
424+
425+ // Maintain additional rent count for the context so
426+ // that it is not evicted from the cache and future
427+ // SSL objects can reuse it. This call should always
428+ // succeed because already have increased rent count
429+ // when getting the context from the cache
430+ bool success = sslCtxHandle . TryAddRentCount ( ) ;
431+ Debug . Assert ( success ) ;
370432 sslHandle . SslContextHandle = sslCtxHandle ;
371433 }
372434 }
373435
374436 // relevant to TLS 1.3 only: if user supplied a client cert or cert callback,
375437 // advertise that we are willing to send the certificate post-handshake.
376- if ( sslAuthenticationOptions . ClientCertificates ? . Count > 0 ||
438+ if ( sslAuthenticationOptions . CertificateContext != null ||
439+ sslAuthenticationOptions . ClientCertificates ? . Count > 0 ||
377440 sslAuthenticationOptions . CertSelectionDelegate != null )
378441 {
379442 Ssl . SslSetPostHandshakeAuth ( sslHandle , 1 ) ;
@@ -434,10 +497,6 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
434497
435498 throw ;
436499 }
437- finally
438- {
439- newCtxHandle ? . Dispose ( ) ;
440- }
441500
442501 return sslHandle ;
443502 }
@@ -708,6 +767,12 @@ private static unsafe int NewSessionCallback(IntPtr ssl, IntPtr session)
708767 Debug . Assert ( ssl != IntPtr . Zero ) ;
709768 Debug . Assert ( session != IntPtr . Zero ) ;
710769
770+ // remember if the session used a certificate, this information is used after
771+ // session resumption, the pointer is not being dereferenced and the refcount
772+ // is not going to be manipulated.
773+ IntPtr cert = Interop . Ssl . SslGetCertificate ( ssl ) ;
774+ Interop . Ssl . SslSessionSetData ( session , cert ) ;
775+
711776 IntPtr ptr = Ssl . SslGetData ( ssl ) ;
712777 if ( ptr != IntPtr . Zero )
713778 {
0 commit comments