-
Notifications
You must be signed in to change notification settings - Fork 5.3k
PoC TLS resume on Linux client #64369
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e52b12f
1811028
f9a2eab
9b87b4e
fd5635d
99240d4
342d916
acd3b46
51ec560
dc453d4
2170ec4
929e5c7
c2c8580
d2ab19b
576d4d5
876b5a0
434ab42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using System.Diagnostics; | ||
| using System.Globalization; | ||
|
|
@@ -21,7 +22,9 @@ internal static partial class OpenSsl | |
| { | ||
| private const string DisableTlsResumeCtxSwitch = "System.Net.Security.DisableTlsResume"; | ||
| private const string DisableTlsResumeEnvironmentVariable = "DOTNET_SYSTEM_NET_SECURITY_DISABLETLSRESUME"; | ||
| private const SslProtocols FakeAlpnSslProtocol = (SslProtocols)1; // used to distinguish server sessions with ALPN | ||
| private static readonly IdnMapping s_idnMapping = new IdnMapping(); | ||
| private static readonly ConcurrentDictionary<SslProtocols, SafeSslContextHandle> s_clientSslContexts = new ConcurrentDictionary<SslProtocols, SafeSslContextHandle>(); | ||
|
|
||
| #region internal methods | ||
| internal static SafeChannelBindingHandle? QueryChannelBinding(SafeSslHandle context, ChannelBindingKind bindingType) | ||
|
|
@@ -80,7 +83,7 @@ private static bool DisableTlsResume | |
| private static SslProtocols CalculateEffectiveProtocols(SslAuthenticationOptions sslAuthenticationOptions) | ||
| { | ||
| // make sure low bit is not set since we use it in context dictionary to distinguish use with ALPN | ||
| Debug.Assert(((int)sslAuthenticationOptions.EnabledSslProtocols & 1) == 0); | ||
| Debug.Assert((sslAuthenticationOptions.EnabledSslProtocols & FakeAlpnSslProtocol) == 0); | ||
| SslProtocols protocols = sslAuthenticationOptions.EnabledSslProtocols & ~((SslProtocols)1); | ||
|
|
||
| if (!Interop.Ssl.Capabilities.Tls13Supported) | ||
|
|
@@ -127,7 +130,7 @@ private static SslProtocols CalculateEffectiveProtocols(SslAuthenticationOptions | |
| } | ||
|
|
||
| // This essentially wraps SSL_CTX* aka SSL_CTX_new + setting | ||
| internal static SafeSslContextHandle AllocateSslContext(SafeFreeSslCredentials credential, SslAuthenticationOptions sslAuthenticationOptions, SslProtocols protocols, bool enableResume) | ||
| internal static unsafe SafeSslContextHandle AllocateSslContext(SafeFreeSslCredentials credential, SslAuthenticationOptions sslAuthenticationOptions, SslProtocols protocols, bool enableResume) | ||
| { | ||
| SafeX509Handle? certHandle = credential.CertHandle; | ||
| SafeEvpPKeyHandle? certKeyHandle = credential.CertKeyHandle; | ||
|
|
@@ -164,16 +167,13 @@ internal static SafeSslContextHandle AllocateSslContext(SafeFreeSslCredentials c | |
|
|
||
| Debug.Assert(cipherSuites == null || (cipherSuites.Length >= 1 && cipherSuites[cipherSuites.Length - 1] == 0)); | ||
|
|
||
| unsafe | ||
| fixed (byte* cipherListStr = cipherList) | ||
| fixed (byte* cipherSuitesStr = cipherSuites) | ||
| { | ||
| fixed (byte* cipherListStr = cipherList) | ||
| fixed (byte* cipherSuitesStr = cipherSuites) | ||
| if (!Ssl.SslCtxSetCiphers(sslCtx, cipherListStr, cipherSuitesStr)) | ||
| { | ||
| if (!Ssl.SslCtxSetCiphers(sslCtx, cipherListStr, cipherSuitesStr)) | ||
| { | ||
| Crypto.ErrClearError(); | ||
| throw new PlatformNotSupportedException(SR.Format(SR.net_ssl_encryptionpolicy_notsupported, sslAuthenticationOptions.EncryptionPolicy)); | ||
| } | ||
| Crypto.ErrClearError(); | ||
| throw new PlatformNotSupportedException(SR.Format(SR.net_ssl_encryptionpolicy_notsupported, sslAuthenticationOptions.EncryptionPolicy)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -186,15 +186,28 @@ internal static SafeSslContextHandle AllocateSslContext(SafeFreeSslCredentials c | |
| // https://www.openssl.org/docs/manmaster/ssl/SSL_shutdown.html | ||
| Ssl.SslCtxSetQuietShutdown(sslCtx); | ||
|
|
||
| Ssl.SslCtxSetCaching(sslCtx, enableResume ? 1 : 0); | ||
|
|
||
| if (sslAuthenticationOptions.IsServer && sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0) | ||
| if (enableResume) | ||
| { | ||
| unsafe | ||
| if (sslAuthenticationOptions.IsServer) | ||
| { | ||
| Ssl.SslCtxSetCaching(sslCtx, 1, null, null); | ||
| } | ||
| else | ||
| { | ||
| Interop.Ssl.SslCtxSetAlpnSelectCb(sslCtx, &AlpnServerSelectCallback, IntPtr.Zero); | ||
| int result = Ssl.SslCtxSetCaching(sslCtx, 1, &NewSessionCallback, &RemoveSessionCallback); | ||
| Debug.Assert(result == 1); | ||
| sslCtx.EnableSessionCache(); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| Ssl.SslCtxSetCaching(sslCtx, 0, null, null); | ||
| } | ||
|
|
||
| if (sslAuthenticationOptions.IsServer && sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0) | ||
| { | ||
| Interop.Ssl.SslCtxSetAlpnSelectCb(sslCtx, &AlpnServerSelectCallback, IntPtr.Zero); | ||
| } | ||
|
|
||
| bool hasCertificateAndKey = | ||
| certHandle != null && !certHandle.IsInvalid | ||
|
|
@@ -269,25 +282,63 @@ internal static SafeSslHandle AllocateSslHandle(SafeFreeSslCredentials credentia | |
| SafeSslContextHandle? newCtxHandle = null; | ||
| SslProtocols protocols = CalculateEffectiveProtocols(sslAuthenticationOptions); | ||
| bool hasAlpn = sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0; | ||
| bool cacheSslContext = !DisableTlsResume && sslAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.RequireEncryption && | ||
| sslAuthenticationOptions.IsServer && | ||
| sslAuthenticationOptions.CertificateContext != null && | ||
| sslAuthenticationOptions.CertificateContext.SslContexts != null && | ||
| sslAuthenticationOptions.CipherSuitesPolicy == null; | ||
| bool cacheSslContext = !DisableTlsResume && sslAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.RequireEncryption && sslAuthenticationOptions.CipherSuitesPolicy == null; | ||
|
|
||
| if (cacheSslContext) | ||
| { | ||
| sslAuthenticationOptions.CertificateContext!.SslContexts!.TryGetValue(protocols | (SslProtocols)(hasAlpn ? 1 : 0), out sslCtxHandle); | ||
| if (sslAuthenticationOptions.IsClient) | ||
| { | ||
| // We don't support client resume on old OpenSSL versions. | ||
| // We don't want to try on empty TargetName since that is our key. | ||
| // And we don't want to mess up with client authentication. It may be possible | ||
| // but it seems safe to get full new session. | ||
| if (!Interop.Ssl.Capabilities.Tls13Supported || | ||
| string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) || | ||
| sslAuthenticationOptions.CertificateContext != null || | ||
| sslAuthenticationOptions.CertSelectionDelegate != null) | ||
| { | ||
| cacheSslContext = false; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| // Server should always have certificate | ||
| Debug.Assert(sslAuthenticationOptions.CertificateContext != null); | ||
| if (sslAuthenticationOptions.CertificateContext == null || | ||
| sslAuthenticationOptions.CertificateContext.SslContexts == null) | ||
| { | ||
| cacheSslContext = false; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (cacheSslContext) | ||
| { | ||
| if (sslAuthenticationOptions.IsServer) | ||
| { | ||
| sslAuthenticationOptions.CertificateContext!.SslContexts!.TryGetValue(protocols | (hasAlpn ? FakeAlpnSslProtocol : SslProtocols.None), out sslCtxHandle); | ||
| } | ||
| else | ||
| { | ||
|
|
||
| s_clientSslContexts.TryGetValue(protocols, out sslCtxHandle); | ||
| } | ||
| } | ||
|
|
||
| if (sslCtxHandle == null) | ||
| { | ||
| // We did not get SslContext from cache | ||
| sslCtxHandle = newCtxHandle = AllocateSslContext(credential, sslAuthenticationOptions, protocols, cacheSslContext); | ||
|
|
||
| if (cacheSslContext && sslAuthenticationOptions.CertificateContext!.SslContexts!.TryAdd(protocols | (SslProtocols)(hasAlpn ? 1 : 0), newCtxHandle)) | ||
| if (cacheSslContext) | ||
| { | ||
| newCtxHandle = null; | ||
| bool added = sslAuthenticationOptions.IsServer ? | ||
| sslAuthenticationOptions.CertificateContext!.SslContexts!.TryAdd(protocols | (SslProtocols)(hasAlpn ? 1 : 0), newCtxHandle) : | ||
| s_clientSslContexts.TryAdd(protocols, newCtxHandle); | ||
| if (added) | ||
| { | ||
| newCtxHandle = null; | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -306,6 +357,7 @@ internal static SafeSslHandle AllocateSslHandle(SafeFreeSslCredentials credentia | |
| { | ||
| if (sslAuthenticationOptions.IsServer) | ||
| { | ||
| Debug.Assert(Interop.Ssl.SslGetData(sslHandle) == IntPtr.Zero); | ||
| alpnHandle = GCHandle.Alloc(sslAuthenticationOptions.ApplicationProtocols); | ||
| Interop.Ssl.SslSetData(sslHandle, GCHandle.ToIntPtr(alpnHandle)); | ||
| sslHandle.AlpnHandle = alpnHandle; | ||
|
|
@@ -319,7 +371,7 @@ internal static SafeSslHandle AllocateSslHandle(SafeFreeSslCredentials credentia | |
| } | ||
| } | ||
|
|
||
| if (!sslAuthenticationOptions.IsServer) | ||
| if (sslAuthenticationOptions.IsClient) | ||
| { | ||
| // The IdnMapping converts unicode input into the IDNA punycode sequence. | ||
| string punyCode = string.IsNullOrEmpty(sslAuthenticationOptions.TargetHost) ? string.Empty : s_idnMapping.GetAscii(sslAuthenticationOptions.TargetHost!); | ||
|
|
@@ -330,6 +382,11 @@ internal static SafeSslHandle AllocateSslHandle(SafeFreeSslCredentials credentia | |
| Crypto.ErrClearError(); | ||
| } | ||
|
|
||
| if (cacheSslContext && !string.IsNullOrEmpty(punyCode)) | ||
| { | ||
| sslCtxHandle.TrySetSession(sslHandle, punyCode); | ||
| } | ||
|
|
||
| // relevant to TLS 1.3 only: if user supplied a client cert or cert callback, | ||
| // advertise that we are willing to send the certificate post-handshake. | ||
| if (sslAuthenticationOptions.ClientCertificates?.Count > 0 || | ||
|
|
@@ -644,6 +701,58 @@ private static unsafe int AlpnServerSelectCallback(IntPtr ssl, byte** outp, byte | |
| return Ssl.SSL_TLSEXT_ERR_ALERT_FATAL; | ||
| } | ||
|
|
||
| [UnmanagedCallersOnly] | ||
| // Invoked from OpenSSL when new session is created. | ||
| // We attached GCHandle to the SSL so we can find back SafeSslContextHandle holding the cache. | ||
| // New session has refCount of 1. | ||
| // If this function returns 0, OpenSSL will drop the refCount and discard the session. | ||
| // If we return 1, the ownership is transfered to us and we will need to call SessionFree(). | ||
| private static unsafe int NewSessionCallback(IntPtr ssl, IntPtr session) | ||
| { | ||
| Debug.Assert(ssl != IntPtr.Zero); | ||
| Debug.Assert(session != IntPtr.Zero); | ||
|
|
||
| IntPtr ptr = Ssl.SslGetData(ssl); | ||
| Debug.Assert(ptr != IntPtr.Zero); | ||
| GCHandle gch = GCHandle.FromIntPtr(ptr); | ||
|
|
||
| SafeSslContextHandle? ctxHandle = gch.Target as SafeSslContextHandle; | ||
| // There is no relation between SafeSslContextHandle and SafeSslHandle so the handle | ||
| // may be released while the ssl session is still active. | ||
| if (ctxHandle != null && ctxHandle.TryAddSession(Ssl.SslGetServerName(ssl), session)) | ||
| { | ||
| // offered session was stored in our cache. | ||
| return 1; | ||
| } | ||
|
|
||
| // OpenSSL will destroy session. | ||
| return 0; | ||
|
Comment on lines
+725
to
+729
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these interesting from a logging perspective (if so, that can easily be in a future change)?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking about logging but there are conditions that make it normal. If we hit this, there should be no functional change as we simply won't do the caching & resume. |
||
| } | ||
|
|
||
| [UnmanagedCallersOnly] | ||
| private static unsafe void RemoveSessionCallback(IntPtr ctx, IntPtr session) | ||
| { | ||
| Debug.Assert(ctx != IntPtr.Zero && session != IntPtr.Zero); | ||
|
|
||
| IntPtr ptr = Ssl.SslCtxGetData(ctx); | ||
| if (ptr == IntPtr.Zero) | ||
| { | ||
| // Same as above, SafeSslContextHandle could be released while OpenSSL still holds reference. | ||
| return; | ||
| } | ||
|
|
||
| GCHandle gch = GCHandle.FromIntPtr(ptr); | ||
| SafeSslContextHandle? ctxHandle = gch.Target as SafeSslContextHandle; | ||
| if (ctxHandle == null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| IntPtr name = Ssl.SessionGetHostname(session); | ||
| Debug.Assert(name != IntPtr.Zero); | ||
| ctxHandle.RemoveSession(name, session); | ||
| } | ||
|
|
||
| private static int BioRead(SafeBioHandle bio, byte[] buffer, int count) | ||
| { | ||
| Debug.Assert(buffer != null); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.