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 b675f7f022473..1694ca539a86a 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/Interop.SSPI.cs @@ -65,6 +65,7 @@ internal enum ContextAttribute SECPKG_ATTR_LOCAL_CERT_CONTEXT = 0x54, // returns PCCERT_CONTEXT SECPKG_ATTR_ROOT_STORE = 0x55, // returns HCERTCONTEXT to the root store SECPKG_ATTR_ISSUER_LIST_EX = 0x59, // returns SecPkgContext_IssuerListInfoEx + SECPKG_ATTR_CLIENT_CERT_POLICY = 0x60, // sets SecPkgCred_ClientCertCtlPolicy SECPKG_ATTR_CONNECTION_INFO = 0x5A, // returns SecPkgContext_ConnectionInfo SECPKG_ATTR_CIPHER_INFO = 0x64, // returns SecPkgContext_CipherInfo SECPKG_ATTR_UI_INFO = 0x68, // sets SEcPkgContext_UiInfo @@ -315,6 +316,20 @@ public SecBufferDesc(int count) } } + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct SecPkgCred_ClientCertPolicy + { + public uint dwFlags; + public Guid guidPolicyId; + public uint dwCertFlags; + public uint dwUrlRetrievalTimeout; + public BOOL fCheckRevocationFreshnessTime; + public uint dwRevocationFreshnessTime; + public BOOL fOmitUsageCheck; + public char* pwszSslCtlStoreName; + public char* pwszSslCtlIdentifier; + } + [DllImport(Interop.Libraries.SspiCli, ExactSpelling = true, SetLastError = true)] internal static extern int EncryptMessage( ref CredHandle contextHandle, @@ -472,5 +487,12 @@ internal static extern SECURITY_STATUS SspiEncodeStringsAsAuthIdentity( [In] string domainName, [In] string password, [Out] out SafeSspiAuthDataHandle authData); + + [DllImport(Interop.Libraries.SspiCli, ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern SECURITY_STATUS SetCredentialsAttributesW( + [In] ref CredHandle handlePtr, + [In] long ulAttribute, + [In] ref SecPkgCred_ClientCertPolicy pBuffer, + [In] long cbBuffer); } } 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 0fee14684e2ea..dc5b421e19cad 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -386,10 +386,12 @@ + + Link="Common\Interop\Windows\Interop.UNICODE_STRING.cs" /> + + Setting an SNI hostname is not supported on this API level. + + Only LocalMachine stores are supported on Windows. + + + Sending trust from collection is not supported on Windows. + 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 100115a0920a2..ca89fca29fa5b 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -4,6 +4,7 @@ $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Android;$(NetCoreAppCurrent)-OSX;$(NetCoreAppCurrent)-iOS;$(NetCoreAppCurrent)-tvOS;$(NetCoreAppCurrent) $(DefineConstants);PRODUCT + $(DefineConstants);TARGET_WINDOWS enable @@ -29,6 +30,7 @@ + @@ -156,6 +158,8 @@ + buffer, out int outputOffset, out --*/ //This method validates a remote certificate. - internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remoteCertValidationCallback, ref ProtocolToken? alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus) + internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remoteCertValidationCallback, SslCertificateTrust? trust, ref ProtocolToken? alertToken, out SslPolicyErrors sslPolicyErrors, out X509ChainStatusFlags chainStatus) { sslPolicyErrors = SslPolicyErrors.None; chainStatus = X509ChainStatusFlags.NoError; @@ -965,6 +965,19 @@ internal bool VerifyRemoteCertificate(RemoteCertificateValidationCallback? remot chain.ChainPolicy.ExtraStore.AddRange(remoteCertificateStore); } + if (trust != null) + { + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + if (trust._store != null) + { + chain.ChainPolicy.CustomTrustStore.AddRange(trust._store.Certificates); + } + if (trust._trustList != null) + { + chain.ChainPolicy.CustomTrustStore.AddRange(trust._trustList); + } + } + sslPolicyErrors |= CertificateValidationPal.VerifyCertificateProperties( _securityContext!, chain, diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslCertificateTrust.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslCertificateTrust.cs new file mode 100644 index 0000000000000..982d206c85935 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslCertificateTrust.cs @@ -0,0 +1,64 @@ +// 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.Versioning; +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Security +{ + public sealed class SslCertificateTrust + { + internal X509Store? _store; + internal X509Certificate2Collection? _trustList; + internal bool _sendTrustInHandshake; + + public static SslCertificateTrust CreateForX509Store(X509Store store, bool sendTrustInHandshake = false) + { + +#if TARGET_WINDOWS + if (sendTrustInHandshake && store.Location != StoreLocation.LocalMachine) + { + throw new PlatformNotSupportedException(SR.net_ssl_trust_store); + } +#else + if (sendTrustInHandshake) + { + // to be removed when implemented. + throw new PlatformNotSupportedException("Not supported yet."); + } +#endif + if (!store.IsOpen) + { + store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); + } + + var trust = new SslCertificateTrust(); + trust._store = store; + trust._sendTrustInHandshake = sendTrustInHandshake; + return trust; + } + + [UnsupportedOSPlatform("windows")] + public static SslCertificateTrust CreateForX509Collection(X509Certificate2Collection trustList, bool sendTrustInHandshake = false) + { + if (sendTrustInHandshake) + { + // to be removed when implemented. + throw new PlatformNotSupportedException("Not supported yet."); + } + +#if TARGET_WINDOWS + if (sendTrustInHandshake) + { + throw new PlatformNotSupportedException(SR.net_ssl_trust_collection); + } +#endif + var trust = new SslCertificateTrust(); + trust._trustList = trustList; + trust._sendTrustInHandshake = sendTrustInHandshake; + return trust; + } + + private SslCertificateTrust() { } + } +} diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs index 17858bb0e68b6..2ecc2a3932abf 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs @@ -664,7 +664,7 @@ private bool CompleteHandshake(ref ProtocolToken? alertToken, out SslPolicyError return true; } - if (!_context.VerifyRemoteCertificate(_sslAuthenticationOptions!.CertValidationDelegate, ref alertToken, out sslPolicyErrors, out chainStatus)) + if (!_context.VerifyRemoteCertificate(_sslAuthenticationOptions!.CertValidationDelegate, _sslAuthenticationOptions!.CertificateContext?.Trust, ref alertToken, out sslPolicyErrors, out chainStatus)) { _handshakeCompleted = false; return false; diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs index f5e9dd2114d61..facb437f02b42 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs @@ -9,10 +9,11 @@ public partial class SslStreamCertificateContext { private const bool TrimRootCertificate = true; - private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates) + private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates, SslCertificateTrust? trust) { Certificate = target; IntermediateCertificates = intermediates; + Trust = trust; } internal static SslStreamCertificateContext Create(X509Certificate2 target) => Create(target, null); diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.OSX.cs index 4579f15407fde..9cef66806ac06 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.OSX.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.OSX.cs @@ -10,16 +10,17 @@ public partial class SslStreamCertificateContext // No leaf, no root. private const bool TrimRootCertificate = true; - private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates) + private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates, SslCertificateTrust? trust) { Certificate = target; IntermediateCertificates = intermediates; + Trust = trust; } internal static SslStreamCertificateContext Create(X509Certificate2 target) { // On OSX we do not need to build chain unless we are asked for it. - return new SslStreamCertificateContext(target, Array.Empty()); + return new SslStreamCertificateContext(target, Array.Empty(), null); } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs index fcb84b2dda6a0..699a5c8c01239 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs @@ -13,10 +13,10 @@ public partial class SslStreamCertificateContext internal static SslStreamCertificateContext Create(X509Certificate2 target) { // On Windows we do not need to build chain unless we are asked for it. - return new SslStreamCertificateContext(target, Array.Empty()); + return new SslStreamCertificateContext(target, Array.Empty(), null); } - private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates) + private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates, SslCertificateTrust? trust) { if (intermediates.Length > 0) { @@ -103,6 +103,7 @@ private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] Certificate = target; IntermediateCertificates = intermediates; + Trust = trust; } } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs index 4ae127aadcc15..c585d7e033de0 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel; using System.Security.Cryptography.X509Certificates; namespace System.Net.Security @@ -9,8 +10,15 @@ public partial class SslStreamCertificateContext { internal readonly X509Certificate2 Certificate; internal readonly X509Certificate2[] IntermediateCertificates; + internal readonly SslCertificateTrust? Trust; - public static SslStreamCertificateContext Create(X509Certificate2 target, X509Certificate2Collection? additionalCertificates, bool offline = false) + [EditorBrowsable(EditorBrowsableState.Never)] + public static SslStreamCertificateContext Create(X509Certificate2 target, X509Certificate2Collection? additionalCertificates, bool offline) + { + return Create(target, additionalCertificates, offline, null); + } + + public static SslStreamCertificateContext Create(X509Certificate2 target, X509Certificate2Collection? additionalCertificates, bool offline = false, SslCertificateTrust? trust = null) { if (!target.HasPrivateKey) { @@ -81,13 +89,12 @@ public static SslStreamCertificateContext Create(X509Certificate2 target, X509Ce } } - return new SslStreamCertificateContext(target, intermediates); + return new SslStreamCertificateContext(target, intermediates, trust); } internal SslStreamCertificateContext Duplicate() { - return new SslStreamCertificateContext(new X509Certificate2(Certificate), IntermediateCertificates); - + return new SslStreamCertificateContext(new X509Certificate2(Certificate), IntermediateCertificates, Trust); } } } 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 4060296c4ed87..29ab3bed0ee8c 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 @@ -4,11 +4,13 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Authentication; using System.Security.Authentication.ExtendedProtection; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; +using System.Text; using Microsoft.Win32.SafeHandles; namespace System.Net.Security @@ -121,14 +123,41 @@ public static SecurityStatusPal Renegotiate(ref SafeFreeCredentials? credentials public static SafeFreeCredentials AcquireCredentialsHandle(SslStreamCertificateContext? certificateContext, 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 ? + SafeFreeCredentials cred = !UseNewCryptoApi || policy == EncryptionPolicy.NoEncryption ? AcquireCredentialsHandleSchannelCred(certificateContext?.Certificate, protocols, policy, isServer) : AcquireCredentialsHandleSchCredentials(certificateContext?.Certificate, protocols, policy, isServer); + if (certificateContext != null && certificateContext.Trust != null && certificateContext.Trust._sendTrustInHandshake) + { + AttachCertificateStore(cred, certificateContext.Trust._store!); + } + + return cred; + } + + private static unsafe void AttachCertificateStore(SafeFreeCredentials cred, X509Store store) + { + Interop.SspiCli.SecPkgCred_ClientCertPolicy clientCertPolicy = default; + fixed (char* ptr = store.Name) + { + clientCertPolicy.pwszSslCtlStoreName = ptr; + Interop.SECURITY_STATUS errorCode = Interop.SspiCli.SetCredentialsAttributesW( + ref cred._handle, + (long)Interop.SspiCli.ContextAttribute.SECPKG_ATTR_CLIENT_CERT_POLICY, + ref clientCertPolicy, + sizeof(Interop.SspiCli.SecPkgCred_ClientCertPolicy)); + + if (errorCode != Interop.SECURITY_STATUS.OK) + { + throw new Win32Exception((int)errorCode); + } + } + + return; } // This is legacy crypto API used on .NET Framework and older Windows versions. // It only supports TLS up to 1.2 - public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(X509Certificate? certificate, SslProtocols protocols, EncryptionPolicy policy, bool isServer) + public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(X509Certificate2? certificate, SslProtocols protocols, EncryptionPolicy policy, bool isServer) { int protocolFlags = GetProtocolFlagsFromSslProtocols(protocols, isServer); Interop.SspiCli.SCHANNEL_CRED.Flags flags; @@ -174,12 +203,11 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchannelCred(X5 } // 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) + public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials(X509Certificate2? 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; @@ -215,7 +243,6 @@ public static unsafe SafeFreeCredentials AcquireCredentialsHandleSchCredentials( Interop.SspiCli.SCH_CREDENTIALS credential = default; credential.dwVersion = Interop.SspiCli.SCH_CREDENTIALS.CurrentVersion; credential.dwFlags = flags; - Interop.Crypt32.CERT_CONTEXT *certificateHandle = null; if (certificate != null) { diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCredentialCacheTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCredentialCacheTest.cs index e2e8314249d36..f135095476320 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCredentialCacheTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamCredentialCacheTest.cs @@ -28,15 +28,13 @@ public async Task SslStream_SameCertUsedForClientAndServer_Ok() X509Certificate2Collection clientCertificateCollection = new X509Certificate2Collection(certificate); - var tasks = new Task[2]; - - tasks[0] = server.AuthenticateAsServerAsync(certificate, true, false); - tasks[1] = client.AuthenticateAsClientAsync( + Task t1 = server.AuthenticateAsServerAsync(certificate, true, false); + Task t2 = client.AuthenticateAsClientAsync( certificate.GetNameInfo(X509NameType.SimpleName, false), clientCertificateCollection, false); - await Task.WhenAll(tasks).WaitAsync(TestConfiguration.PassingTestTimeout); + await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2); if (!PlatformDetection.IsWindows7 || Capability.IsTrustedRootCertificateInstalled())