diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/ISSPIInterface.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/ISSPIInterface.cs index b1d82a736d58e9..62daefd267e77f 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/ISSPIInterface.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/ISSPIInterface.cs @@ -14,6 +14,7 @@ internal interface ISSPIInterface int EnumerateSecurityPackages(out int pkgnum, out SafeFreeContextBuffer pkgArray); int AcquireCredentialsHandle(string moduleName, Interop.SspiCli.CredentialUse usage, ref SafeSspiAuthDataHandle authdata, out SafeFreeCredentials outCredential); int AcquireCredentialsHandle(string moduleName, Interop.SspiCli.CredentialUse usage, ref Interop.SspiCli.SCHANNEL_CRED authdata, out SafeFreeCredentials outCredential); + unsafe int AcquireCredentialsHandle(string moduleName, Interop.SspiCli.CredentialUse usage, Interop.SspiCli.SCH_CREDENTIALS* authdata, out SafeFreeCredentials outCredential); int AcquireDefaultCredential(string moduleName, Interop.SspiCli.CredentialUse usage, out SafeFreeCredentials outCredential); int AcceptSecurityContext(SafeFreeCredentials? credential, ref SafeDeleteSslContext? context, InputSecurityBuffers inputBuffers, Interop.SspiCli.ContextFlags inFlags, Interop.SspiCli.Endianness endianness, ref SecurityBuffer outputBuffer, ref Interop.SspiCli.ContextFlags outFlags); int InitializeSecurityContext(ref SafeFreeCredentials? credential, ref SafeDeleteSslContext? context, string? targetName, Interop.SspiCli.ContextFlags inFlags, Interop.SspiCli.Endianness endianness, InputSecurityBuffers inputBuffers, ref SecurityBuffer outputBuffer, ref Interop.SspiCli.ContextFlags outFlags); 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 9db80f3ae96546..9556c5054533aa 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs @@ -214,6 +214,92 @@ public enum Flags } } + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct SCH_CREDENTIALS + { + public const int CurrentVersion = 0x5; + + public int dwVersion; + public int dwCredformat; + public int cCreds; + + // This is pointer to arry of CERT_CONTEXT* + // We do not use it directly in .NET. Instead, we wrap returned OS pointer in safe handle. + public void* paCred; + + public IntPtr hRootStore; // == always null, OTHERWISE NOT RELIABLE + public int cMappers; + public IntPtr aphMappers; // == always null, OTHERWISE NOT RELIABLE + + public int dwSessionLifespan; + public SCH_CREDENTIALS.Flags dwFlags; + public int cTlsParameters; + public TLS_PARAMETERS* pTlsParameters; + + [Flags] + public enum Flags + { + Zero = 0, + SCH_CRED_NO_SYSTEM_MAPPER = 0x02, + SCH_CRED_NO_SERVERNAME_CHECK = 0x04, + SCH_CRED_MANUAL_CRED_VALIDATION = 0x08, + SCH_CRED_NO_DEFAULT_CREDS = 0x10, + SCH_CRED_AUTO_CRED_VALIDATION = 0x20, + SCH_CRED_USE_DEFAULT_CREDS = 0x40, + SCH_DISABLE_RECONNECTS = 0x80, + SCH_CRED_REVOCATION_CHECK_END_CERT = 0x100, + SCH_CRED_REVOCATION_CHECK_CHAIN = 0x200, + SCH_CRED_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT = 0x400, + SCH_CRED_IGNORE_NO_REVOCATION_CHECK = 0x800, + SCH_CRED_IGNORE_REVOCATION_OFFLINE = 0x1000, + SCH_CRED_CACHE_ONLY_URL_RETRIEVAL_ON_CREATE = 0x2000, + SCH_SEND_ROOT_CERT = 0x40000, + SCH_SEND_AUX_RECORD = 0x00200000, + SCH_USE_STRONG_CRYPTO = 0x00400000, + SCH_USE_PRESHAREDKEY_ONLY = 0x800000, + SCH_ALLOW_NULL_ENCRYPTION = 0x02000000, + } + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct TLS_PARAMETERS + { + public int cAlpnIds; // Valid for server applications only. Must be zero otherwise. Number of ALPN IDs in rgstrAlpnIds; set to 0 if applies to all. + public IntPtr rgstrAlpnIds; // Valid for server applications only. Must be NULL otherwise. Array of ALPN IDs that the following settings apply to; set to NULL if applies to all. + public uint grbitDisabledProtocols; // List protocols you DO NOT want negotiated. + public int cDisabledCrypto; // Number of CRYPTO_SETTINGS structures; set to 0 if there are none. + public CRYPTO_SETTINGS* pDisabledCrypto; // Array of CRYPTO_SETTINGS structures; set to NULL if there are none; + public TLS_PARAMETERS.Flags dwFlags; // Optional flags to pass; set to 0 if there are none. + + [Flags] + public enum Flags + { + Zero = 0, + TLS_PARAMS_OPTIONAL = 0x01, // Valid for server applications only. Must be zero otherwise. + // TLS_PARAMETERS that will only be honored if they do not cause this server to terminate the handshake. + } + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct CRYPTO_SETTINGS + { + public TlsAlgorithmUsage eAlgorithmUsage; // How this algorithm is being used. + public UNICODE_STRING* strCngAlgId; // CNG algorithm identifier. + public int cChainingModes; // Set to 0 if CNG algorithm does not have a chaining mode. + public UNICODE_STRING* rgstrChainingModes; // Set to NULL if CNG algorithm does not have a chaining mode. + public int dwMinBitLength; // Blacklist key sizes less than this. Set to 0 if not defined or CNG algorithm implies bit length. + public int dwMaxBitLength; // Blacklist key sizes greater than this. Set to 0 if not defined or CNG algorithm implies bit length. + + public enum TlsAlgorithmUsage + { + TlsParametersCngAlgUsageKeyExchange, // Key exchange algorithm. RSA, ECHDE, DHE, etc. + TlsParametersCngAlgUsageSignature, // Signature algorithm. RSA, DSA, ECDSA, etc. + TlsParametersCngAlgUsageCipher, // Encryption algorithm. AES, DES, RC4, etc. + TlsParametersCngAlgUsageDigest, // Digest of cipher suite. SHA1, SHA256, SHA384, etc. + TlsParametersCngAlgUsageCertSig // Signature and/or hash used to sign certificate. RSA, DSA, ECDSA, SHA1, SHA256, etc. + } + } + [StructLayout(LayoutKind.Sequential)] internal unsafe struct SecBuffer { @@ -344,6 +430,20 @@ internal static extern unsafe int AcquireCredentialsHandleW( [Out] out long timeStamp ); + [DllImport(Interop.Libraries.SspiCli, ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern unsafe int AcquireCredentialsHandleW( + [In] string? principal, + [In] string moduleName, + [In] int usage, + [In] void* logonID, + [In] SCH_CREDENTIALS* authData, + [In] void* keyCallback, + [In] void* keyArgument, + ref CredHandle handlePtr, + [Out] out long timeStamp + ); + + [DllImport(Interop.Libraries.SspiCli, ExactSpelling = true, SetLastError = true)] internal static extern unsafe int InitializeSecurityContextW( ref CredHandle credentialHandle, diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIAuthType.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIAuthType.cs index a29658dd67c44a..7d359791a0fe11 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIAuthType.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIAuthType.cs @@ -45,6 +45,11 @@ public int AcquireCredentialsHandle(string moduleName, Interop.SspiCli.Credentia return SafeFreeCredentials.AcquireCredentialsHandle(moduleName, usage, ref authdata, out outCredential); } + public unsafe int AcquireCredentialsHandle(string moduleName, Interop.SspiCli.CredentialUse usage, Interop.SspiCli.SCH_CREDENTIALS* authdata, out SafeFreeCredentials outCredential) + { + return SafeFreeCredentials.AcquireCredentialsHandle(moduleName, usage, authdata, out outCredential); + } + public int AcceptSecurityContext(SafeFreeCredentials? credential, ref SafeDeleteSslContext? context, InputSecurityBuffers inputBuffers, Interop.SspiCli.ContextFlags inFlags, Interop.SspiCli.Endianness endianness, ref SecurityBuffer outputBuffer, ref Interop.SspiCli.ContextFlags outFlags) { return SafeDeleteContext.AcceptSecurityContext(ref credential, ref context, inFlags, endianness, inputBuffers, ref outputBuffer, ref outFlags); diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPISecureChannelType.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPISecureChannelType.cs index 0e9bc6275aa969..e30e53d27262a8 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPISecureChannelType.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPISecureChannelType.cs @@ -45,6 +45,11 @@ public int AcquireCredentialsHandle(string moduleName, Interop.SspiCli.Credentia return SafeFreeCredentials.AcquireCredentialsHandle(moduleName, usage, ref authdata, out outCredential); } + public unsafe int AcquireCredentialsHandle(string moduleName, Interop.SspiCli.CredentialUse usage, Interop.SspiCli.SCH_CREDENTIALS* authdata, out SafeFreeCredentials outCredential) + { + return SafeFreeCredentials.AcquireCredentialsHandle(moduleName, usage, authdata, out outCredential); + } + public int AcceptSecurityContext(SafeFreeCredentials? credential, ref SafeDeleteSslContext? context, InputSecurityBuffers inputBuffers, Interop.SspiCli.ContextFlags inFlags, Interop.SspiCli.Endianness endianness, ref SecurityBuffer outputBuffer, ref Interop.SspiCli.ContextFlags outFlags) { return SafeDeleteContext.AcceptSecurityContext(ref credential, ref context, inFlags, endianness, inputBuffers, ref outputBuffer, ref outFlags); diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs index 74772416a15c55..eaf912697a91ca 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SSPIWrapper.cs @@ -110,14 +110,28 @@ public static SafeFreeCredentials AcquireCredentialsHandle(ISSPIInterface secMod public static SafeFreeCredentials AcquireCredentialsHandle(ISSPIInterface secModule, string package, Interop.SspiCli.CredentialUse intent, Interop.SspiCli.SCHANNEL_CRED scc) { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Log.AcquireCredentialsHandle(package, intent, scc); - - SafeFreeCredentials? outCredential = null; int errorCode = secModule.AcquireCredentialsHandle( package, intent, ref scc, - out outCredential); + out SafeFreeCredentials outCredential); + + if (errorCode != 0) + { + if (NetEventSource.IsEnabled) NetEventSource.Error(null, SR.Format(SR.net_log_operation_failed_with_error, nameof(AcquireCredentialsHandle), $"0x{errorCode:X}")); + throw new Win32Exception(errorCode); + } + + return outCredential; + } + + public static unsafe SafeFreeCredentials AcquireCredentialsHandle(ISSPIInterface secModule, string package, Interop.SspiCli.CredentialUse intent, Interop.SspiCli.SCH_CREDENTIALS* scc) + { + int errorCode = secModule.AcquireCredentialsHandle( + package, + intent, + scc, + out SafeFreeCredentials outCredential); if (errorCode != 0) { diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs index b4f3eb526723b8..bef2693adb8823 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs @@ -301,6 +301,38 @@ public static unsafe int AcquireCredentialsHandle( return errorCode; } + + public static unsafe int AcquireCredentialsHandle( + string package, + Interop.SspiCli.CredentialUse intent, + Interop.SspiCli.SCH_CREDENTIALS* authdata, + out SafeFreeCredentials outCredential) + { + long timeStamp; + + outCredential = new SafeFreeCredential_SECURITY(); + + int errorCode = Interop.SspiCli.AcquireCredentialsHandleW( + null, + package, + (int)intent, + null, + authdata, + null, + null, + ref outCredential._handle, + out timeStamp); + + if (NetEventSource.IsEnabled) NetEventSource.Verbose(null, $"{nameof(Interop.SspiCli.AcquireCredentialsHandleW)} returns 0x{errorCode:x}, handle = {outCredential}"); + + if (errorCode != 0) + { + outCredential.SetHandleAsInvalid(); + } + + return errorCode; + } + } // diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index ace5d2ba28dbfd..65d63c045bbf58 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -429,6 +429,8 @@ + + + diff --git a/src/libraries/System.Net.Mail/tests/Unit/System.Net.Mail.Unit.Tests.csproj b/src/libraries/System.Net.Mail/tests/Unit/System.Net.Mail.Unit.Tests.csproj index 70ee6946ba0886..6b9e7bd1dfa49d 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/System.Net.Mail.Unit.Tests.csproj +++ b/src/libraries/System.Net.Mail/tests/Unit/System.Net.Mail.Unit.Tests.csproj @@ -261,5 +261,7 @@ Link="Common\Interop\Windows\SspiCli\SecuritySafeHandles.cs" /> + 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 e0edf84a0faa32..da7a225cce9ba7 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -146,6 +146,8 @@ + + = 10 && Environment.OSVersion.Version.Build >= 18836; + private const string SecurityPackage = "Microsoft Unified Security Protocol Provider"; private const Interop.SspiCli.ContextFlags RequiredFlags = @@ -106,6 +111,16 @@ public static SecurityStatusPal InitializeSecurityContext(ref SafeFreeCredential } public static SafeFreeCredentials AcquireCredentialsHandle(X509Certificate? certificate, SslProtocols protocols, EncryptionPolicy policy, bool isServer) + { + // New crypto API supports TLS1.3 but it does not allow to force NULL encryption. + return !UseNewCryptoApi || policy == EncryptionPolicy.NoEncryption ? + AcquireCredentialsHandleSchannelCred(certificate, protocols, policy, isServer) : + AcquireCredentialsHandleSchCredentials(certificate, protocols, policy, isServer); + } + + // This is legacy crypto API used on .NET Framework and older Windows versions. + // It only supports TLS up to 1.2 + public static SafeFreeCredentials AcquireCredentialsHandleSchannelCred(X509Certificate? certificate, SslProtocols protocols, EncryptionPolicy policy, bool isServer) { int protocolFlags = GetProtocolFlagsFromSslProtocols(protocols, isServer); Interop.SspiCli.SCHANNEL_CRED.Flags flags; @@ -144,6 +159,72 @@ public static SafeFreeCredentials AcquireCredentialsHandle(X509Certificate? cert return AcquireCredentialsHandle(direction, secureCredential); } + // This function uses new crypto API to support TLS 1.3 and beyond. + public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials(X509Certificate? certificate, SslProtocols protocols, EncryptionPolicy policy, bool isServer) + { + int protocolFlags = GetProtocolFlagsFromSslProtocols(protocols, isServer); + Interop.SspiCli.SCH_CREDENTIALS.Flags flags; + Interop.SspiCli.CredentialUse direction; + + if (isServer) + { + direction = Interop.SspiCli.CredentialUse.SECPKG_CRED_INBOUND; + flags = Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_SEND_AUX_RECORD; + } + else + { + direction = Interop.SspiCli.CredentialUse.SECPKG_CRED_OUTBOUND; + flags = + Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_MANUAL_CRED_VALIDATION | + Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_CRED_NO_DEFAULT_CREDS | + Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_SEND_AUX_RECORD; + } + + if (policy == EncryptionPolicy.RequireEncryption) + { + // Always opt-in SCH_USE_STRONG_CRYPTO for TLS. + if (!isServer && ((protocolFlags & Interop.SChannel.SP_PROT_SSL3) == 0)) + { + flags |= Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_USE_STRONG_CRYPTO; + } + } + else if (policy == EncryptionPolicy.AllowNoEncryption) + { + // Allow null encryption cipher in addition to other ciphers. + flags |= Interop.SspiCli.SCH_CREDENTIALS.Flags.SCH_ALLOW_NULL_ENCRYPTION; + } + else + { + throw new ArgumentException(SR.Format(SR.net_invalid_enum, "EncryptionPolicy"), nameof(policy)); + } + + Interop.SspiCli.SCH_CREDENTIALS credential = default; + credential.dwVersion = Interop.SspiCli.SCH_CREDENTIALS.CurrentVersion; + credential.dwFlags = flags; + + IntPtr certificateHandle = IntPtr.Zero; + if (certificate != null) + { + credential.cCreds = 1; + certificateHandle = certificate.Handle; + credential.paCred = &certificateHandle; + } + + if (NetEventSource.IsEnabled) NetEventSource.Info($"flags=({flags}), ProtocolFlags=({protocolFlags}), EncryptionPolicy={policy}"); + + if (protocolFlags != 0) + { + // If we were asked to do specific protocol we need to fill TLS_PARAMETERS. + Interop.SspiCli.TLS_PARAMETERS tlsParameters = default; + tlsParameters.grbitDisabledProtocols = (uint)protocolFlags ^ uint.MaxValue; + + credential.cTlsParameters = 1; + credential.pTlsParameters = &tlsParameters; + } + + return AcquireCredentialsHandle(direction, &credential); + } + internal static byte[]? GetNegotiatedApplicationProtocol(SafeDeleteContext context) { Interop.SecPkgContext_ApplicationProtocol alpnContext = default; @@ -436,5 +517,26 @@ private static SafeFreeCredentials AcquireCredentialsHandle(Interop.SspiCli.Cred return SSPIWrapper.AcquireCredentialsHandle(GlobalSSPI.SSPISecureChannel, SecurityPackage, credUsage, secureCredential); } } + + private static unsafe SafeFreeCredentials AcquireCredentialsHandle(Interop.SspiCli.CredentialUse credUsage, Interop.SspiCli.SCH_CREDENTIALS* secureCredential) + { + // First try without impersonation, if it fails, then try the process account. + // I.E. We don't know which account the certificate context was created under. + try + { + // + // For app-compat we want to ensure the credential are accessed under >>process<< account. + // + return WindowsIdentity.RunImpersonated(SafeAccessTokenHandle.InvalidHandle, () => + { + return SSPIWrapper.AcquireCredentialsHandle(GlobalSSPI.SSPISecureChannel, SecurityPackage, credUsage, secureCredential); + }); + } + catch + { + return SSPIWrapper.AcquireCredentialsHandle(GlobalSSPI.SSPISecureChannel, SecurityPackage, credUsage, secureCredential); + } + } + } } diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs index 3cb3f5c5363fd3..96f293c7685271 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlertsTest.cs @@ -87,13 +87,14 @@ public async Task SslStream_StreamToStream_ServerInitiatedCloseNotify_Ok() } } - [Fact] - public async Task SslStream_StreamToStream_ClientInitiatedCloseNotify_Ok() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task SslStream_StreamToStream_ClientInitiatedCloseNotify_Ok(bool sendData) { - VirtualNetwork network = new VirtualNetwork(); - - using (var clientStream = new VirtualNetworkStream(network, isServer: false)) - using (var serverStream = new VirtualNetworkStream(network, isServer: true)) + (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); + using (clientStream) + using (serverStream) using (var client = new SslStream(clientStream, true, AllowAnyServerCertificate)) using (var server = new SslStream(serverStream)) using (X509Certificate2 certificate = Configuration.Certificates.GetServerCertificate()) @@ -105,13 +106,21 @@ public async Task SslStream_StreamToStream_ClientInitiatedCloseNotify_Ok() await Task.WhenAll(handshake).TimeoutAfter(TestConfiguration.PassingTestTimeoutMilliseconds); + var readBuffer = new byte[1024]; + if (sendData) + { + // Send some data before shutting down. This may matter for TLS13. + handshake[0] = server.WriteAsync(readBuffer, 0, 1); + handshake[1] = client.ReadAsync(readBuffer, 0, 1); + await Task.WhenAll(handshake).TimeoutAfter(TestConfiguration.PassingTestTimeoutMilliseconds); + } + await client.ShutdownAsync(); int bytesRead = await server.ReadAsync(readBuffer, 0, readBuffer.Length); // close_notify received by the server. Assert.Equal(0, bytesRead); - await server.ShutdownAsync(); bytesRead = await client.ReadAsync(readBuffer, 0, readBuffer.Length); // close_notify received by the client. diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs index 9de96182671314..7f76226b39f639 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/TestHelper.cs @@ -33,6 +33,7 @@ public static (Stream ClientStream, Stream ServerStream) GetConnectedStreams() { if (Capability.SecurityForceSocketStreams()) { + // DOTNET_TEST_NET_SECURITY_FORCE_SOCKET_STREAMS is set. return GetConnectedTcpStreams(); }