diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs index 9cc01436b9138..3923c9e2f5e92 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs @@ -288,7 +288,7 @@ internal static SafeSslHandle AllocateSslHandle(SslAuthenticationOptions sslAuth SafeSslContextHandle? newCtxHandle = null; SslProtocols protocols = CalculateEffectiveProtocols(sslAuthenticationOptions); bool hasAlpn = sslAuthenticationOptions.ApplicationProtocols != null && sslAuthenticationOptions.ApplicationProtocols.Count != 0; - bool cacheSslContext = !DisableTlsResume && sslAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.RequireEncryption && sslAuthenticationOptions.CipherSuitesPolicy == null; + bool cacheSslContext = sslAuthenticationOptions.AllowTlsResume && !DisableTlsResume && sslAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.RequireEncryption && sslAuthenticationOptions.CipherSuitesPolicy == null; if (cacheSslContext) { diff --git a/src/libraries/Common/src/Interop/Windows/SChannel/Interop.Alerts.cs b/src/libraries/Common/src/Interop/Windows/SChannel/Interop.Alerts.cs index e0caca208c3af..faa86744fee78 100644 --- a/src/libraries/Common/src/Interop/Windows/SChannel/Interop.Alerts.cs +++ b/src/libraries/Common/src/Interop/Windows/SChannel/Interop.Alerts.cs @@ -17,11 +17,8 @@ internal static partial class SChannel // to be passed into ApplyControlToken // through a PkgParams buffer. - public const int SCHANNEL_RENEGOTIATE = 0; // renegotiate a connection public const int SCHANNEL_SHUTDOWN = 1; // gracefully close down a connection public const int SCHANNEL_ALERT = 2; // build an error message - public const int SCHANNEL_SESSION = 3; // session control - // Alert token structure. [StructLayout(LayoutKind.Sequential)] diff --git a/src/libraries/Common/src/Interop/Windows/SChannel/Interop.Session.cs b/src/libraries/Common/src/Interop/Windows/SChannel/Interop.Session.cs new file mode 100644 index 0000000000000..8d4ba5b3f7ee8 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/SChannel/Interop.Session.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class SChannel + { + // schannel.h; + + public const int SCHANNEL_SESSION = 3; // session control + + // Session structure. + [StructLayout(LayoutKind.Sequential)] + public struct SCHANNEL_SESSION_TOKEN + { + public uint dwTokenType; // SCHANNEL_SESSION + public uint dwFlags; + } + + public const int SSL_SESSION_ENABLE_RECONNECTS = 1; + public const int SSL_SESSION_DISABLE_RECONNECTS = 2; + } +} diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/ISSPIInterface.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/ISSPIInterface.cs index f107ac14aec87..dedee345a40a4 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/ISSPIInterface.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/ISSPIInterface.cs @@ -24,6 +24,6 @@ internal interface ISSPIInterface int QueryContextAttributes(SafeDeleteContext phContext, Interop.SspiCli.ContextAttribute attribute, Span buffer, Type? handleType, out SafeHandle? refHandle); int QuerySecurityContextToken(SafeDeleteContext phContext, out SecurityContextTokenHandle phToken); int CompleteAuthToken(ref SafeDeleteSslContext? refContext, in InputSecurityBuffer inputBuffer); - int ApplyControlToken(ref SafeDeleteContext? refContext, in SecurityBuffer inputBuffer); + int ApplyControlToken(ref SafeDeleteSslContext? refContext, in SecurityBuffer inputBuffer); } } diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs index 3d7c146282f9e..cbff2b2798efe 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs @@ -201,6 +201,7 @@ public enum Flags SCH_CRED_MANUAL_CRED_VALIDATION = 0x08, SCH_CRED_NO_DEFAULT_CREDS = 0x10, SCH_CRED_AUTO_CRED_VALIDATION = 0x20, + SCH_CRED_DISABLE_RECONNECTS = 0x80, SCH_CRED_REVOCATION_CHECK_END_CERT = 0x100, SCH_CRED_IGNORE_NO_REVOCATION_CHECK = 0x800, SCH_CRED_IGNORE_REVOCATION_OFFLINE = 0x1000, @@ -239,7 +240,7 @@ public enum Flags SCH_CRED_NO_DEFAULT_CREDS = 0x10, SCH_CRED_AUTO_CRED_VALIDATION = 0x20, SCH_CRED_USE_DEFAULT_CREDS = 0x40, - SCH_DISABLE_RECONNECTS = 0x80, + SCH_CRED_DISABLE_RECONNECTS = 0x80, SCH_CRED_REVOCATION_CHECK_END_CERT = 0x100, SCH_CRED_REVOCATION_CHECK_CHAIN = 0x200, SCH_CRED_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT = 0x400, diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIAuthType.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIAuthType.cs index a6c0f1a3a0a4a..09078ceaf3f97 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIAuthType.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIAuthType.cs @@ -150,7 +150,7 @@ private static int GetSecurityContextToken(SafeDeleteContext phContext, out Secu } } - public int ApplyControlToken(ref SafeDeleteContext? refContext, in SecurityBuffer inputBuffers) + public int ApplyControlToken(ref SafeDeleteSslContext? refContext, in SecurityBuffer inputBuffers) { throw new NotSupportedException(); } diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPISecureChannelType.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPISecureChannelType.cs index a580a7d26b313..cbaf079a7e25c 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPISecureChannelType.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPISecureChannelType.cs @@ -136,7 +136,7 @@ public int CompleteAuthToken(ref SafeDeleteSslContext? refContext, in InputSecur throw new NotSupportedException(); } - public int ApplyControlToken(ref SafeDeleteContext? refContext, in SecurityBuffer inputBuffer) + public int ApplyControlToken(ref SafeDeleteSslContext? refContext, in SecurityBuffer inputBuffer) { return SafeDeleteContext.ApplyControlToken(ref refContext, in inputBuffer); } diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs index b13e217b7374e..81e775d306581 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs @@ -172,7 +172,7 @@ internal static int CompleteAuthToken(ISSPIInterface secModule, ref SafeDeleteSs return errorCode; } - internal static int ApplyControlToken(ISSPIInterface secModule, ref SafeDeleteContext? context, in SecurityBuffer inputBuffer) + internal static int ApplyControlToken(ISSPIInterface secModule, ref SafeDeleteSslContext? context, in SecurityBuffer inputBuffer) { int errorCode = secModule.ApplyControlToken(ref context, in inputBuffer); diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs index addbdd6458347..9195286988376 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs @@ -1009,7 +1009,7 @@ internal static unsafe int CompleteAuthToken( } internal static unsafe int ApplyControlToken( - ref SafeDeleteContext? refContext, + ref SafeDeleteSslContext? refContext, in SecurityBuffer inSecBuffer) { if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"refContext = {refContext}, inSecBuffer = {inSecBuffer}"); diff --git a/src/libraries/System.Net.Security/ref/System.Net.Security.cs b/src/libraries/System.Net.Security/ref/System.Net.Security.cs index 87bdf170a71f7..b94c0efa91dd7 100644 --- a/src/libraries/System.Net.Security/ref/System.Net.Security.cs +++ b/src/libraries/System.Net.Security/ref/System.Net.Security.cs @@ -199,6 +199,7 @@ public partial class SslClientAuthenticationOptions { public SslClientAuthenticationOptions() { } public bool AllowRenegotiation { get { throw null; } set { } } + public bool AllowTlsResume { get { throw null; } set { } } public System.Collections.Generic.List? ApplicationProtocols { get { throw null; } set { } } public System.Security.Cryptography.X509Certificates.X509ChainPolicy? CertificateChainPolicy { get { throw null; } set { } } public System.Security.Cryptography.X509Certificates.X509RevocationMode CertificateRevocationCheckMode { get { throw null; } set { } } @@ -223,6 +224,7 @@ public partial class SslServerAuthenticationOptions { public SslServerAuthenticationOptions() { } public bool AllowRenegotiation { get { throw null; } set { } } + public bool AllowTlsResume { get { throw null; } set { } } public System.Collections.Generic.List? ApplicationProtocols { get { throw null; } set { } } public System.Security.Cryptography.X509Certificates.X509ChainPolicy? CertificateChainPolicy { get { throw null; } set { } } public System.Security.Cryptography.X509Certificates.X509RevocationMode CertificateRevocationCheckMode { get { throw null; } set { } } diff --git a/src/libraries/System.Net.Security/src/System.Net.Security.csproj b/src/libraries/System.Net.Security/src/System.Net.Security.csproj index 567656de2e14e..5579515616d2b 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -214,6 +214,8 @@ Link="Common\Interop\Windows\Kernel32\Interop.CloseHandle.cs" /> + _allowRenegotiation = value; } + /// + /// Gets or sets a value that indicates whether the SslStream should allow TLS resumption. + /// + public bool AllowTlsResume + { + get => _allowTlsResume; + set => _allowTlsResume = value; + } + public LocalCertificateSelectionCallback? LocalCertificateSelectionCallback { get; set; } public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; set; } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Linux.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Linux.cs index 1fc383b0f9cf5..40d88dd90f571 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Linux.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Linux.cs @@ -28,7 +28,9 @@ public void UpdateSslConnectionInfo(SafeSslHandle sslContext) { ApplicationProtocol = alpn.ToArray(); } - +#if DEBUG + TlsResumed = Interop.Ssl.SslSessionReused(sslContext); +#endif MapCipherSuite(SslGetCurrentCipherSuite(sslContext)); } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Windows.cs index b96c90e44a42c..3d219c7378c8a 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.Windows.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using static Interop.SspiCli; namespace System.Net.Security { @@ -66,6 +67,16 @@ public void UpdateSslConnectionInfo(SafeDeleteContext securityContext) TlsCipherSuite = cipherSuite; ApplicationProtocol = GetNegotiatedApplicationProtocol(securityContext); + +#if DEBUG + SecPkgContext_SessionInfo info = default; + TlsResumed = SSPIWrapper.QueryBlittableContextAttributes( + GlobalSSPI.SSPISecureChannel, + securityContext, + Interop.SspiCli.ContextAttribute.SECPKG_ATTR_SESSION_INFO, + ref info) && + ((SecPkgContext_SessionInfo.Flags)info.dwFlags).HasFlag(SecPkgContext_SessionInfo.Flags.SSL_SESSION_RECONNECT); +#endif } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.cs index dd309bdebe4f2..0407cb71011fe 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslConnectionInfo.cs @@ -21,5 +21,8 @@ internal partial struct SslConnectionInfo public int KeyExchKeySize { get; private set; } public byte[]? ApplicationProtocol { get; internal set; } +#if DEBUG + public bool TlsResumed { get; private set; } +#endif } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslServerAuthenticationOptions.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslServerAuthenticationOptions.cs index 8adb42e660167..cc580f588db99 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslServerAuthenticationOptions.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslServerAuthenticationOptions.cs @@ -13,6 +13,7 @@ public class SslServerAuthenticationOptions private SslProtocols _enabledSslProtocols = SecurityProtocol.SystemDefaultSecurityProtocols; private EncryptionPolicy _encryptionPolicy = EncryptionPolicy.RequireEncryption; private bool _allowRenegotiation; + private bool _allowTlsResume = true; public bool AllowRenegotiation { @@ -20,6 +21,15 @@ public bool AllowRenegotiation set => _allowRenegotiation = value; } + /// + /// Gets or sets a value that indicates whether the SslStream should allow TLS resumption. + /// + public bool AllowTlsResume + { + get => _allowTlsResume; + set => _allowTlsResume = value; + } + public bool ClientCertificateRequired { get; set; } public List? ApplicationProtocols { get; set; } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslSessionsCache.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslSessionsCache.cs index 70ebe537fcd43..4c74bcb2af000 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslSessionsCache.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslSessionsCache.cs @@ -28,6 +28,7 @@ internal static class SslSessionsCache private readonly bool _isServerMode; private readonly bool _sendTrustList; private readonly bool _checkRevocation; + private readonly bool _allowTlsResume; // // SECURITY: X509Certificate.GetCertHash() is virtual hence before going here, @@ -40,7 +41,8 @@ internal SslCredKey( bool isServerMode, EncryptionPolicy encryptionPolicy, bool sendTrustList, - bool checkRevocation) + bool checkRevocation, + bool allowTlsResume) { _thumbPrint = thumbPrint ?? Array.Empty(); _allowedProtocols = allowedProtocols; @@ -48,38 +50,24 @@ internal SslCredKey( _isServerMode = isServerMode; _checkRevocation = checkRevocation; _sendTrustList = sendTrustList; + _allowTlsResume = allowTlsResume; } public override int GetHashCode() { int hashCode = 0; - - if (_thumbPrint.Length > 0) + if (_thumbPrint.Length > 3) { - hashCode ^= _thumbPrint[0]; - if (1 < _thumbPrint.Length) - { - hashCode ^= (_thumbPrint[1] << 8); - } - - if (2 < _thumbPrint.Length) - { - hashCode ^= (_thumbPrint[2] << 16); - } - - if (3 < _thumbPrint.Length) - { - hashCode ^= (_thumbPrint[3] << 24); - } + hashCode ^= _thumbPrint[0] | (_thumbPrint[1] << 8) | (_thumbPrint[2] << 16) | (_thumbPrint[3] << 24); } - hashCode ^= _allowedProtocols; - hashCode ^= (int)_encryptionPolicy; - hashCode ^= _isServerMode ? 0x10000 : 0x20000; - hashCode ^= _sendTrustList ? 0x40000 : 0x80000; - hashCode ^= _checkRevocation ? 0x100000 : 0x200000; - - return hashCode; + return HashCode.Combine(_allowedProtocols, + (int)_encryptionPolicy, + _isServerMode, + _sendTrustList, + _checkRevocation, + _allowedProtocols, + hashCode); } public override bool Equals([NotNullWhen(true)] object? obj) => @@ -97,6 +85,7 @@ public bool Equals(SslCredKey other) _isServerMode == other._isServerMode && _sendTrustList == other._sendTrustList && _checkRevocation == other._checkRevocation && + _allowTlsResume == other._allowTlsResume && thumbPrint.AsSpan().SequenceEqual(otherThumbPrint); } } @@ -113,7 +102,8 @@ public bool Equals(SslCredKey other) bool isServer, EncryptionPolicy encryptionPolicy, bool checkRevocation, - bool sendTrustList = false) + bool allowTlsResume, + bool sendTrustList) { if (s_cachedCreds.IsEmpty) { @@ -121,7 +111,7 @@ public bool Equals(SslCredKey other) return null; } - var key = new SslCredKey(thumbPrint, (int)sslProtocols, isServer, encryptionPolicy, sendTrustList, checkRevocation); + var key = new SslCredKey(thumbPrint, (int)sslProtocols, isServer, encryptionPolicy, sendTrustList, checkRevocation, allowTlsResume); //SafeCredentialReference? cached; SafeFreeCredentials? credentials = GetCachedCredential(key); @@ -153,7 +143,8 @@ internal static void CacheCredential( bool isServer, EncryptionPolicy encryptionPolicy, bool checkRevocation, - bool sendTrustList = false) + bool allowTlsResume, + bool sendTrustList) { Debug.Assert(creds != null, "creds == null"); @@ -163,7 +154,7 @@ internal static void CacheCredential( return; } - SslCredKey key = new SslCredKey(thumbPrint, (int)sslProtocols, isServer, encryptionPolicy, sendTrustList, checkRevocation); + SslCredKey key = new SslCredKey(thumbPrint, (int)sslProtocols, isServer, encryptionPolicy, sendTrustList, checkRevocation, allowTlsResume); SafeFreeCredentials? credentials = GetCachedCredential(key); diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs index 1bd2d229c57e7..545cdf155c61b 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Protocol.cs @@ -544,7 +544,9 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint, bool newCredential _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.IsServer, _sslAuthenticationOptions.EncryptionPolicy, - _sslAuthenticationOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck); + _sslAuthenticationOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck, + _sslAuthenticationOptions.AllowTlsResume, + sendTrustList: false); // We can probably do some optimization here. If the selectedCert is returned by the delegate // we can always go ahead and use the certificate to create our credential @@ -688,6 +690,8 @@ private bool AcquireServerCredentials(ref byte[]? thumbPrint) _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.IsServer, _sslAuthenticationOptions.EncryptionPolicy, + _sslAuthenticationOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck, + _sslAuthenticationOptions.AllowTlsResume, sendTrustedList); if (cachedCredentialHandle != null) { @@ -895,6 +899,7 @@ private SecurityStatusPal GenerateToken(ReadOnlySpan inputBuffer, ref byte _sslAuthenticationOptions.IsServer, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck, + _sslAuthenticationOptions.AllowTlsResume, sendTrustList); } } @@ -933,6 +938,14 @@ internal void ProcessHandshakeSuccess() Debug.Assert(_maxDataSize > 0, "_maxDataSize > 0"); SslStreamPal.QueryContextConnectionInfo(_securityContext!, ref _connectionInfo); +#if DEBUG + if (NetEventSource.Log.IsEnabled()) + { + // This keeps the property alive only for tests via reflection + // Otherwise it could be optimized out as it is not used by production code. + NetEventSource.Info(this, $"TLS resumed {_connectionInfo.TlsResumed}"); + } +#endif } /*++ diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs index 0190efeecbcb7..2cf92d6051437 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Windows.cs @@ -45,6 +45,14 @@ public static Exception GetException(SecurityStatusPal status) internal const bool StartMutualAuthAsAnonymous = true; internal const bool CanEncryptEmptyMessage = true; + private static readonly byte[] s_sessionTokenBuffer = MemoryMarshal.AsBytes(new ReadOnlySpan( + new Interop.SChannel.SCHANNEL_SESSION_TOKEN() + { + dwTokenType = Interop.SChannel.SCHANNEL_SESSION, + dwFlags = Interop.SChannel.SSL_SESSION_DISABLE_RECONNECTS, + } + )).ToArray(); + public static void VerifyPackageInfo() { SSPIWrapper.GetVerifyPackageInfo(GlobalSSPI.SSPISecureChannel, SecurityPackage, true); @@ -139,6 +147,7 @@ public static unsafe SecurityStatusPal InitializeSecurityContext( ref byte[]? outputBuffer, SslAuthenticationOptions sslAuthenticationOptions) { + bool newContext = context == null; Interop.SspiCli.ContextFlags unusedAttributes = default; scoped InputSecurityBuffers inputBuffers = default; @@ -163,6 +172,20 @@ public static unsafe SecurityStatusPal InitializeSecurityContext( ref resultBuffer, ref unusedAttributes); + if (!sslAuthenticationOptions.AllowTlsResume && newContext && context != null) + { + var securityBuffer = new SecurityBuffer(s_sessionTokenBuffer, SecurityBufferType.SECBUFFER_TOKEN); + + SecurityStatusPal result = SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(SSPIWrapper.ApplyControlToken( + GlobalSSPI.SSPISecureChannel, + ref context, + in securityBuffer)); + + if (result.ErrorCode != SecurityStatusPalErrorCode.OK) + { + return result; + } + } outputBuffer = resultBuffer.token; return SecurityStatusAdapterPal.GetSecurityStatusPalFromNativeInt(errorCode); } @@ -278,6 +301,11 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(Ss flags = Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_SEND_AUX_RECORD | Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_NO_SYSTEM_MAPPER; + if (!authOptions.AllowTlsResume) + { + // Works only on server + flags |= Interop.SspiCli.SCHANNEL_CRED.Flags.SCH_CRED_DISABLE_RECONNECTS; + } } EncryptionPolicy policy = authOptions.EncryptionPolicy; @@ -298,6 +326,11 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(Ss protocolFlags, policy); + if (!isServer && !authOptions.AllowTlsResume) + { + secureCredential.dwSessionLifespan = -1; + } + if (certificate != null) { secureCredential.cCreds = 1; @@ -325,6 +358,11 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials( { flags |= Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_NO_SYSTEM_MAPPER; } + if (!authOptions.AllowTlsResume) + { + // Works only on server + flags |= Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_DISABLE_RECONNECTS; + } } else { @@ -369,6 +407,11 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials( Interop.SspiCli.SCH_CREDENTIALS credential = default; credential.dwVersion = Interop.SspiCli.SCH_CREDENTIALS.CurrentVersion; credential.dwFlags = flags; + if (!isServer && !authOptions.AllowTlsResume) + { + credential.dwSessionLifespan = -1; + } + if (certificate != null) { credential.cCreds = 1; @@ -500,7 +543,7 @@ public static unsafe SecurityStatusPal DecryptMessage(SafeDeleteSslContext? secu } } - public static SecurityStatusPal ApplyAlertToken(SafeDeleteContext? securityContext, TlsAlertType alertType, TlsAlertMessage alertMessage) + public static SecurityStatusPal ApplyAlertToken(SafeDeleteSslContext? securityContext, TlsAlertType alertType, TlsAlertMessage alertMessage) { var alertToken = new Interop.SChannel.SCHANNEL_ALERT_TOKEN { @@ -521,7 +564,7 @@ public static SecurityStatusPal ApplyAlertToken(SafeDeleteContext? securityConte private static readonly byte[] s_schannelShutdownBytes = BitConverter.GetBytes(Interop.SChannel.SCHANNEL_SHUTDOWN); - public static SecurityStatusPal ApplyShutdownToken(SafeDeleteContext? securityContext) + public static SecurityStatusPal ApplyShutdownToken(SafeDeleteSslContext? securityContext) { var securityBuffer = new SecurityBuffer(s_schannelShutdownBytes, SecurityBufferType.SECBUFFER_TOKEN); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs index 0ec5374b895c8..b026fc03b04a2 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs @@ -26,6 +26,7 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() { // Values used to populate client options bool clientAllowRenegotiation = false; + bool clientAllowTlsResume = false; List clientAppProtocols = new List { SslApplicationProtocol.Http11 }; X509RevocationMode clientRevocation = X509RevocationMode.NoCheck; X509CertificateCollection clientCertificates = new X509CertificateCollection() { clientCert }; @@ -37,6 +38,7 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() // Values used to populate server options bool serverAllowRenegotiation = true; + bool serverAllowTlsResume = false; List serverAppProtocols = new List { SslApplicationProtocol.Http11, SslApplicationProtocol.Http2 }; X509RevocationMode serverRevocation = X509RevocationMode.NoCheck; bool serverCertRequired = false; @@ -58,6 +60,7 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() var clientOptions = new SslClientAuthenticationOptions { AllowRenegotiation = clientAllowRenegotiation, + AllowTlsResume = clientAllowTlsResume, ApplicationProtocols = clientAppProtocols, CertificateRevocationCheckMode = clientRevocation, ClientCertificates = clientCertificates, @@ -73,6 +76,7 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() var serverOptions = new SslServerAuthenticationOptions { AllowRenegotiation = serverAllowRenegotiation, + AllowTlsResume = serverAllowTlsResume, ApplicationProtocols = serverAppProtocols, CertificateRevocationCheckMode = serverRevocation, ClientCertificateRequired = serverCertRequired, @@ -91,6 +95,7 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() // Validate that client options are unchanged Assert.Equal(clientAllowRenegotiation, clientOptions.AllowRenegotiation); + Assert.Equal(clientAllowTlsResume, clientOptions.AllowTlsResume); Assert.Same(clientAppProtocols, clientOptions.ApplicationProtocols); Assert.Equal(1, clientOptions.ApplicationProtocols.Count); Assert.Equal(clientRevocation, clientOptions.CertificateRevocationCheckMode); @@ -105,6 +110,7 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() // Validate that server options are unchanged Assert.Equal(serverAllowRenegotiation, serverOptions.AllowRenegotiation); + Assert.Equal(serverAllowTlsResume, serverOptions.AllowTlsResume); Assert.Same(serverAppProtocols, serverOptions.ApplicationProtocols); Assert.Equal(2, serverOptions.ApplicationProtocols.Count); Assert.Equal(clientRevocation, serverOptions.CertificateRevocationCheckMode); diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAllowTlsResumeTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAllowTlsResumeTests.cs new file mode 100644 index 0000000000000..494250e92e3b6 --- /dev/null +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAllowTlsResumeTests.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Xunit; +using Microsoft.DotNet.XUnitExtensions; + +#if DEBUG +namespace System.Net.Security.Tests +{ + using Configuration = System.Net.Test.Common.Configuration; + + public class SslStreamTlsResumeTests + { + private static FieldInfo connectionInfo = typeof(SslStream).GetField( + "_connectionInfo", + BindingFlags.Instance | BindingFlags.NonPublic); + + private bool CheckResumeFlag(SslStream ssl) + { + // This works only on Debug build where SslStream has extra property so we can validate. + object info = connectionInfo.GetValue(ssl); + return (bool)info.GetType().GetProperty("TlsResumed").GetValue(info); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + public async Task SslStream_ClientDisableTlsResume_Succeeds(bool testClient) + { + SslServerAuthenticationOptions serverOptions = new SslServerAuthenticationOptions + { + ServerCertificateContext = SslStreamCertificateContext.Create(Configuration.Certificates.GetServerCertificate(), null, false) + }; + + SslClientAuthenticationOptions clientOptions = new SslClientAuthenticationOptions + { + TargetHost = Guid.NewGuid().ToString("N"), + EnabledSslProtocols = SslProtocols.Tls12, + CertificateRevocationCheckMode = X509RevocationMode.NoCheck, + RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true, + }; + + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + Assert.True(client.IsAuthenticated); + Assert.True(client.IsEncrypted); + await client.ShutdownAsync(); + await server.ShutdownAsync(); + client.Dispose(); + server.Dispose(); + + // create new TLS to the same server. This should resume TLS. + (client, server) = TestHelper.GetConnectedSslStreams(); + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + //Assert.True(CheckResumeFlag(client)); + if (!CheckResumeFlag(client)) + { + throw new SkipTestException("Unable to resume test session"); + } + Assert.True(CheckResumeFlag(server)); + await client.ShutdownAsync(); + await server.ShutdownAsync(); + client.Dispose(); + server.Dispose(); + + // Disable TLS resumption and try it again. + if (testClient) + { + clientOptions.AllowTlsResume = false; + } + else + { + serverOptions.AllowTlsResume = false; + } + + // We do multiple loops to also cover credential cache. + for (int i=0; i < 3; i++) + { + (client, server) = TestHelper.GetConnectedSslStreams(); + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + Assert.False(CheckResumeFlag(client), $"TLS session resumed in round ${i}"); + Assert.False(CheckResumeFlag(server), $"TLS session resumed in round ${i}"); + await client.ShutdownAsync(); + await server.ShutdownAsync(); + client.Dispose(); + server.Dispose(); + } + + // TLS resume still should be possible + if (testClient) + { + clientOptions.AllowTlsResume = true; + } + else + { + serverOptions.AllowTlsResume = true; + } + + // On Windows it may take extra round to refresh the session cache. + (client, server) = TestHelper.GetConnectedSslStreams(); + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + client.Dispose(); + server.Dispose(); + + (client, server) = TestHelper.GetConnectedSslStreams(); + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions), + server.AuthenticateAsServerAsync(serverOptions)); + + Assert.True(CheckResumeFlag(client)); + Assert.True(CheckResumeFlag(server)); + client.Dispose(); + server.Dispose(); + } + } +} +#endif diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj index 0db9e97360eaf..b72ebc87fa38a 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj @@ -105,6 +105,7 @@ +