@@ -24,7 +24,42 @@ internal static partial class OpenSsl
24
24
private const string TlsCacheSizeCtxName = "System.Net.Security.TlsCacheSize" ;
25
25
private const string TlsCacheSizeEnvironmentVariable = "DOTNET_SYSTEM_NET_SECURITY_TLSCACHESIZE" ;
26
26
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
+ }
28
63
29
64
#region internal methods
30
65
internal static SafeChannelBindingHandle ? QueryChannelBinding ( SafeSslHandle context , ChannelBindingKind bindingType )
@@ -113,6 +148,54 @@ private static SslProtocols CalculateEffectiveProtocols(SslAuthenticationOptions
113
148
return protocols ;
114
149
}
115
150
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
+
116
199
// This essentially wraps SSL_CTX* aka SSL_CTX_new + setting
117
200
internal static unsafe SafeSslContextHandle AllocateSslContext ( SslAuthenticationOptions sslAuthenticationOptions , SslProtocols protocols , bool enableResume )
118
201
{
@@ -188,7 +271,7 @@ internal static unsafe SafeSslContextHandle AllocateSslContext(SslAuthentication
188
271
Interop . Ssl . SslCtxSetAlpnSelectCb ( sslCtx , & AlpnServerSelectCallback , IntPtr . Zero ) ;
189
272
}
190
273
191
- if ( sslAuthenticationOptions . CertificateContext != null )
274
+ if ( sslAuthenticationOptions . CertificateContext != null && sslAuthenticationOptions . IsServer )
192
275
{
193
276
SetSslCertificate ( sslCtx , sslAuthenticationOptions . CertificateContext . CertificateHandle , sslAuthenticationOptions . CertificateContext . KeyHandle ) ;
194
277
@@ -257,10 +340,6 @@ internal static void UpdateClientCertificate(SafeSslHandle ssl, SslAuthenticatio
257
340
internal static SafeSslHandle AllocateSslHandle ( SslAuthenticationOptions sslAuthenticationOptions )
258
341
{
259
342
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 ;
264
343
bool cacheSslContext = sslAuthenticationOptions . AllowTlsResume && ! SslStream . DisableTlsResume && sslAuthenticationOptions . EncryptionPolicy == EncryptionPolicy . RequireEncryption && sslAuthenticationOptions . CipherSuitesPolicy == null ;
265
344
266
345
if ( cacheSslContext )
@@ -269,13 +348,12 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
269
348
{
270
349
// We don't support client resume on old OpenSSL versions.
271
350
// 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.
274
354
if ( ! Interop . Ssl . Capabilities . Tls13Supported ||
275
355
string . IsNullOrEmpty ( sslAuthenticationOptions . TargetHost ) ||
276
- sslAuthenticationOptions . CertificateContext != null ||
277
- sslAuthenticationOptions . ClientCertificates ? . Count > 0 ||
278
- sslAuthenticationOptions . CertSelectionDelegate != null )
356
+ ( sslAuthenticationOptions . CertificateContext == null && sslAuthenticationOptions . CertSelectionDelegate != null ) )
279
357
{
280
358
cacheSslContext = false ;
281
359
}
@@ -292,35 +370,14 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
292
370
}
293
371
}
294
372
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 ) ;
324
381
325
382
GCHandle alpnHandle = default ;
326
383
try
@@ -361,19 +418,25 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
361
418
Crypto . ErrClearError ( ) ;
362
419
}
363
420
364
-
365
421
if ( cacheSslContext )
366
422
{
367
423
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 ) ;
370
432
sslHandle . SslContextHandle = sslCtxHandle ;
371
433
}
372
434
}
373
435
374
436
// relevant to TLS 1.3 only: if user supplied a client cert or cert callback,
375
437
// 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 ||
377
440
sslAuthenticationOptions . CertSelectionDelegate != null )
378
441
{
379
442
Ssl . SslSetPostHandshakeAuth ( sslHandle , 1 ) ;
@@ -434,10 +497,6 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth
434
497
435
498
throw ;
436
499
}
437
- finally
438
- {
439
- newCtxHandle ? . Dispose ( ) ;
440
- }
441
500
442
501
return sslHandle ;
443
502
}
@@ -708,6 +767,12 @@ private static unsafe int NewSessionCallback(IntPtr ssl, IntPtr session)
708
767
Debug . Assert ( ssl != IntPtr . Zero ) ;
709
768
Debug . Assert ( session != IntPtr . Zero ) ;
710
769
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
+
711
776
IntPtr ptr = Ssl . SslGetData ( ssl ) ;
712
777
if ( ptr != IntPtr . Zero )
713
778
{
0 commit comments