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 e522e7ee6dff62..52b1a5b1dbf474 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 @@ -199,42 +199,11 @@ internal static SafeSslHandle AllocateSslContext(SslProtocols protocols, SafeX50 } } - if (hasCertificateAndKey) + if (sslAuthenticationOptions.CertificateContext != null && sslAuthenticationOptions.CertificateContext.IntermediateCertificates.Length > 0) { - bool hasCertReference = false; - try + if (!Ssl.AddExtraChainCertificates(context, sslAuthenticationOptions.CertificateContext!.IntermediateCertificates)) { - certHandle!.DangerousAddRef(ref hasCertReference); - using (X509Certificate2 cert = new X509Certificate2(certHandle.DangerousGetHandle())) - { - X509Chain? chain = null; - try - { - chain = TLSCertificateExtensions.BuildNewChain(cert, includeClientApplicationPolicy: false); - if (chain != null && !Ssl.AddExtraChainCertificates(context, chain)) - { - throw CreateSslException(SR.net_ssl_use_cert_failed); - } - } - finally - { - if (chain != null) - { - int elementsCount = chain.ChainElements.Count; - for (int i = 0; i < elementsCount; i++) - { - chain.ChainElements[i].Certificate!.Dispose(); - } - - chain.Dispose(); - } - } - } - } - finally - { - if (hasCertReference) - certHandle!.DangerousRelease(); + throw CreateSslException(SR.net_ssl_use_cert_failed); } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs index b7edaf3919684b..81799ae6e0946e 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs @@ -193,6 +193,25 @@ internal static bool AddExtraChainCertificates(SafeSslHandle sslContext, X509Cha return true; } + internal static bool AddExtraChainCertificates(SafeSslHandle sslContext, X509Certificate2[] chain) + { + // send pre-computed list of intermediates. + for (int i = 0; i < chain.Length; i++) + { + SafeX509Handle dupCertHandle = Crypto.X509UpRef(chain[i].Handle); + Crypto.CheckValidOpenSslHandle(dupCertHandle); + if (!SslAddExtraChainCert(sslContext, dupCertHandle)) + { + Crypto.ErrClearError(); + dupCertHandle.Dispose(); // we still own the safe handle; clean it up + return false; + } + dupCertHandle.SetHandleAsInvalid(); // ownership has been transferred to sslHandle; do not free via this safe handle + } + + return true; + } + internal static class SslMethods { internal static readonly IntPtr SSLv23_method = SslV2_3Method(); 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 d657bb1fd59d79..c47277547b74b3 100644 --- a/src/libraries/System.Net.Security/ref/System.Net.Security.cs +++ b/src/libraries/System.Net.Security/ref/System.Net.Security.cs @@ -124,6 +124,11 @@ public enum ProtectionLevel public static bool operator !=(System.Net.Security.SslApplicationProtocol left, System.Net.Security.SslApplicationProtocol right) { throw null; } public override string ToString() { throw null; } } + public sealed partial class SslStreamCertificateContext + { + internal SslStreamCertificateContext() { throw null; } + public static SslStreamCertificateContext Create(System.Security.Cryptography.X509Certificates.X509Certificate2 target, System.Security.Cryptography.X509Certificates.X509Certificate2Collection? additionalCertificates, bool offline = false) { throw null; } + } public partial class SslClientAuthenticationOptions { public SslClientAuthenticationOptions() { } @@ -150,6 +155,7 @@ public SslServerAuthenticationOptions() { } public System.Net.Security.EncryptionPolicy EncryptionPolicy { get { throw null; } set { } } public System.Net.Security.RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get { throw null; } set { } } public System.Security.Cryptography.X509Certificates.X509Certificate? ServerCertificate { get { throw null; } set { } } + public System.Net.Security.SslStreamCertificateContext? ServerCertificateContext { get { throw null; } set { } } public System.Net.Security.ServerCertificateSelectionCallback? ServerCertificateSelectionCallback { get { throw null; } set { } } } public partial class SslStream : System.Net.Security.AuthenticatedStream 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 a4a3356a8d200e..7827fa79c8a194 100644 --- a/src/libraries/System.Net.Security/src/System.Net.Security.csproj +++ b/src/libraries/System.Net.Security/src/System.Net.Security.csproj @@ -28,6 +28,7 @@ + @@ -125,6 +126,7 @@ + @@ -247,6 +249,7 @@ + @@ -328,6 +331,7 @@ + diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/NetEventSource.Security.cs b/src/libraries/System.Net.Security/src/System/Net/Security/NetEventSource.Security.cs index e21c04c8698acc..8de9ebe840c3e8 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/NetEventSource.Security.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/NetEventSource.Security.cs @@ -105,11 +105,11 @@ private unsafe void SecureChannelCtor(string sslStream, string hostname, int sec WriteEvent(SecureChannelCtorId, sslStream, hostname, secureChannelHash, clientCertificatesCount, (int)encryptionPolicy); [NonEvent] - public void LocatingPrivateKey(X509Certificate x509Certificate, SecureChannel secureChannel) + public void LocatingPrivateKey(X509Certificate x509Certificate, object instance) { if (IsEnabled()) { - LocatingPrivateKey(x509Certificate.ToString(true), GetHashCode(secureChannel)); + LocatingPrivateKey(x509Certificate.ToString(true), GetHashCode(instance)); } } [Event(LocatingPrivateKeyId, Keywords = Keywords.Default, Level = EventLevel.Informational)] @@ -117,11 +117,11 @@ private void LocatingPrivateKey(string x509Certificate, int secureChannelHash) = WriteEvent(LocatingPrivateKeyId, x509Certificate, secureChannelHash); [NonEvent] - public void CertIsType2(SecureChannel secureChannel) + public void CertIsType2(object instance) { if (IsEnabled()) { - CertIsType2(GetHashCode(secureChannel)); + CertIsType2(GetHashCode(instance)); } } [Event(CertIsType2Id, Keywords = Keywords.Default, Level = EventLevel.Informational)] @@ -129,11 +129,11 @@ private void CertIsType2(int secureChannelHash) => WriteEvent(CertIsType2Id, secureChannelHash); [NonEvent] - public void FoundCertInStore(bool serverMode, SecureChannel secureChannel) + public void FoundCertInStore(bool serverMode, object instance) { if (IsEnabled()) { - FoundCertInStore(serverMode ? "LocalMachine" : "CurrentUser", GetHashCode(secureChannel)); + FoundCertInStore(serverMode ? "LocalMachine" : "CurrentUser", GetHashCode(instance)); } } [Event(FoundCertInStoreId, Keywords = Keywords.Default, Level = EventLevel.Informational)] @@ -141,11 +141,11 @@ private void FoundCertInStore(string store, int secureChannelHash) => WriteEvent(FoundCertInStoreId, store, secureChannelHash); [NonEvent] - public void NotFoundCertInStore(SecureChannel secureChannel) + public void NotFoundCertInStore(object instance) { if (IsEnabled()) { - NotFoundCertInStore(GetHashCode(secureChannel)); + NotFoundCertInStore(GetHashCode(instance)); } } [Event(NotFoundCertInStoreId, Keywords = Keywords.Default, Level = EventLevel.Informational)] diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs index 8b7e39f46c3c24..01064218ec9c54 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs @@ -77,7 +77,7 @@ internal X509Certificate? LocalServerCertificate { get { - return _sslAuthenticationOptions.ServerCertificate; + return _sslAuthenticationOptions.CertificateContext?.Certificate; } } @@ -186,7 +186,7 @@ internal void Close() // SECURITY: we open a private key container on behalf of the caller // and we require the caller to have permission associated with that operation. // - private X509Certificate2? EnsurePrivateKey(X509Certificate certificate) + internal static X509Certificate2? FindCertificateWithPrivateKey(object instance, bool isServer, X509Certificate certificate) { if (certificate == null) { @@ -194,7 +194,7 @@ internal void Close() } if (NetEventSource.IsEnabled) - NetEventSource.Log.LocatingPrivateKey(certificate, this); + NetEventSource.Log.LocatingPrivateKey(certificate, instance); try { @@ -206,12 +206,12 @@ internal void Close() if (certEx.HasPrivateKey) { if (NetEventSource.IsEnabled) - NetEventSource.Log.CertIsType2(this); + NetEventSource.Log.CertIsType2(instance); return certEx; } - if ((object)certificate != (object)certEx) + if (!object.ReferenceEquals(certificate, certEx)) { certEx.Dispose(); } @@ -222,26 +222,26 @@ internal void Close() // ELSE Try the MY user and machine stores for private key check. // For server side mode MY machine store takes priority. - X509Store? store = CertificateValidationPal.EnsureStoreOpened(_sslAuthenticationOptions.IsServer); + X509Store? store = CertificateValidationPal.EnsureStoreOpened(isServer); if (store != null) { collectionEx = store.Certificates.Find(X509FindType.FindByThumbprint, certHash, false); if (collectionEx.Count > 0 && collectionEx[0].HasPrivateKey) { if (NetEventSource.IsEnabled) - NetEventSource.Log.FoundCertInStore(_sslAuthenticationOptions.IsServer, this); + NetEventSource.Log.FoundCertInStore(isServer, instance); return collectionEx[0]; } } - store = CertificateValidationPal.EnsureStoreOpened(!_sslAuthenticationOptions.IsServer); + store = CertificateValidationPal.EnsureStoreOpened(!isServer); if (store != null) { collectionEx = store.Certificates.Find(X509FindType.FindByThumbprint, certHash, false); if (collectionEx.Count > 0 && collectionEx[0].HasPrivateKey) { if (NetEventSource.IsEnabled) - NetEventSource.Log.FoundCertInStore(_sslAuthenticationOptions.IsServer, this); + NetEventSource.Log.FoundCertInStore(!isServer, instance); return collectionEx[0]; } } @@ -251,7 +251,7 @@ internal void Close() } if (NetEventSource.IsEnabled) - NetEventSource.Log.NotFoundCertInStore(this); + NetEventSource.Log.NotFoundCertInStore(instance); return null; } @@ -531,13 +531,13 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint) // // SECURITY: Accessing X509 cert Credential is disabled for semitrust. // We no longer need to demand for unmanaged code permissions. - // EnsurePrivateKey should do the right demand for us. + // FindCertificateWithPrivateKey should do the right demand for us. if (filteredCerts != null) { for (int i = 0; i < filteredCerts.Count; ++i) { clientCertificate = filteredCerts[i]; - if ((selectedCert = EnsurePrivateKey(clientCertificate)) != null) + if ((selectedCert = FindCertificateWithPrivateKey(this, _sslAuthenticationOptions.IsServer, clientCertificate)) != null) { break; } @@ -608,10 +608,9 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint) } finally { - // An extra cert could have been created, dispose it now. - if (selectedCert != null && (object?)clientCertificate != (object?)selectedCert) + if (selectedCert != null) { - selectedCert.Dispose(); + _sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(selectedCert); } } @@ -631,84 +630,91 @@ private bool AcquireServerCredentials(ref byte[]? thumbPrint) NetEventSource.Enter(this); X509Certificate? localCertificate = null; + X509Certificate2? selectedCert = null; bool cachedCred = false; // There are three options for selecting the server certificate. When // selecting which to use, we prioritize the new ServerCertSelectionDelegate // API. If the new API isn't used we call LocalCertSelectionCallback (for compat - // with .NET Framework), and if neither is set we fall back to using ServerCertificate. + // with .NET Framework), and if neither is set we fall back to using CertificateContext. if (_sslAuthenticationOptions.ServerCertSelectionDelegate != null) { localCertificate = _sslAuthenticationOptions.ServerCertSelectionDelegate(_sslAuthenticationOptions.TargetHost); if (localCertificate == null) { + if (NetEventSource.IsEnabled) + NetEventSource.Error(this, $"ServerCertSelectionDelegate returned no certificaete for '{_sslAuthenticationOptions.TargetHost}'."); throw new AuthenticationException(SR.net_ssl_io_no_server_cert); } + if (NetEventSource.IsEnabled) - NetEventSource.Info(this, "Use delegate selected Cert"); + NetEventSource.Info(this, "ServerCertSelectionDelegate selected Cert"); } else if (_sslAuthenticationOptions.CertSelectionDelegate != null) { X509CertificateCollection tempCollection = new X509CertificateCollection(); - tempCollection.Add(_sslAuthenticationOptions.ServerCertificate!); + tempCollection.Add(_sslAuthenticationOptions.CertificateContext!.Certificate!); // We pass string.Empty here to maintain strict compatability with .NET Framework. localCertificate = _sslAuthenticationOptions.CertSelectionDelegate(string.Empty, tempCollection, null, Array.Empty()); + if (localCertificate == null) + { + if (NetEventSource.IsEnabled) + NetEventSource.Error(this, $"CertSelectionDelegate returned no certificaete for '{_sslAuthenticationOptions.TargetHost}'."); + throw new NotSupportedException(SR.net_ssl_io_no_server_cert); + } + if (NetEventSource.IsEnabled) - NetEventSource.Info(this, "Use delegate selected Cert"); + NetEventSource.Info(this, "CertSelectionDelegate selected Cert"); } - else + else if (_sslAuthenticationOptions.CertificateContext != null) { - localCertificate = _sslAuthenticationOptions.ServerCertificate; + selectedCert = _sslAuthenticationOptions.CertificateContext.Certificate; } - if (localCertificate == null) + if (selectedCert == null) { - throw new NotSupportedException(SR.net_ssl_io_no_server_cert); - } + // We will get here if vertificate was slected via legacy callback using X509Certificate + // Fail immediately if no certificate was given. + if (localCertificate == null) + { + if (NetEventSource.IsEnabled) + NetEventSource.Error(this, "Certiticate callback returned no certificaete."); + throw new NotSupportedException(SR.net_ssl_io_no_server_cert); + } - // SECURITY: Accessing X509 cert Credential is disabled for semitrust. - // We no longer need to demand for unmanaged code permissions. - // EnsurePrivateKey should do the right demand for us. - X509Certificate2? selectedCert = EnsurePrivateKey(localCertificate); + // SECURITY: Accessing X509 cert Credential is disabled for semitrust. + // We no longer need to demand for unmanaged code permissions. + // EnsurePrivateKey should do the right demand for us. + selectedCert = FindCertificateWithPrivateKey(this, _sslAuthenticationOptions.IsServer, localCertificate); - if (selectedCert == null) - { - throw new NotSupportedException(SR.net_ssl_io_no_server_cert); - } + if (selectedCert == null) + { + throw new NotSupportedException(SR.net_ssl_io_no_server_cert); + } - if (!localCertificate.Equals(selectedCert)) - { - NetEventSource.Fail(this, "'selectedCert' does not match 'localCertificate'."); + if (!localCertificate.Equals(selectedCert)) + { + NetEventSource.Fail(this, "'selectedCert' does not match 'localCertificate'."); + } + + _sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(selectedCert); } // // Note selectedCert is a safe ref possibly cloned from the user passed Cert object // byte[] guessedThumbPrint = selectedCert.GetCertHash(); - try - { - SafeFreeCredentials? cachedCredentialHandle = SslSessionsCache.TryCachedCredential(guessedThumbPrint, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.IsServer, _sslAuthenticationOptions.EncryptionPolicy); + SafeFreeCredentials? cachedCredentialHandle = SslSessionsCache.TryCachedCredential(guessedThumbPrint, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.IsServer, _sslAuthenticationOptions.EncryptionPolicy); - if (cachedCredentialHandle != null) - { - _credentialsHandle = cachedCredentialHandle; - _sslAuthenticationOptions.ServerCertificate = localCertificate; - cachedCred = true; - } - else - { - _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(selectedCert, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); - thumbPrint = guessedThumbPrint; - _sslAuthenticationOptions.ServerCertificate = localCertificate; - } + if (cachedCredentialHandle != null) + { + _credentialsHandle = cachedCredentialHandle; + cachedCred = true; } - finally + else { - // An extra cert could have been created, dispose it now. - if ((object)localCertificate != (object)selectedCert) - { - selectedCert.Dispose(); - } + _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(selectedCert, _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); + thumbPrint = guessedThumbPrint; } if (NetEventSource.IsEnabled) diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs index 4895d91c2e3967..90d5acfb9fd6f6 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs @@ -47,9 +47,35 @@ internal SslAuthenticationOptions(SslServerAuthenticationOptions sslServerAuthen TargetHost = string.Empty; // Server specific options. - CertificateRevocationCheckMode = sslServerAuthenticationOptions.CertificateRevocationCheckMode; - ServerCertificate = sslServerAuthenticationOptions.ServerCertificate; CipherSuitesPolicy = sslServerAuthenticationOptions.CipherSuitesPolicy; + CertificateRevocationCheckMode = sslServerAuthenticationOptions.CertificateRevocationCheckMode; + + if (sslServerAuthenticationOptions.ServerCertificateContext != null) + { + CertificateContext = sslServerAuthenticationOptions.ServerCertificateContext; + } + else if (sslServerAuthenticationOptions.ServerCertificate != null) + { + X509Certificate2? certificateWithKey = sslServerAuthenticationOptions.ServerCertificate as X509Certificate2; + + if (certificateWithKey != null && certificateWithKey.HasPrivateKey) + { + // given cert is X509Certificate2 with key. We can use it directly. + CertificateContext = SslStreamCertificateContext.Create(certificateWithKey, null); + } + else + { + // This is legacy fix-up. If the Certificate did not have key, we will search stores and we + // will try to find one with matching hash. + certificateWithKey = SecureChannel.FindCertificateWithPrivateKey(this, true, sslServerAuthenticationOptions.ServerCertificate); + if (certificateWithKey == null) + { + throw new AuthenticationException(SR.net_ssl_io_no_server_cert); + } + + CertificateContext = SslStreamCertificateContext.Create(certificateWithKey); + } + } } private static SslProtocols FilterOutIncompatibleSslProtocols(SslProtocols protocols) @@ -72,7 +98,7 @@ private static SslProtocols FilterOutIncompatibleSslProtocols(SslProtocols proto internal X509CertificateCollection? ClientCertificates { get; set; } internal List? ApplicationProtocols { get; } internal bool IsServer { get; set; } - internal X509Certificate? ServerCertificate { get; set; } + internal SslStreamCertificateContext? CertificateContext { get; set; } internal SslProtocols EnabledSslProtocols { get; set; } internal X509RevocationMode CertificateRevocationCheckMode { get; set; } internal EncryptionPolicy EncryptionPolicy { get; set; } 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 ca345ad4fe0355..756f9f45ba05b7 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 @@ -31,6 +31,8 @@ public bool AllowRenegotiation public X509Certificate? ServerCertificate { get; set; } + public SslStreamCertificateContext? ServerCertificateContext { get; set; } + public SslProtocols EnabledSslProtocols { get => _enabledSslProtocols; diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs index 61011fde371c8e..ca04e414735ee0 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.cs @@ -177,12 +177,13 @@ private X509Certificate UserCertSelectionCallbackWrapper(string targetHost, X509 private SslAuthenticationOptions CreateAuthenticationOptions(SslServerAuthenticationOptions sslServerAuthenticationOptions) { - if (sslServerAuthenticationOptions.ServerCertificate == null && sslServerAuthenticationOptions.ServerCertificateSelectionCallback == null && _certSelectionDelegate == null) + if (sslServerAuthenticationOptions.ServerCertificate == null && sslServerAuthenticationOptions.ServerCertificateContext == null && + sslServerAuthenticationOptions.ServerCertificateSelectionCallback == null && _certSelectionDelegate == null) { throw new ArgumentNullException(nameof(sslServerAuthenticationOptions.ServerCertificate)); } - if ((sslServerAuthenticationOptions.ServerCertificate != null || _certSelectionDelegate != null) && sslServerAuthenticationOptions.ServerCertificateSelectionCallback != null) + if ((sslServerAuthenticationOptions.ServerCertificate != null || sslServerAuthenticationOptions.ServerCertificateContext != null || _certSelectionDelegate != null) && sslServerAuthenticationOptions.ServerCertificateSelectionCallback != null) { throw new InvalidOperationException(SR.Format(SR.net_conflicting_options, nameof(ServerCertificateSelectionCallback))); } 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 new file mode 100644 index 00000000000000..f7fbe01ca03a6b --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Linux.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Security +{ + public partial class SslStreamCertificateContext + { + 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 new file mode 100644 index 00000000000000..9ab01c90397dc0 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.OSX.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Security +{ + public partial class SslStreamCertificateContext + { + 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()); + } + } +} 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 new file mode 100644 index 00000000000000..6438f38da38e51 --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.Windows.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Security +{ + 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()); + } + } +} 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 new file mode 100644 index 00000000000000..f110c55afb694b --- /dev/null +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamCertificateContext.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Security.Cryptography.X509Certificates; + +namespace System.Net.Security +{ + public partial class SslStreamCertificateContext + { + internal readonly X509Certificate2 Certificate; + internal readonly X509Certificate2[] IntermediateCertificates; + + public static SslStreamCertificateContext Create(X509Certificate2 target, X509Certificate2Collection? additionalCertificates, bool offline = false) + { + if (!target.HasPrivateKey) + { + throw new NotSupportedException(SR.net_ssl_io_no_server_cert); + } + + X509Certificate2[] intermediates = Array.Empty(); + + using (X509Chain chain = new X509Chain()) + { + if (additionalCertificates != null) + { + foreach (X509Certificate cert in additionalCertificates) + { + chain.ChainPolicy.ExtraStore.Add(cert); + } + } + + chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllFlags; + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.DisableCertificateDownloads = offline; + chain.Build(target); + + // No leaf, no root. + int count = chain.ChainElements.Count - 2; + + foreach (X509ChainStatus status in chain.ChainStatus) + { + if (status.Status.HasFlag(X509ChainStatusFlags.PartialChain)) + { + // The last cert isn't a root cert + count++; + break; + } + } + + // Count can be zero for a self-signed certificate, or a cert issued directly from a root. + if (count > 0) + { + intermediates = new X509Certificate2[count]; + for (int i = 0; i < count; i++) + { + intermediates[i] = chain.ChainElements[i + 1].Certificate; + } + } + + // Dispose the copy of the target cert. + chain.ChainElements[0].Certificate.Dispose(); + + // Dispose the last cert, if we didn't include it. + for (int i = count + 1; i < chain.ChainElements.Count; i++) + { + chain.ChainElements[i].Certificate.Dispose(); + } + } + + return new SslStreamCertificateContext(target, intermediates); + } + + private SslStreamCertificateContext(X509Certificate2 target, X509Certificate2[] intermediates) + { + Certificate = target; + IntermediateCertificates = intermediates; + } + } +} diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs index af9e02ad03ed60..c9a0350d8d9a72 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslAuthenticationOptionsTest.cs @@ -43,6 +43,7 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() SslProtocols serverSslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12; EncryptionPolicy serverEncryption = EncryptionPolicy.AllowNoEncryption; RemoteCertificateValidationCallback serverRemoteCallback = new RemoteCertificateValidationCallback(delegate { return true; }); + SslStreamCertificateContext certificateContext = SslStreamCertificateContext.Create(serverCert, null, false); var network = new VirtualNetwork(); using (var client = new SslStream(new VirtualNetworkStream(network, isServer: false))) @@ -72,7 +73,8 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() EnabledSslProtocols = serverSslProtocols, EncryptionPolicy = serverEncryption, RemoteCertificateValidationCallback = serverRemoteCallback, - ServerCertificate = serverCert + ServerCertificate = serverCert, + ServerCertificateContext = certificateContext, }; // Authenticate @@ -103,6 +105,7 @@ public async Task ClientOptions_ServerOptions_NotMutatedDuringAuthentication() Assert.Equal(serverEncryption, serverOptions.EncryptionPolicy); Assert.Same(serverRemoteCallback, serverOptions.RemoteCertificateValidationCallback); Assert.Same(serverCert, serverOptions.ServerCertificate); + Assert.Same(certificateContext, serverOptions.ServerCertificateContext); } } } diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlpnTests.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlpnTests.cs index c5284653585b78..bdbe2c40cc507d 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlpnTests.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamAlpnTests.cs @@ -42,7 +42,7 @@ private async Task DoHandshakeWithOptions(SslStream clientSslStream, SslStream s { clientOptions.RemoteCertificateValidationCallback = AllowAnyServerCertificate; clientOptions.TargetHost = certificate.GetNameInfo(X509NameType.SimpleName, false); - serverOptions.ServerCertificate = certificate; + serverOptions.ServerCertificateContext = SslStreamCertificateContext.Create(certificate, null); Task t1 = clientSslStream.AuthenticateAsClientAsync(TestAuthenticateAsync, clientOptions); Task t2 = serverSslStream.AuthenticateAsServerAsync(TestAuthenticateAsync, serverOptions); diff --git a/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeSslStream.Implementation.cs b/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeSslStream.Implementation.cs index ccd294beb54ddd..65314424280d13 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeSslStream.Implementation.cs +++ b/src/libraries/System.Net.Security/tests/UnitTests/Fakes/FakeSslStream.Implementation.cs @@ -87,6 +87,11 @@ internal class SecureChannel internal X509Certificate LocalClientCertificate => default; internal X509RevocationMode CheckCertRevocationStatus => default; internal ProtocolToken CreateShutdownToken() => default; + + internal static X509Certificate2? FindCertificateWithPrivateKey(object instance, bool isServer, X509Certificate certificate) + { + return certificate as X509Certificate2; + } } internal class ProtocolToken diff --git a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj index fd76bb91ceb23a..1afe1fcf5829c5 100644 --- a/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/UnitTests/System.Net.Security.Unit.Tests.csproj @@ -46,6 +46,10 @@ Link="ProductionCode\System\Net\Security\SslApplicationProtocol.cs" /> + +