Skip to content

Commit 4a8a95f

Browse files
authored
Support TLS Resume with client certificates on Linux (#102656)
* Change SSL_CTX caching * Add failing test * Revert "Add failing test" This reverts commit 5f30d11. * WIP, tests pass. * Update src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs * Apply suggestions from code review * Move definition * Minor improvements * Add more tests * Add more no-resume tests * Move MsQuicConfiguration cache logic to common code * Use shared cache code for client SSL_CTX cache * Fix build * Update src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs * Fix tests * Fix test
1 parent 96be3e2 commit 4a8a95f

File tree

18 files changed

+804
-175
lines changed

18 files changed

+804
-175
lines changed

src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs

+113-48
Original file line numberDiff line numberDiff line change
@@ -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
{

src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs

+18-1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ internal static unsafe ReadOnlySpan<byte> SslGetAlpnSelected(SafeSslHandle ssl)
116116
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetPeerCertificate")]
117117
internal static partial IntPtr SslGetPeerCertificate(SafeSslHandle ssl);
118118

119+
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetCertificate")]
120+
internal static partial IntPtr SslGetCertificate(SafeSslHandle ssl);
121+
122+
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetCertificate")]
123+
internal static partial IntPtr SslGetCertificate(IntPtr ssl);
124+
119125
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetPeerCertChain")]
120126
internal static partial SafeSharedX509StackHandle SslGetPeerCertChain(SafeSslHandle ssl);
121127

@@ -129,6 +135,9 @@ internal static unsafe ReadOnlySpan<byte> SslGetAlpnSelected(SafeSslHandle ssl)
129135
[return: MarshalAs(UnmanagedType.Bool)]
130136
internal static partial bool SslSessionReused(SafeSslHandle ssl);
131137

138+
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetSession")]
139+
internal static partial IntPtr SslGetSession(SafeSslHandle ssl);
140+
132141
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslGetClientCAList")]
133142
private static partial SafeSharedX509NameStackHandle SslGetClientCAList_private(SafeSslHandle ssl);
134143

@@ -182,6 +191,12 @@ internal static unsafe ReadOnlySpan<byte> SslGetAlpnSelected(SafeSslHandle ssl)
182191
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslSessionSetHostname")]
183192
internal static partial int SessionSetHostname(IntPtr session, IntPtr name);
184193

194+
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslSessionGetData")]
195+
internal static partial IntPtr SslSessionGetData(IntPtr session);
196+
197+
[LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslSessionSetData")]
198+
internal static partial void SslSessionSetData(IntPtr session, IntPtr val);
199+
185200
internal static class Capabilities
186201
{
187202
// needs separate type (separate static cctor) to be sure OpenSSL is initialized.
@@ -430,7 +445,9 @@ protected override bool ReleaseHandle()
430445
Disconnect();
431446
}
432447

433-
SslContextHandle?.DangerousRelease();
448+
// drop reference to any SSL_CTX handle, any handle present here is being
449+
// rented from (client) SSL_CTX cache.
450+
SslContextHandle?.Dispose();
434451

435452
if (AlpnHandle.IsAllocated)
436453
{

src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtx.cs

+40-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Net;
56
using System.Collections.Generic;
67
using System.Collections.ObjectModel;
78
using System.Diagnostics;
89
using System.Net.Security;
910
using System.Runtime.InteropServices;
1011
using System.Security.Cryptography.X509Certificates;
1112
using System.Text;
13+
using System.Threading;
1214
using Microsoft.Win32.SafeHandles;
1315

1416
internal static partial class Interop
@@ -65,12 +67,17 @@ internal static bool AddExtraChainCertificates(SafeSslContextHandle ctx, ReadOnl
6567

6668
namespace Microsoft.Win32.SafeHandles
6769
{
68-
internal sealed class SafeSslContextHandle : SafeHandle
70+
internal sealed class SafeSslContextHandle : SafeHandle, ISafeHandleCachable
6971
{
7072
// This is session cache keyed by SNI e.g. TargetHost
7173
private Dictionary<string, IntPtr>? _sslSessions;
7274
private GCHandle _gch;
7375

76+
// SSL_CTX handles are cached, so we need to keep track of the
77+
// number of times a handle is being used. Once we decide to dispose the handle,
78+
// we set the _rentCount to -1.
79+
private volatile int _rentCount;
80+
7481
public SafeSslContextHandle()
7582
: base(IntPtr.Zero, true)
7683
{
@@ -86,6 +93,38 @@ public override bool IsInvalid
8693
get { return handle == IntPtr.Zero; }
8794
}
8895

96+
public bool TryAddRentCount()
97+
{
98+
int oldCount;
99+
100+
do
101+
{
102+
oldCount = _rentCount;
103+
if (oldCount < 0)
104+
{
105+
// The handle is already disposed.
106+
return false;
107+
}
108+
} while (Interlocked.CompareExchange(ref _rentCount, oldCount + 1, oldCount) != oldCount);
109+
110+
return true;
111+
}
112+
113+
public bool TryMarkForDispose()
114+
{
115+
return Interlocked.CompareExchange(ref _rentCount, -1, 0) == 0;
116+
}
117+
118+
protected override void Dispose(bool disposing)
119+
{
120+
if (Interlocked.Decrement(ref _rentCount) < 0)
121+
{
122+
// _rentCount is 0 if the handle was never rented (e.g. failure during creation),
123+
// and is -1 when evicted from cache.
124+
base.Dispose(disposing);
125+
}
126+
}
127+
89128
protected override bool ReleaseHandle()
90129
{
91130
if (_sslSessions != null)

0 commit comments

Comments
 (0)