CipherSuitePolicy implementation (dotnet/corefx#36775)
authorKrzysztof Wicher <mordotymoja@gmail.com>
Sat, 13 Apr 2019 01:23:31 +0000 (18:23 -0700)
committerGitHub <noreply@github.com>
Sat, 13 Apr 2019 01:23:31 +0000 (18:23 -0700)
* CipherSuitePolicy implementation (Linux)

* SSL_CIPHER_find

* do not call TLS1.3 APIs on platforms which don't support it

* Non-TLS1.3 specific tests are skipped when not enough cipher suites is enabled

* clean ups

* attempt to fix OSX

* another attempt to fix OSX

* missing define

* address some feedback, try to fix test failures

* portable build fix

* do not call old set ciphers API when only TLS 1.3 is requested

* apply feedback

* add OSX implementation

* fixes to OSX

* explicit convert

* use explicit SSLCipherSuite instead of uint16_t

* random change to trigger CI

* s/unsafe/fixed

* fixes

* random change to trigger CI

* client ordering does not have to win

* tests: AllowedCipherSuites, new CipherSuitesPolicy(null)

* run AllowedCipherSuites tests only when CSP is supported

* add summary on CipherSuitesPolicy

* address feedback

* move OS specific files to CipherSuitesPolicyPal

* FALLBACK->LIGHTUP and remove local_

* do not call 1.1.1 function on non-portable build when lower openssl version is installed

* get rid of warning that arg is unused

* make CipherSuitesPolicyPal public members internal

Commit migrated from https://github.com/dotnet/corefx/commit/07f443b6c9f27dd050ffb5eb3afa126a2b1bdddd

22 files changed:
src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Ssl.cs
src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs
src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Ssl.cs
src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.SslCtxOptions.cs
src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/pal_ssl.c
src/libraries/Native/Unix/System.Security.Cryptography.Native.Apple/pal_ssl.h
src/libraries/Native/Unix/System.Security.Cryptography.Native/opensslshim.h
src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_ssl.c
src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_ssl.h
src/libraries/System.Net.Security/ref/System.Net.Security.cs
src/libraries/System.Net.Security/src/Resources/Strings.resx
src/libraries/System.Net.Security/src/System.Net.Security.csproj
src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicy.cs [new file with mode: 0644]
src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.Linux.cs [new file with mode: 0644]
src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.OSX.cs [new file with mode: 0644]
src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.Windows.cs [new file with mode: 0644]
src/libraries/System.Net.Security/src/System/Net/Security/Pal.OSX/SafeDeleteSslContext.cs
src/libraries/System.Net.Security/src/System/Net/Security/SslAuthenticationOptions.cs
src/libraries/System.Net.Security/src/System/Net/Security/SslClientAuthenticationOptions.cs
src/libraries/System.Net.Security/src/System/Net/Security/SslServerAuthenticationOptions.cs
src/libraries/System.Net.Security/src/System/Net/Security/TlsCipherSuiteNameParser.ttinclude
src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNegotiatedCipherSuiteTest.cs

index ed79d49..d8e591c 100644 (file)
@@ -146,6 +146,9 @@ internal static partial class Interop
         [DllImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_SslGetProtocolVersion")]
         internal static extern int SslGetProtocolVersion(SafeSslHandle sslHandle, out SslProtocols protocol);
 
+        [DllImport(Interop.Libraries.AppleCryptoNative, EntryPoint = "AppleCryptoNative_SslSetEnabledCipherSuites")]
+        unsafe internal static extern int SslSetEnabledCipherSuites(SafeSslHandle sslHandle, uint* cipherSuites, int numCipherSuites);
+
         internal static void SslSetAcceptClientCert(SafeSslHandle sslHandle)
         {
             int osStatus = AppleCryptoNative_SslSetAcceptClientCert(sslHandle);
index c3c5105..98ccebd 100644 (file)
@@ -64,31 +64,55 @@ internal static partial class Interop
                     throw CreateSslException(SR.net_allocate_ssl_context_failed);
                 }
 
-                // TLS 1.3 uses different ciphersuite restrictions than previous versions.
-                // It has no equivalent to a NoEncryption option.
-                if (policy == EncryptionPolicy.NoEncryption)
+                if (!Interop.Ssl.Tls13Supported)
+                {
+                    if (protocols != SslProtocols.None &&
+                        CipherSuitesPolicyPal.WantsTls13(protocols))
+                    {
+                        protocols = protocols & (~SslProtocols.Tls13);
+                    }
+                }
+                else if (CipherSuitesPolicyPal.WantsTls13(protocols) &&
+                    CipherSuitesPolicyPal.ShouldOptOutOfTls13(sslAuthenticationOptions.CipherSuitesPolicy, policy))
                 {
                     if (protocols == SslProtocols.None)
                     {
+                        // we are using default settings but cipher suites policy says that TLS 1.3
+                        // is not compatible with our settings (i.e. we requested no encryption or disabled
+                        // all TLS 1.3 cipher suites)
                         protocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
                     }
                     else
                     {
-                        protocols &= ~SslProtocols.Tls13;
-
-                        if (protocols == SslProtocols.None)
-                        {
-                            throw new SslException(
-                                SR.Format(SR.net_ssl_encryptionpolicy_notsupported, policy));
-                        }
+                        // user explicitly asks for TLS 1.3 but their policy is not compatible with TLS 1.3
+                        throw new SslException(
+                            SR.Format(SR.net_ssl_encryptionpolicy_notsupported, policy));
                     }
                 }
 
+                if (CipherSuitesPolicyPal.ShouldOptOutOfLowerThanTls13(sslAuthenticationOptions.CipherSuitesPolicy, policy))
+                {
+                    if (!CipherSuitesPolicyPal.WantsTls13(protocols))
+                    {
+                        // We cannot provide neither TLS 1.3 or non TLS 1.3, user disabled all cipher suites
+                        throw new SslException(
+                            SR.Format(SR.net_ssl_encryptionpolicy_notsupported, policy));
+                    }
+
+                    protocols = SslProtocols.Tls13;
+                }
 
                 // Configure allowed protocols. It's ok to use DangerousGetHandle here without AddRef/Release as we just
                 // create the handle, it's rooted by the using, no one else has a reference to it, etc.
                 Ssl.SetProtocolOptions(innerContext.DangerousGetHandle(), protocols);
 
+                // Sets policy and security level
+                if (!Ssl.SetEncryptionPolicy(innerContext, policy))
+                {
+                    throw new SslException(
+                        SR.Format(SR.net_ssl_encryptionpolicy_notsupported, policy));
+                }
+
                 // The logic in SafeSslHandle.Disconnect is simple because we are doing a quiet
                 // shutdown (we aren't negotiating for session close to enable later session
                 // restoration).
@@ -98,10 +122,23 @@ internal static partial class Interop
                 // https://www.openssl.org/docs/manmaster/ssl/SSL_shutdown.html
                 Ssl.SslCtxSetQuietShutdown(innerContext);
 
-                if (!Ssl.SetEncryptionPolicy(innerContext, policy))
+                byte[] cipherList =
+                    CipherSuitesPolicyPal.GetOpenSslCipherList(sslAuthenticationOptions.CipherSuitesPolicy, protocols, policy);
+
+                byte[] cipherSuites =
+                    CipherSuitesPolicyPal.GetOpenSslCipherSuites(sslAuthenticationOptions.CipherSuitesPolicy, protocols, policy);
+
+                unsafe
                 {
-                    Crypto.ErrClearError();
-                    throw new PlatformNotSupportedException(SR.Format(SR.net_ssl_encryptionpolicy_notsupported, policy));
+                    fixed (byte* cipherListStr = cipherList)
+                    fixed (byte* cipherSuitesStr = cipherSuites)
+                    {
+                        if (!Ssl.SetCiphers(innerContext, cipherListStr, cipherSuitesStr))
+                        {
+                            Crypto.ErrClearError();
+                            throw new PlatformNotSupportedException(SR.Format(SR.net_ssl_encryptionpolicy_notsupported, policy));
+                        }
+                    }
                 }
 
                 bool hasCertificateAndKey =
index 4c686b2..323a1b9 100644 (file)
@@ -4,6 +4,7 @@
 
 using System;
 using System.Diagnostics;
+using System.Net.Security;
 using System.Runtime.InteropServices;
 using System.Security.Cryptography.X509Certificates;
 using Microsoft.Win32.SafeHandles;
@@ -135,6 +136,21 @@ internal static partial class Interop
         [return: MarshalAs(UnmanagedType.Bool)]
         internal static extern bool SslGetCurrentCipherId(SafeSslHandle ssl, out int cipherId);
 
+        [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_GetOpenSslCipherSuiteName")]
+        private static extern IntPtr GetOpenSslCipherSuiteName(SafeSslHandle ssl, int cipherSuite, out int isTls12OrLower);
+
+        internal static string GetOpenSslCipherSuiteName(SafeSslHandle ssl, TlsCipherSuite cipherSuite, out bool isTls12OrLower)
+        {
+            string ret = Marshal.PtrToStringAnsi(GetOpenSslCipherSuiteName(ssl, (int)cipherSuite, out int isTls12OrLowerInt));
+            isTls12OrLower = isTls12OrLowerInt != 0;
+            return ret;
+        }
+
+        [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_Tls13Supported")]
+        [return: MarshalAs(UnmanagedType.Bool)]
+        private static extern bool Tls13SupportedImpl();
+        internal static readonly bool Tls13Supported = Tls13SupportedImpl();
+
         internal static SafeSharedX509NameStackHandle SslGetClientCAList(SafeSslHandle ssl)
         {
             Crypto.CheckValidOpenSslHandle(ssl);
index 4c4721f..f841734 100644 (file)
@@ -30,6 +30,9 @@ internal static partial class Interop
         [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SslCtxSetVerify")]
         internal static extern void SslCtxSetVerify(SafeSslContextHandle ctx, SslCtxSetVerifyCallback callback);
 
+        [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SetCiphers")]
+        internal static unsafe extern bool SetCiphers(SafeSslContextHandle ctx, byte* cipherList, byte* cipherSuites);
+
         [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_SetEncryptionPolicy")]
         internal static extern bool SetEncryptionPolicy(SafeSslContextHandle ctx, EncryptionPolicy policy);
     }
index 6fe52e2..afa97e5 100644 (file)
@@ -443,12 +443,44 @@ int32_t AppleCryptoNative_SslGetProtocolVersion(SSLContextRef sslContext, PAL_Ss
     return osStatus;
 }
 
-int32_t AppleCryptoNative_SslGetCipherSuite(SSLContextRef sslContext, uint32_t* pCipherSuiteOut)
+int32_t AppleCryptoNative_SslGetCipherSuite(SSLContextRef sslContext, uint16_t* pCipherSuiteOut)
 {
     if (pCipherSuiteOut == NULL)
-        *pCipherSuiteOut = 0;
+    {
+        return errSecParam;
+    }
+
+    SSLCipherSuite cipherSuite;
+    OSStatus status = SSLGetNegotiatedCipher(sslContext, &cipherSuite);
+    *pCipherSuiteOut = (uint16_t)cipherSuite;
+
+    return status;
+}
+
+int32_t AppleCryptoNative_SslSetEnabledCipherSuites(SSLContextRef sslContext, const uint32_t* cipherSuites, int32_t numCipherSuites)
+{
+    // Max numCipherSuites is 2^16 (all possible cipher suites)
+    assert(numCipherSuites < (1 << 16));
 
-    return SSLGetNegotiatedCipher(sslContext, pCipherSuiteOut);
+    if (sizeof(SSLCipherSuite) == sizeof(uint32_t))
+    {
+        // macOS
+        return SSLSetEnabledCiphers(sslContext, cipherSuites, (size_t)numCipherSuites);
+    }
+    else
+    {
+        // iOS, tvOS, watchOS
+        SSLCipherSuite* cipherSuites16 = (SSLCipherSuite*)malloc(sizeof(SSLCipherSuite) * (size_t)numCipherSuites);
+        for (int i = 0; i < numCipherSuites; i++)
+        {
+            cipherSuites16[i] = (SSLCipherSuite)cipherSuites[i];
+        }
+
+        OSStatus status = SSLSetEnabledCiphers(sslContext, cipherSuites16, (size_t)numCipherSuites);
+
+        free(cipherSuites16);
+        return status;
+    }
 }
 
 __attribute__((constructor)) static void InitializeAppleCryptoSslShim()
index 2337b10..e6c922c 100644 (file)
@@ -230,7 +230,14 @@ Retrieve the TLS Cipher Suite which was negotiated for the current session.
 Returns the output of SSLGetNegotiatedCipher.
 
 Output:
-pProtocol: The TLS CipherSuite value (from the RFC), e.g. ((uint32_t)0xC030) for
+pProtocol: The TLS CipherSuite value (from the RFC), e.g. ((uint16_t)0xC030) for
 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
 */
-DLLEXPORT int32_t AppleCryptoNative_SslGetCipherSuite(SSLContextRef sslContext, uint32_t* pCipherSuiteOut);
+DLLEXPORT int32_t AppleCryptoNative_SslGetCipherSuite(SSLContextRef sslContext, uint16_t* pCipherSuiteOut);
+
+/*
+Sets enabled cipher suites for the current session.
+
+Returns the output of SSLSetEnabledCiphers.
+*/
+DLLEXPORT int32_t AppleCryptoNative_SslSetEnabledCipherSuites(SSLContextRef sslContext, const uint32_t* cipherSuites, int32_t numCipherSuites);
index 59c0d23..585ff67 100644 (file)
 #include <openssl/x509v3.h>
 
 #include "pal_crypto_config.h"
+#define OPENSSL_VERSION_1_1_1_RTM 0x10101000L
 #define OPENSSL_VERSION_1_1_0_RTM 0x10100000L
 #define OPENSSL_VERSION_1_0_2_RTM 0x10002000L
 
+#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_1_1_1_RTM
+#define HAVE_OPENSSL_SET_CIPHERSUITES 1
+#else
+#define HAVE_OPENSSL_SET_CIPHERSUITES 0
+#endif
+
 #if OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_1_1_0_RTM
 
 // Remove problematic #defines
@@ -77,6 +84,14 @@ int EC_POINT_get_affine_coordinates_GF2m(const EC_GROUP* group, const EC_POINT*
 int EC_POINT_set_affine_coordinates_GF2m(
     const EC_GROUP* group, EC_POINT* p, const BIGNUM* x, const BIGNUM* y, BN_CTX* ctx);
 #endif
+
+#if !HAVE_OPENSSL_SET_CIPHERSUITES
+#undef HAVE_OPENSSL_SET_CIPHERSUITES
+#define HAVE_OPENSSL_SET_CIPHERSUITES 1
+int SSL_CTX_set_ciphersuites(SSL_CTX *ctx, const char *str);
+const SSL_CIPHER* SSL_CIPHER_find(SSL *ssl, const unsigned char *ptr);
+#endif
+
 #if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_1_1_0_RTM
 typedef struct stack_st _STACK;
 int CRYPTO_add_lock(int* pointer, int amount, int type, const char* file, int line);
@@ -422,9 +437,11 @@ void SSL_get0_alpn_selected(const SSL* ssl, const unsigned char** protocol, unsi
     REQUIRED_FUNCTION(RSA_size) \
     REQUIRED_FUNCTION(RSA_up_ref) \
     REQUIRED_FUNCTION(RSA_verify) \
-    REQUIRED_FUNCTION(SSL_CIPHER_description) \
+    LIGHTUP_FUNCTION(SSL_CIPHER_find) \
     REQUIRED_FUNCTION(SSL_CIPHER_get_bits) \
     REQUIRED_FUNCTION(SSL_CIPHER_get_id) \
+    LIGHTUP_FUNCTION(SSL_CIPHER_get_name) \
+    LIGHTUP_FUNCTION(SSL_CIPHER_get_version) \
     REQUIRED_FUNCTION(SSL_ctrl) \
     REQUIRED_FUNCTION(SSL_set_quiet_shutdown) \
     REQUIRED_FUNCTION(SSL_CTX_check_private_key) \
@@ -436,6 +453,7 @@ void SSL_get0_alpn_selected(const SSL* ssl, const unsigned char** protocol, unsi
     LIGHTUP_FUNCTION(SSL_CTX_set_alpn_select_cb) \
     REQUIRED_FUNCTION(SSL_CTX_set_cert_verify_callback) \
     REQUIRED_FUNCTION(SSL_CTX_set_cipher_list) \
+    LIGHTUP_FUNCTION(SSL_CTX_set_ciphersuites) \
     REQUIRED_FUNCTION(SSL_CTX_set_client_cert_cb) \
     REQUIRED_FUNCTION(SSL_CTX_set_quiet_shutdown) \
     FALLBACK_FUNCTION(SSL_CTX_set_options) \
@@ -810,8 +828,10 @@ FOR_ALL_OPENSSL_FUNCTIONS
 #define sk_push OPENSSL_sk_push_ptr
 #define sk_value OPENSSL_sk_value_ptr
 #define SSL_CIPHER_get_bits SSL_CIPHER_get_bits_ptr
-#define SSL_CIPHER_description SSL_CIPHER_description_ptr
+#define SSL_CIPHER_find SSL_CIPHER_find_ptr
 #define SSL_CIPHER_get_id SSL_CIPHER_get_id_ptr
+#define SSL_CIPHER_get_name SSL_CIPHER_get_name_ptr
+#define SSL_CIPHER_get_version SSL_CIPHER_get_version_ptr
 #define SSL_ctrl SSL_ctrl_ptr
 #define SSL_set_quiet_shutdown SSL_set_quiet_shutdown_ptr
 #define SSL_CTX_check_private_key SSL_CTX_check_private_key_ptr
@@ -822,6 +842,7 @@ FOR_ALL_OPENSSL_FUNCTIONS
 #define SSL_CTX_set_alpn_select_cb SSL_CTX_set_alpn_select_cb_ptr
 #define SSL_CTX_set_cert_verify_callback SSL_CTX_set_cert_verify_callback_ptr
 #define SSL_CTX_set_cipher_list SSL_CTX_set_cipher_list_ptr
+#define SSL_CTX_set_ciphersuites SSL_CTX_set_ciphersuites_ptr
 #define SSL_CTX_set_client_cert_cb SSL_CTX_set_client_cert_cb_ptr
 #define SSL_CTX_set_options SSL_CTX_set_options_ptr
 #define SSL_CTX_set_quiet_shutdown SSL_CTX_set_quiet_shutdown_ptr
index fec8453..fd7815c 100644 (file)
@@ -229,247 +229,6 @@ int32_t CryptoNative_SslSessionReused(SSL* ssl)
     return SSL_session_reused(ssl) == 1;
 }
 
-static bool StringSpanEquals(const char* lhs, const char* rhs, size_t lhsLength)
-{
-    if (lhsLength != strlen(rhs))
-    {
-        return false;
-    }
-
-    return strncmp(lhs, rhs, lhsLength) == 0;
-}
-
-static CipherAlgorithmType MapCipherAlgorithmType(const char* encryption, size_t encryptionLength)
-{
-    if (StringSpanEquals(encryption, "DES(56)", encryptionLength))
-        return Des;
-    if (StringSpanEquals(encryption, "3DES(168)", encryptionLength))
-        return TripleDes;
-    if (StringSpanEquals(encryption, "RC4(128)", encryptionLength))
-        return Rc4;
-    if (StringSpanEquals(encryption, "RC2(128)", encryptionLength))
-        return Rc2;
-    if (StringSpanEquals(encryption, "None", encryptionLength))
-        return Null;
-    if (StringSpanEquals(encryption, "IDEA(128)", encryptionLength))
-        return SSL_IDEA;
-    if (StringSpanEquals(encryption, "SEED(128)", encryptionLength))
-        return SSL_SEED;
-    if (StringSpanEquals(encryption, "AES(128)", encryptionLength))
-        return Aes128;
-    if (StringSpanEquals(encryption, "AES(256)", encryptionLength))
-        return Aes256;
-    if (StringSpanEquals(encryption, "Camellia(128)", encryptionLength))
-        return SSL_CAMELLIA128;
-    if (StringSpanEquals(encryption, "Camellia(256)", encryptionLength))
-        return SSL_CAMELLIA256;
-    if (StringSpanEquals(encryption, "GOST89(256)", encryptionLength))
-        return SSL_eGOST2814789CNT;
-    if (StringSpanEquals(encryption, "AESGCM(128)", encryptionLength))
-        return Aes128;
-    if (StringSpanEquals(encryption, "AESGCM(256)", encryptionLength))
-        return Aes256;
-
-    return CipherAlgorithmType_None;
-}
-
-static ExchangeAlgorithmType MapExchangeAlgorithmType(const char* keyExchange, size_t keyExchangeLength)
-{
-    if (StringSpanEquals(keyExchange, "RSA", keyExchangeLength))
-        return RsaKeyX;
-    if (StringSpanEquals(keyExchange, "DH/RSA", keyExchangeLength))
-        return DiffieHellman;
-    if (StringSpanEquals(keyExchange, "DH/DSS", keyExchangeLength))
-        return DiffieHellman;
-    if (StringSpanEquals(keyExchange, "DH", keyExchangeLength))
-        return DiffieHellman;
-    if (StringSpanEquals(keyExchange, "KRB5", keyExchangeLength))
-        return SSL_kKRB5;
-    if (StringSpanEquals(keyExchange, "ECDH", keyExchangeLength))
-        return SSL_ECDHE;
-    if (StringSpanEquals(keyExchange, "ECDH/RSA", keyExchangeLength))
-        return SSL_ECDH;
-    if (StringSpanEquals(keyExchange, "ECDH/ECDSA", keyExchangeLength))
-        return SSL_ECDSA;
-    if (StringSpanEquals(keyExchange, "PSK", keyExchangeLength))
-        return SSL_kPSK;
-    if (StringSpanEquals(keyExchange, "GOST", keyExchangeLength))
-        return SSL_kGOST;
-    if (StringSpanEquals(keyExchange, "SRP", keyExchangeLength))
-        return SSL_kSRP;
-
-    return ExchangeAlgorithmType_None;
-}
-
-static void GetHashAlgorithmTypeAndSize(const char* mac,
-                                        size_t macLength,
-                                        HashAlgorithmType* dataHashAlg,
-                                        DataHashSize* hashKeySize)
-{
-    if (StringSpanEquals(mac, "MD5", macLength))
-    {
-        *dataHashAlg = Md5;
-        *hashKeySize = MD5_HashKeySize;
-        return;
-    }
-    if (StringSpanEquals(mac, "SHA1", macLength))
-    {
-        *dataHashAlg = Sha1;
-        *hashKeySize = SHA1_HashKeySize;
-        return;
-    }
-    if (StringSpanEquals(mac, "GOST94", macLength))
-    {
-        *dataHashAlg = SSL_GOST94;
-        *hashKeySize = GOST_HashKeySize;
-        return;
-    }
-    if (StringSpanEquals(mac, "GOST89", macLength))
-    {
-        *dataHashAlg = SSL_GOST89;
-        *hashKeySize = GOST_HashKeySize;
-        return;
-    }
-    if (StringSpanEquals(mac, "SHA256", macLength))
-    {
-        *dataHashAlg = SSL_SHA256;
-        *hashKeySize = SHA256_HashKeySize;
-        return;
-    }
-    if (StringSpanEquals(mac, "SHA384", macLength))
-    {
-        *dataHashAlg = SSL_SHA384;
-        *hashKeySize = SHA384_HashKeySize;
-        return;
-    }
-    if (StringSpanEquals(mac, "AEAD", macLength))
-    {
-        *dataHashAlg = SSL_AEAD;
-        *hashKeySize = Default;
-        return;
-    }
-
-    *dataHashAlg = HashAlgorithmType_None;
-    *hashKeySize = Default;
-}
-
-/*
-Given a keyName string like "Enc=XXX", parses the description string and returns the
-'XXX' into value and valueLength return variables.
-
-Returns a value indicating whether the pattern starting with keyName was found in description.
-*/
-static bool GetDescriptionValue(
-    const char* description, const char* keyName, size_t keyNameLength, const char** value, size_t* valueLength)
-{
-    // search for keyName in description
-    const char* keyNameStart = strstr(description, keyName);
-    if (keyNameStart != NULL)
-    {
-        // set valueStart to the beginning of the value
-        const char* valueStart = keyNameStart + keyNameLength;
-        size_t index = 0;
-
-        // the value ends when we hit a space or the end of the string
-        while (valueStart[index] != ' ' && valueStart[index] != '\0')
-        {
-            index++;
-        }
-
-        *value = valueStart;
-        *valueLength = index;
-        return true;
-    }
-
-    return false;
-}
-
-#define descriptionLength 256
-
-/*
-Parses the Kx, Enc, and Mac values out of the SSL_CIPHER_description and
-maps the values to the corresponding .NET enum value.
-*/
-static bool GetSslConnectionInfoFromDescription(const SSL_CIPHER* cipher,
-                                                CipherAlgorithmType* dataCipherAlg,
-                                                ExchangeAlgorithmType* keyExchangeAlg,
-                                                HashAlgorithmType* dataHashAlg,
-                                                DataHashSize* hashKeySize)
-{
-    char description[descriptionLength] = { 0 };
-    SSL_CIPHER_description(cipher, description, descriptionLength - 1); // ensure description is NULL-terminated
-
-    const char* keyExchange;
-    size_t keyExchangeLength;
-    if (!GetDescriptionValue(description, "Kx=", 3, &keyExchange, &keyExchangeLength))
-    {
-        return false;
-    }
-
-    const char* encryption;
-    size_t encryptionLength;
-    if (!GetDescriptionValue(description, "Enc=", 4, &encryption, &encryptionLength))
-    {
-        return false;
-    }
-
-    const char* mac;
-    size_t macLength;
-    if (!GetDescriptionValue(description, "Mac=", 4, &mac, &macLength))
-    {
-        return false;
-    }
-
-    *keyExchangeAlg = MapExchangeAlgorithmType(keyExchange, keyExchangeLength);
-    *dataCipherAlg = MapCipherAlgorithmType(encryption, encryptionLength);
-    GetHashAlgorithmTypeAndSize(mac, macLength, dataHashAlg, hashKeySize);
-    return true;
-}
-
-int32_t CryptoNative_GetSslConnectionInfo(SSL* ssl,
-                                                     CipherAlgorithmType* dataCipherAlg,
-                                                     ExchangeAlgorithmType* keyExchangeAlg,
-                                                     HashAlgorithmType* dataHashAlg,
-                                                     int32_t* dataKeySize,
-                                                     DataHashSize* hashKeySize)
-{
-    const SSL_CIPHER* cipher;
-
-    if (!ssl || !dataCipherAlg || !keyExchangeAlg || !dataHashAlg || !dataKeySize || !hashKeySize)
-    {
-        goto err;
-    }
-
-    cipher = SSL_get_current_cipher(ssl);
-    if (!cipher)
-    {
-        goto err;
-    }
-
-    SSL_CIPHER_get_bits(cipher, dataKeySize);
-
-    if (GetSslConnectionInfoFromDescription(cipher, dataCipherAlg, keyExchangeAlg, dataHashAlg, hashKeySize))
-    {
-        return 1;
-    }
-
-err:
-    assert(false);
-
-    if (dataCipherAlg)
-        *dataCipherAlg = CipherAlgorithmType_None;
-    if (keyExchangeAlg)
-        *keyExchangeAlg = ExchangeAlgorithmType_None;
-    if (dataHashAlg)
-        *dataHashAlg = HashAlgorithmType_None;
-    if (dataKeySize)
-        *dataKeySize = 0;
-    if (hashKeySize)
-        *hashKeySize = Default;
-
-    return 0;
-}
-
 int32_t CryptoNative_SslWrite(SSL* ssl, const void* buf, int32_t num)
 {
     return SSL_write(ssl, buf, num);
@@ -560,46 +319,110 @@ CryptoNative_SslCtxSetCertVerifyCallback(SSL_CTX* ctx, SslCtxSetCertVerifyCallba
     SSL_CTX_set_cert_verify_callback(ctx, callback, arg);
 }
 
-// delimiter ":" is used to allow more than one strings
-// below string is corresponding to "AllowNoEncryption"
-#define SSL_TXT_Separator ":"
-#define SSL_TXT_Exclusion "!"
-#define SSL_TXT_AllIncludingNull SSL_TXT_ALL SSL_TXT_Separator SSL_TXT_eNULL
-#define SSL_TXT_NotAnon SSL_TXT_Separator SSL_TXT_Exclusion SSL_TXT_aNULL
-
 int32_t CryptoNative_SetEncryptionPolicy(SSL_CTX* ctx, EncryptionPolicy policy)
 {
-    const char* cipherString = NULL;
-    bool clearSecLevel = false;
-
     switch (policy)
     {
+        case AllowNoEncryption:
+        case NoEncryption:
+            // No minimum security policy, same as OpenSSL 1.0
+            SSL_CTX_set_security_level(ctx, 0);
+            return true;
         case RequireEncryption:
-            cipherString = SSL_TXT_ALL SSL_TXT_NotAnon;
-            break;
+            return true;
+    }
 
-        case AllowNoEncryption:
-            cipherString = SSL_TXT_AllIncludingNull;
-            clearSecLevel = true;
-            break;
+    return false;
+}
 
-        case NoEncryption:
-            cipherString = SSL_TXT_eNULL;
-            clearSecLevel = true;
-            break;
+int32_t CryptoNative_SetCiphers(SSL_CTX* ctx, const char* cipherList, const char* cipherSuites)
+{
+    int32_t ret = true;
+
+    // for < TLS 1.3
+    if (cipherList != NULL)
+    {
+        ret &= SSL_CTX_set_cipher_list(ctx, cipherList);
+        if (!ret)
+        {
+            return ret;
+        }
+    }
+
+    // for TLS 1.3
+#if HAVE_OPENSSL_SET_CIPHERSUITES
+    if (CryptoNative_Tls13Supported() && cipherSuites != NULL)
+    {
+        ret &= SSL_CTX_set_ciphersuites(ctx, cipherSuites);
     }
+#else
+    (void)cipherSuites;
+#endif
+
+    return ret;
+}
+
+const char* CryptoNative_GetOpenSslCipherSuiteName(SSL* ssl, int32_t cipherSuite, int32_t* isTls12OrLower)
+{
+    unsigned char cs[2];
+    const SSL_CIPHER* cipher;
+    const char* ret;
+
+    *isTls12OrLower = 0;
+    cs[0] = (cipherSuite >> 8) & 0xFF;
+    cs[1] = cipherSuite & 0xFF;
+    cipher = SSL_CIPHER_find(ssl, cs);
+
+    if (cipher == NULL)
+        return NULL;
 
-    assert(cipherString != NULL);
+    ret = SSL_CIPHER_get_name(cipher);
 
-    if (clearSecLevel)
+    if (ret == NULL)
+        return NULL;
+
+    // we should get (NONE) only when cipher is NULL
+    assert(strcmp("(NONE)", ret) != 0);
+
+    const char* version = SSL_CIPHER_get_version(cipher);
+    assert(version != NULL);
+    assert(strcmp(version, "unknown") != 0);
+
+    // same rules apply for DTLS as for TLS so just shortcut
+    if (version[0] == 'D')
+    {
+        version++;
+    }
+
+    // check if tls1.2 or lower
+    // check most common case first
+    if (strncmp("TLSv1", version, 5) == 0)
+    {
+        const char* tlsver = version + 5;
+        // true for TLSv1, TLSv1.0, TLSv1.1, TLS1.2, anything else is assumed to be newer
+        *isTls12OrLower =
+            tlsver[0] == 0 ||
+            (tlsver[0] == '.' && tlsver[1] >= '0' && tlsver[1] <= '2' && tlsver[2] == 0);
+    }
+    else
     {
-        // No minimum security policy, same as OpenSSL 1.0
-        SSL_CTX_set_security_level(ctx, 0);
+        // if we don't know it assume it is new
+        // worst case scenario OpenSSL will ignore it
+        *isTls12OrLower =
+            strncmp("SSLv", version, 4) == 0;
     }
 
-    return SSL_CTX_set_cipher_list(ctx, cipherString);
+    return ret;
 }
 
+int32_t CryptoNative_Tls13Supported()
+{
+#if HAVE_OPENSSL_SET_CIPHERSUITES
+    return API_EXISTS(SSL_CTX_set_ciphersuites);
+#else
+    return false;
+#endif
+}
 
 void CryptoNative_SslCtxSetClientCertCallback(SSL_CTX* ctx, SslClientCertCallback callback)
 {
index 038e5de..2db6e76 100644 (file)
@@ -205,19 +205,6 @@ Returns the protocol version string for the SSL instance.
 DLLEXPORT const char* CryptoNative_SslGetVersion(SSL* ssl);
 
 /*
-Returns the connection information for the SSL instance.
-
-Returns 1 upon success, otherwise 0.
-*/
-
-DLLEXPORT int32_t CryptoNative_GetSslConnectionInfo(SSL* ssl,
-                                                     CipherAlgorithmType* dataCipherAlg,
-                                                     ExchangeAlgorithmType* keyExchangeAlg,
-                                                     HashAlgorithmType* dataHashAlg,
-                                                     int32_t* dataKeySize,
-                                                     DataHashSize* hashKeySize);
-
-/*
 Shims the SSL_write method.
 
 Returns the positive number of bytes written when successful, 0 or a negative number
@@ -338,11 +325,20 @@ CryptoNative_SslCtxSetCertVerifyCallback(SSL_CTX* ctx, SslCtxSetCertVerifyCallba
 
 /*
 Sets the specified encryption policy on the SSL_CTX.
-Returns 1 if any cipher could be selected, and 0 if none were available.
 */
 DLLEXPORT int32_t CryptoNative_SetEncryptionPolicy(SSL_CTX* ctx, EncryptionPolicy policy);
 
 /*
+Sets ciphers (< TLS 1.3) and cipher suites (TLS 1.3) on the SSL_CTX
+*/
+DLLEXPORT int32_t CryptoNative_SetCiphers(SSL_CTX* ctx, const char* cipherList, const char* cipherSuites);
+
+/*
+Determines if TLS 1.3 is supported by this OpenSSL implementation
+*/
+DLLEXPORT int32_t CryptoNative_Tls13Supported(void);
+
+/*
 Shims the SSL_CTX_set_client_cert_cb method
 */
 DLLEXPORT void CryptoNative_SslCtxSetClientCertCallback(SSL_CTX* ctx, SslClientCertCallback callback);
@@ -396,3 +392,9 @@ DLLEXPORT int32_t CryptoNative_SslSetTlsExtHostName(SSL* ssl, uint8_t* name);
 Shims the SSL_get_current_cipher and SSL_CIPHER_get_id.
 */
 DLLEXPORT int32_t CryptoNative_SslGetCurrentCipherId(SSL* ssl, int32_t* cipherId);
+
+/*
+Looks up a cipher by the IANA identifier, returns a shared string for the OpenSSL name for the cipher,
+and emits a value indicating if the cipher belongs to the SSL2-TLS1.2 list, or the TLS1.3+ list.
+*/
+DLLEXPORT const char* CryptoNative_GetOpenSslCipherSuiteName(SSL* ssl, int32_t cipherSuite, int32_t* isTls12OrLower);
index 01cff41..eec818d 100644 (file)
@@ -117,6 +117,7 @@ namespace System.Net.Security
         public bool AllowRenegotiation { get { throw null; } set { } }
         public System.Collections.Generic.List<System.Net.Security.SslApplicationProtocol> ApplicationProtocols { get { throw null; } set { } }
         public System.Security.Cryptography.X509Certificates.X509RevocationMode CertificateRevocationCheckMode { get { throw null; } set { } }
+        public System.Net.Security.CipherSuitesPolicy CipherSuitesPolicy { get { throw null; } set { } }
         public System.Security.Cryptography.X509Certificates.X509CertificateCollection ClientCertificates { get { throw null; } set { } }
         public System.Security.Authentication.SslProtocols EnabledSslProtocols { get { throw null; } set { } }
         public System.Net.Security.EncryptionPolicy EncryptionPolicy { get { throw null; } set { } }
@@ -130,6 +131,7 @@ namespace System.Net.Security
         public bool AllowRenegotiation { get { throw null; } set { } }
         public System.Collections.Generic.List<System.Net.Security.SslApplicationProtocol> ApplicationProtocols { get { throw null; } set { } }
         public System.Security.Cryptography.X509Certificates.X509RevocationMode CertificateRevocationCheckMode { get { throw null; } set { } }
+        public System.Net.Security.CipherSuitesPolicy CipherSuitesPolicy { get { throw null; } set { } }
         public bool ClientCertificateRequired { get { throw null; } set { } }
         public System.Security.Authentication.SslProtocols EnabledSslProtocols { get { throw null; } set { } }
         public System.Net.Security.EncryptionPolicy EncryptionPolicy { get { throw null; } set { } }
@@ -213,6 +215,13 @@ namespace System.Net.Security
         public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; }
         public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory<byte> buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
     }
+    public sealed class CipherSuitesPolicy
+    {
+        [System.CLSCompliantAttribute(false)]
+        public CipherSuitesPolicy(System.Collections.Generic.IEnumerable<System.Net.Security.TlsCipherSuite> allowedCipherSuites) { }
+        [System.CLSCompliantAttribute(false)]
+        public System.Collections.Generic.IEnumerable<System.Net.Security.TlsCipherSuite> AllowedCipherSuites { get; }
+    }
     [System.CLSCompliantAttribute(false)]
     public enum TlsCipherSuite : ushort
     {
index 40c7686..7270ea8 100644 (file)
   <data name="net_conflicting_options" xml:space="preserve">
     <value>The '{0}' option was already set in the SslStream constructor.</value>
   </data>
+  <data name="net_ssl_ciphersuites_policy_not_supported" xml:space="preserve">
+    <value>CipherSuitesPolicy is not supported on this platform.</value>
+  </data>
 </root>
index ec3a4f7..d01de81 100644 (file)
@@ -23,6 +23,7 @@
     <Compile Include="System\Net\Security\SslStream.Implementation.Adapters.cs" />
     <Compile Include="System\Net\SslStreamContext.cs" />
     <Compile Include="System\Net\Security\AuthenticatedStream.cs" />
+    <Compile Include="System\Net\Security\CipherSuitesPolicy.cs" />
     <Compile Include="System\Net\Security\NetEventSource.Security.cs" />
     <Compile Include="System\Net\Security\SecureChannel.cs" />
     <Compile Include="System\Net\Security\SslSessionsCache.cs" />
   </ItemGroup>
   <ItemGroup Condition=" '$(TargetsWindows)' == 'true'">
     <Compile Include="System\Net\CertificateValidationPal.Windows.cs" />
+    <Compile Include="System\Net\Security\CipherSuitesPolicyPal.Windows.cs" />
     <Compile Include="System\Net\Security\NegotiateStreamPal.Windows.cs" />
     <Compile Include="System\Net\Security\NetEventSource.Security.Windows.cs" />
     <Compile Include="System\Net\Security\SslStreamPal.Windows.cs" />
   </ItemGroup>
   <ItemGroup Condition=" '$(TargetsUnix)' == 'true' AND '$(TargetsOSX)' != 'true' ">
     <Compile Include="System\Net\CertificateValidationPal.Unix.cs" />
+    <Compile Include="System\Net\Security\CipherSuitesPolicyPal.Linux.cs" />
     <Compile Include="System\Net\Security\SslStreamPal.Unix.cs" />
     <Compile Include="System\Net\Security\SslConnectionInfo.Linux.cs" />
     <Compile Include="System\Net\Security\StreamSizes.Unix.cs" />
     <Compile Include="System\Net\Security\SslConnectionInfo.OSX.cs" />
     <Compile Include="System\Net\Security\SslStreamPal.OSX.cs" />
     <Compile Include="System\Net\Security\StreamSizes.OSX.cs" />
+    <Compile Include="System\Net\Security\CipherSuitesPolicyPal.OSX.cs" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="Microsoft.Win32.Primitives" />
     <Reference Include="System.Collections.NonGeneric" />
     <Reference Include="System.Diagnostics.Debug" />
     <Reference Include="System.Diagnostics.Tracing" />
+    <Reference Include="System.Linq" />
     <Reference Include="System.Memory" />
     <Reference Include="System.Net.Primitives" />
     <Reference Include="System.Resources.ResourceManager" />
diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicy.cs b/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicy.cs
new file mode 100644 (file)
index 0000000..b07f85b
--- /dev/null
@@ -0,0 +1,41 @@
+// 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.Collections.Generic;
+
+namespace System.Net.Security
+{
+    /// <summary>
+    /// Specifies allowed cipher suites.
+    /// </summary>
+    public sealed partial class CipherSuitesPolicy
+    {
+        internal CipherSuitesPolicyPal Pal { get; private set; }
+
+        [CLSCompliant(false)]
+        public CipherSuitesPolicy(IEnumerable<TlsCipherSuite> allowedCipherSuites)
+        {
+            if (allowedCipherSuites == null)
+            {
+                throw new ArgumentNullException(nameof(allowedCipherSuites));
+            }
+
+            Pal = new CipherSuitesPolicyPal(allowedCipherSuites);
+        }
+
+        [CLSCompliant(false)]
+        public IEnumerable<TlsCipherSuite> AllowedCipherSuites
+        {
+            get
+            {
+                // This method is only useful only for diagnostic purposes so perf is not important
+                // We do not want users to be able to cast result to something they can modify
+                foreach (TlsCipherSuite cs in Pal.GetCipherSuites())
+                {
+                    yield return cs;
+                }
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.Linux.cs b/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.Linux.cs
new file mode 100644 (file)
index 0000000..a0044b7
--- /dev/null
@@ -0,0 +1,231 @@
+// 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 Microsoft.Win32.SafeHandles;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Security.Authentication;
+using System.Text;
+using Ssl = Interop.Ssl;
+using OpenSsl = Interop.OpenSsl;
+
+namespace System.Net.Security
+{
+    internal class CipherSuitesPolicyPal
+    {
+        private static readonly byte[] RequireEncryptionDefault =
+            Encoding.ASCII.GetBytes("DEFAULT\0");
+
+        private static readonly byte[] AllowNoEncryptionDefault =
+            Encoding.ASCII.GetBytes("ALL:eNULL\0");
+
+        private static readonly byte[] NoEncryptionDefault =
+            Encoding.ASCII.GetBytes("eNULL\0");
+
+        private byte[] _cipherSuites;
+        private byte[] _tls13CipherSuites;
+        private List<TlsCipherSuite> _tlsCipherSuites = new List<TlsCipherSuite>();
+
+        internal IEnumerable<TlsCipherSuite> GetCipherSuites() => _tlsCipherSuites;
+
+        internal CipherSuitesPolicyPal(IEnumerable<TlsCipherSuite> allowedCipherSuites)
+        {
+            if (!Interop.Ssl.Tls13Supported)
+            {
+                throw new PlatformNotSupportedException(SR.net_ssl_ciphersuites_policy_not_supported);
+            }
+
+            using (SafeSslContextHandle innerContext = Ssl.SslCtxCreate(Ssl.SslMethods.SSLv23_method))
+            {
+                if (innerContext.IsInvalid)
+                {
+                    throw OpenSsl.CreateSslException(SR.net_allocate_ssl_context_failed);
+                }
+
+                using (SafeSslHandle ssl = SafeSslHandle.Create(innerContext, false))
+                {
+                    if (ssl.IsInvalid)
+                    {
+                        throw OpenSsl.CreateSslException(SR.net_allocate_ssl_context_failed);
+                    }
+
+                    using (var tls13CipherSuites = new OpenSslStringBuilder())
+                    using (var cipherSuites = new OpenSslStringBuilder())
+                    {
+                        foreach (TlsCipherSuite cs in allowedCipherSuites)
+                        {
+                            string name = Interop.Ssl.GetOpenSslCipherSuiteName(
+                                ssl,
+                                cs,
+                                out bool isTls12OrLower);
+
+                            if (name == null)
+                            {
+                                // we do not have a corresponding name
+                                // allowing less than user requested is OK
+                                continue;
+                            }
+
+                            _tlsCipherSuites.Add(cs);
+                            (isTls12OrLower ? cipherSuites : tls13CipherSuites).AllowCipherSuite(name);
+                        }
+
+                        _cipherSuites = cipherSuites.GetOpenSslString();
+                        _tls13CipherSuites = tls13CipherSuites.GetOpenSslString();
+                    }
+                }
+            }
+        }
+
+        internal static bool ShouldOptOutOfTls13(CipherSuitesPolicy policy, EncryptionPolicy encryptionPolicy)
+        {
+            // if TLS 1.3 was explicitly requested the underlying code will throw
+            // if default option (SslProtocols.None) is used we will opt-out of TLS 1.3
+
+            if (encryptionPolicy == EncryptionPolicy.NoEncryption)
+            {
+                // TLS 1.3 uses different ciphersuite restrictions than previous versions.
+                // It has no equivalent to a NoEncryption option.
+                return true;
+            }
+
+            if (policy == null)
+            {
+                // null means default, by default OpenSSL will choose if it wants to opt-out or not
+                return false;
+            }
+
+            Debug.Assert(
+                policy.Pal._tls13CipherSuites.Length != 0 &&
+                    policy.Pal._tls13CipherSuites[policy.Pal._tls13CipherSuites.Length - 1] == 0,
+                "null terminated string expected");
+
+            // we should opt out only when policy is empty
+            return policy.Pal._tls13CipherSuites.Length == 1;
+        }
+
+        internal static bool ShouldOptOutOfLowerThanTls13(CipherSuitesPolicy policy, EncryptionPolicy encryptionPolicy)
+        {
+            if (policy == null)
+            {
+                // null means default, by default OpenSSL will choose if it wants to opt-out or not
+                return false;
+            }
+
+            Debug.Assert(
+                policy.Pal._cipherSuites.Length != 0 &&
+                    policy.Pal._cipherSuites[policy.Pal._cipherSuites.Length - 1] == 0,
+                "null terminated string expected");
+
+            // we should opt out only when policy is empty
+            return policy.Pal._cipherSuites.Length == 1;
+        }
+
+        private static bool IsOnlyTls13(SslProtocols protocols)
+            => protocols == SslProtocols.Tls13;
+
+        internal static bool WantsTls13(SslProtocols protocols)
+            => protocols == SslProtocols.None || (protocols & SslProtocols.Tls13) != 0;
+
+        internal static byte[] GetOpenSslCipherList(
+            CipherSuitesPolicy policy,
+            SslProtocols protocols,
+            EncryptionPolicy encryptionPolicy)
+        {
+            if (IsOnlyTls13(protocols))
+            {
+                // older cipher suites will be disabled through protocols
+                return null;
+            }
+
+            if (policy == null)
+            {
+                return CipherListFromEncryptionPolicy(encryptionPolicy);
+            }
+
+            if (encryptionPolicy == EncryptionPolicy.NoEncryption)
+            {
+                throw new PlatformNotSupportedException(SR.net_ssl_ciphersuites_policy_not_supported);
+            }
+
+            return policy.Pal._cipherSuites;
+        }
+
+        internal static byte[] GetOpenSslCipherSuites(
+            CipherSuitesPolicy policy,
+            SslProtocols protocols,
+            EncryptionPolicy encryptionPolicy)
+        {
+            if (!WantsTls13(protocols) || policy == null)
+            {
+                // do not call TLS 1.3 API, let OpenSSL choose what to do
+                return null;
+            }
+
+            if (encryptionPolicy == EncryptionPolicy.NoEncryption)
+            {
+                throw new PlatformNotSupportedException(SR.net_ssl_ciphersuites_policy_not_supported);
+            }
+
+            return policy.Pal._tls13CipherSuites;
+        }
+
+        private static byte[] CipherListFromEncryptionPolicy(EncryptionPolicy policy)
+        {
+            switch (policy)
+            {
+                case EncryptionPolicy.RequireEncryption:
+                    return RequireEncryptionDefault;
+                case EncryptionPolicy.AllowNoEncryption:
+                    return AllowNoEncryptionDefault;
+                case EncryptionPolicy.NoEncryption:
+                    return NoEncryptionDefault;
+                default:
+                    Debug.Fail($"Unknown EncryptionPolicy value ({policy})");
+                    return null;
+            }
+        }
+
+        private class OpenSslStringBuilder : StreamWriter
+        {
+            private const string SSL_TXT_Separator = ":";
+            private static readonly byte[] EmptyString = new byte[1] { 0 };
+
+            private MemoryStream _ms;
+            private bool _first = true;
+
+            public OpenSslStringBuilder() : base(new MemoryStream(), Encoding.ASCII)
+            {
+                _ms = (MemoryStream)BaseStream;
+            }
+
+            public void AllowCipherSuite(string cipherSuite)
+            {
+                if (_first)
+                {
+                    _first = false;
+                }
+                else
+                {
+                    Write(SSL_TXT_Separator);
+                }
+
+                Write(cipherSuite);
+            }
+
+            public byte[] GetOpenSslString()
+            {
+                if (_first)
+                {
+                    return EmptyString;
+                }
+
+                Flush();
+                _ms.WriteByte(0);
+                return _ms.ToArray();
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.OSX.cs b/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.OSX.cs
new file mode 100644 (file)
index 0000000..608364f
--- /dev/null
@@ -0,0 +1,24 @@
+// 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.Collections.Generic;
+using System.Linq;
+
+namespace System.Net.Security
+{
+    internal class CipherSuitesPolicyPal
+    {
+        internal uint[] TlsCipherSuites { get; private set; }
+
+        internal CipherSuitesPolicyPal(IEnumerable<TlsCipherSuite> allowedCipherSuites)
+        {
+            TlsCipherSuites = allowedCipherSuites.Select((cs) => (uint)cs).ToArray();
+        }
+
+        internal IEnumerable<TlsCipherSuite> GetCipherSuites()
+        {
+            return TlsCipherSuites.Select((cs) => (TlsCipherSuite)cs);
+        }
+    }
+}
diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.Windows.cs b/src/libraries/System.Net.Security/src/System/Net/Security/CipherSuitesPolicyPal.Windows.cs
new file mode 100644 (file)
index 0000000..9d0d614
--- /dev/null
@@ -0,0 +1,18 @@
+// 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.Collections.Generic;
+
+namespace System.Net.Security
+{
+    internal class CipherSuitesPolicyPal
+    {
+        internal CipherSuitesPolicyPal(IEnumerable<TlsCipherSuite> allowedCipherSuites)
+        {
+            throw new PlatformNotSupportedException(SR.net_ssl_ciphersuites_policy_not_supported);
+        }
+
+        internal IEnumerable<TlsCipherSuite> GetCipherSuites() => null;
+    }
+}
index cba6225..82a273a 100644 (file)
@@ -29,6 +29,8 @@ namespace System.Net
 
             try
             {
+                int osStatus;
+
                 unsafe
                 {
                     _readCallback = ReadFromConnection;
@@ -37,7 +39,7 @@ namespace System.Net
 
                 _sslContext = CreateSslContext(credential, sslAuthenticationOptions.IsServer);
 
-                int osStatus = Interop.AppleCrypto.SslSetIoCallbacks(
+                osStatus = Interop.AppleCrypto.SslSetIoCallbacks(
                     _sslContext,
                     _readCallback,
                     _writeCallback);
@@ -47,6 +49,27 @@ namespace System.Net
                     throw Interop.AppleCrypto.CreateExceptionForOSStatus(osStatus);
                 }
 
+                if (sslAuthenticationOptions.CipherSuitesPolicy != null)
+                {
+                    uint[] tlsCipherSuites = sslAuthenticationOptions.CipherSuitesPolicy.Pal.TlsCipherSuites;
+
+                    unsafe
+                    {
+                        fixed (uint* cipherSuites = tlsCipherSuites)
+                        {
+                            osStatus = Interop.AppleCrypto.SslSetEnabledCipherSuites(
+                                _sslContext,
+                                cipherSuites,
+                                tlsCipherSuites.Length);
+
+                            if (osStatus != 0)
+                            {
+                                throw Interop.AppleCrypto.CreateExceptionForOSStatus(osStatus);
+                            }
+                        }
+                    }
+                }
+
                 if (sslAuthenticationOptions.ApplicationProtocols != null)
                 {
                     // On OSX coretls supports only client side. For server, we will silently ignore the option.
@@ -314,7 +337,7 @@ namespace System.Net
             // If we didn't find an unset protocol after the min, go all the way to the last one.
             if (maxProtocolId == (SslProtocols)(-1))
             {
-                maxProtocolId = orderedSslProtocols[orderedSslProtocols.Length - 1]; 
+                maxProtocolId = orderedSslProtocols[orderedSslProtocols.Length - 1];
             }
 
             // Finally set this min and max.
index 7b64a7f..13fd8be 100644 (file)
@@ -27,6 +27,7 @@ namespace System.Net.Security
             CertSelectionDelegate = localCallback;
             CertificateRevocationCheckMode = sslClientAuthenticationOptions.CertificateRevocationCheckMode;
             ClientCertificates = sslClientAuthenticationOptions.ClientCertificates;
+            CipherSuitesPolicy = sslClientAuthenticationOptions.CipherSuitesPolicy;
         }
 
         internal SslAuthenticationOptions(SslServerAuthenticationOptions sslServerAuthenticationOptions)
@@ -48,6 +49,7 @@ namespace System.Net.Security
             // Server specific options.
             CertificateRevocationCheckMode = sslServerAuthenticationOptions.CertificateRevocationCheckMode;
             ServerCertificate = sslServerAuthenticationOptions.ServerCertificate;
+            CipherSuitesPolicy = sslServerAuthenticationOptions.CipherSuitesPolicy;
         }
 
         internal bool AllowRenegotiation { get; set; }
@@ -64,6 +66,7 @@ namespace System.Net.Security
         internal RemoteCertValidationCallback CertValidationDelegate { get; set; }
         internal LocalCertSelectionCallback CertSelectionDelegate { get; set; }
         internal ServerCertSelectionCallback ServerCertSelectionDelegate { get; set; }
+        internal CipherSuitesPolicy CipherSuitesPolicy { get; set; }
     }
 }
 
index bf68f1d..bffa7d4 100644 (file)
@@ -65,6 +65,12 @@ namespace System.Net.Security
             get => _enabledSslProtocols;
             set => _enabledSslProtocols = value;
         }
+
+        /// <summary>
+        /// Specifies cipher suites allowed to be used for TLS.
+        /// When set to null operating system default will be used.
+        /// Use extreme caution when changing this setting.
+        /// </summary>
+        public CipherSuitesPolicy CipherSuitesPolicy { get; set; }
     }
 }
-
index 5b3935b..614dff0 100644 (file)
@@ -64,6 +64,13 @@ namespace System.Net.Security
                 _encryptionPolicy = value;
             }
         }
+
+        /// <summary>
+        /// Specifies cipher suites allowed to be used for TLS.
+        /// When set to null operating system default will be used.
+        /// Use extreme caution when changing this setting.
+        /// </summary>
+        public CipherSuitesPolicy CipherSuitesPolicy { get; set; }
     }
 }
 
index 2d510ef..65dd43c 100644 (file)
@@ -135,7 +135,20 @@ internal static class EnumHelpers
 
     public static string ToFrameworkName(HashAlgorithmType val)
     {
-        return val == HashAlgorithmType.Aead ? "None" : val.ToString();
+        switch (val)
+        {
+            case HashAlgorithmType.Aead:
+            case HashAlgorithmType.None:
+                return "None";
+            case HashAlgorithmType.Md5:
+            case HashAlgorithmType.Sha1:
+            case HashAlgorithmType.Sha256:
+            case HashAlgorithmType.Sha384:
+            case HashAlgorithmType.Sha512:
+                return val.ToString();
+            default:
+                throw new Exception($"Value `HashAlgorithmType.{val}` does not have framework corresponding name. See TlsCipherSuiteNameParser.ttinclude: ToFrameworkName.");
+        }
     }
 }
 
index 4a249dd..18c77ff 100644 (file)
@@ -5,11 +5,13 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.IO;
+using System.Linq;
 using System.Net.Test.Common;
 using System.Security.Authentication;
 using System.Security.Cryptography.X509Certificates;
 using System.Threading;
 using System.Threading.Tasks;
+using Microsoft.DotNet.XUnitExtensions;
 using Xunit;
 
 namespace System.Net.Security.Tests
@@ -18,20 +20,44 @@ namespace System.Net.Security.Tests
 
     public class NegotiatedCipherSuiteTest
     {
+#pragma warning disable CS0618 // Ssl2 and Ssl3 are obsolete
+        private const SslProtocols AllProtocols =
+            SslProtocols.Ssl2 | SslProtocols.Ssl3 |
+            SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13;
+#pragma warning restore CS0618
+
+        private const SslProtocols NonTls13Protocols = AllProtocols & (~SslProtocols.Tls13);
+
         private static bool IsKnownPlatformSupportingTls13 => PlatformDetection.IsUbuntu1810OrHigher;
-        private static bool Tls13Supported { get; set; } = ProtocolsSupported(SslProtocols.Tls13);
+        private static bool CipherSuitesPolicySupported => s_cipherSuitePolicySupported.Value;
+        private static bool Tls13Supported { get; set; } = IsKnownPlatformSupportingTls13 || ProtocolsSupported(SslProtocols.Tls13);
+        private static bool CipherSuitesPolicyAndTls13Supported => Tls13Supported && CipherSuitesPolicySupported;
 
         private static HashSet<TlsCipherSuite> s_tls13CipherSuiteLookup = new HashSet<TlsCipherSuite>(GetTls13CipherSuites());
         private static HashSet<TlsCipherSuite> s_tls12CipherSuiteLookup = new HashSet<TlsCipherSuite>(GetTls12CipherSuites());
         private static HashSet<TlsCipherSuite> s_tls10And11CipherSuiteLookup = new HashSet<TlsCipherSuite>(GetTls10And11CipherSuites());
 
-        private static Dictionary<SslProtocols, HashSet<TlsCipherSuite>> _protocolCipherSuiteLookup = new Dictionary<SslProtocols, HashSet<TlsCipherSuite>>()
+        private static Dictionary<SslProtocols, HashSet<TlsCipherSuite>> s_protocolCipherSuiteLookup = new Dictionary<SslProtocols, HashSet<TlsCipherSuite>>()
         {
             { SslProtocols.Tls12, s_tls12CipherSuiteLookup },
             { SslProtocols.Tls11, s_tls10And11CipherSuiteLookup },
             { SslProtocols.Tls, s_tls10And11CipherSuiteLookup },
         };
 
+        private static Lazy<bool> s_cipherSuitePolicySupported = new Lazy<bool>(() =>
+        {
+            try
+            {
+                new CipherSuitesPolicy(Array.Empty<TlsCipherSuite>());
+                return true;
+            }
+            catch (PlatformNotSupportedException) { }
+
+            return false;
+        });
+
+        private static IReadOnlyList<TlsCipherSuite> SupportedNonTls13CipherSuites = GetSupportedNonTls13CipherSuites();
+
         [ConditionalFact(nameof(IsKnownPlatformSupportingTls13))]
         public void Tls13IsSupported_GetValue_ReturnsTrue()
         {
@@ -70,7 +96,7 @@ namespace System.Net.Security.Tests
             ret.Succeeded();
 
             Assert.True(
-                _protocolCipherSuiteLookup[protocol].Contains(ret.CipherSuite),
+                s_protocolCipherSuiteLookup[protocol].Contains(ret.CipherSuite),
                 $"`{ret.CipherSuite}` is not recognized as {protocol} cipher suite");
         }
 
@@ -84,6 +110,366 @@ namespace System.Net.Security.Tests
             }
         }
 
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_AllowSomeCipherSuitesWithNoEncryptionOption_Fails()
+        {
+            CheckPrereqsForNonTls13Tests(1);
+            var p = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(TlsCipherSuite.TLS_AES_128_GCM_SHA256,
+                                                 SupportedNonTls13CipherSuites[0]),
+                EncryptionPolicy = EncryptionPolicy.NoEncryption,
+            };
+
+            NegotiatedParams ret = ConnectAndGetNegotiatedParams(p, p);
+            ret.Failed();
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_NothingAllowed_Fails()
+        {
+            CipherSuitesPolicy csp = BuildPolicy();
+
+            var sp = new ConnectionParams();
+            sp.CipherSuitesPolicy = csp;
+
+            var cp = new ConnectionParams();
+            cp.CipherSuitesPolicy = csp;
+
+            NegotiatedParams ret = ConnectAndGetNegotiatedParams(sp, cp);
+            ret.Failed();
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicyAndTls13Supported))]
+        public void CipherSuitesPolicy_AllowOneOnOneSideTls13_Success()
+        {
+            bool hasSucceededAtLeastOnce = false;
+            AllowOneOnOneSide(GetTls13CipherSuites(),
+                              RequiredByTls13Spec,
+                              (cs) => hasSucceededAtLeastOnce = true);
+            Assert.True(hasSucceededAtLeastOnce);
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_AllowTwoOnBothSidesWithSingleOverlapNonTls13_Success()
+        {
+            CheckPrereqsForNonTls13Tests(3);
+            var a = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[0],
+                                                 SupportedNonTls13CipherSuites[1])
+            };
+            var b = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[1],
+                                                 SupportedNonTls13CipherSuites[2])
+            };
+
+            for (int i = 0; i < 2; i++)
+            {
+                NegotiatedParams ret = i == 0 ?
+                    ConnectAndGetNegotiatedParams(a, b) :
+                    ConnectAndGetNegotiatedParams(b, a);
+
+                ret.Succeeded();
+                ret.CheckCipherSuite(SupportedNonTls13CipherSuites[1]);
+            }
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_AllowTwoOnBothSidesWithNoOverlapNonTls13_Fails()
+        {
+            CheckPrereqsForNonTls13Tests(4);
+            var a = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[0],
+                                                 SupportedNonTls13CipherSuites[1])
+            };
+            var b = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[2],
+                                                 SupportedNonTls13CipherSuites[3])
+            };
+
+            for (int i = 0; i < 2; i++)
+            {
+                NegotiatedParams ret = i == 0 ?
+                    ConnectAndGetNegotiatedParams(a, b) :
+                    ConnectAndGetNegotiatedParams(b, a);
+
+                ret.Failed();
+            }
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_AllowSameTwoOnBothSidesLessPreferredIsTls13_Success()
+        {
+            CheckPrereqsForNonTls13Tests(1);
+            var p = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[0],
+                                                 TlsCipherSuite.TLS_AES_128_GCM_SHA256)
+            };
+
+            NegotiatedParams ret = ConnectAndGetNegotiatedParams(p, p);
+            ret.Succeeded();
+
+            // If both sides can speak TLS 1.3 they should speak it
+            if (Tls13Supported)
+            {
+                ret.CheckCipherSuite(TlsCipherSuite.TLS_AES_128_GCM_SHA256);
+            }
+            else
+            {
+                ret.CheckCipherSuite(SupportedNonTls13CipherSuites[0]);
+            }
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_TwoCipherSuitesWithAllOverlapping_Success()
+        {
+            CheckPrereqsForNonTls13Tests(2);
+            var a = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[0],
+                                                 SupportedNonTls13CipherSuites[1])
+            };
+            var b = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[1],
+                                                 SupportedNonTls13CipherSuites[0])
+            };
+
+            for (int i = 0; i < 2; i++)
+            {
+                bool isAClient = i == 0;
+                NegotiatedParams ret = isAClient ?
+                    ConnectAndGetNegotiatedParams(b, a) :
+                    ConnectAndGetNegotiatedParams(a, b);
+
+                ret.Succeeded();
+                Assert.True(ret.CipherSuite == SupportedNonTls13CipherSuites[0] ||
+                            ret.CipherSuite == SupportedNonTls13CipherSuites[1]);
+            }
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_ThreeCipherSuitesWithTwoOverlapping_Success()
+        {
+            CheckPrereqsForNonTls13Tests(4);
+            var a = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[0],
+                                                 SupportedNonTls13CipherSuites[1],
+                                                 SupportedNonTls13CipherSuites[2])
+            };
+            var b = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[3],
+                                                 SupportedNonTls13CipherSuites[2],
+                                                 SupportedNonTls13CipherSuites[1])
+            };
+
+            for (int i = 0; i < 2; i++)
+            {
+                bool isAClient = i == 0;
+                NegotiatedParams ret = isAClient ?
+                    ConnectAndGetNegotiatedParams(b, a) :
+                    ConnectAndGetNegotiatedParams(a, b);
+
+                ret.Succeeded();
+
+                Assert.True(ret.CipherSuite == SupportedNonTls13CipherSuites[1] ||
+                            ret.CipherSuite == SupportedNonTls13CipherSuites[2]);
+            }
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicyAndTls13Supported))]
+        public void CipherSuitesPolicy_OnlyTls13CipherSuiteAllowedButChosenProtocolsDoesNotAllowIt_Fails()
+        {
+            var a = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(TlsCipherSuite.TLS_AES_128_GCM_SHA256),
+                SslProtocols = NonTls13Protocols,
+            };
+
+            var b = new ConnectionParams();
+
+            for (int i = 0; i < 2; i++)
+            {
+                NegotiatedParams ret = i == 0 ?
+                    ConnectAndGetNegotiatedParams(a, b) :
+                    ConnectAndGetNegotiatedParams(b, a);
+                ret.Failed();
+            }
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicyAndTls13Supported))]
+        public void CipherSuitesPolicy_OnlyTls13CipherSuiteAllowedOtherSideDoesNotAllowTls13_Fails()
+        {
+            var a = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(TlsCipherSuite.TLS_AES_128_GCM_SHA256)
+            };
+
+            var b = new ConnectionParams()
+            {
+                SslProtocols = NonTls13Protocols
+            };
+
+            for (int i = 0; i < 2; i++)
+            {
+                NegotiatedParams ret = i == 0 ?
+                    ConnectAndGetNegotiatedParams(a, b) :
+                    ConnectAndGetNegotiatedParams(b, a);
+                ret.Failed();
+            }
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_OnlyNonTls13CipherSuitesAllowedButChosenProtocolDoesNotAllowIt_Fails()
+        {
+            CheckPrereqsForNonTls13Tests(1);
+            var a = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[0]),
+                SslProtocols = SslProtocols.Tls13,
+            };
+
+            var b = new ConnectionParams();
+
+            for (int i = 0; i < 2; i++)
+            {
+                NegotiatedParams ret = i == 0 ?
+                    ConnectAndGetNegotiatedParams(a, b) :
+                    ConnectAndGetNegotiatedParams(b, a);
+                ret.Failed();
+            }
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_OnlyNonTls13CipherSuiteAllowedButOtherSideDoesNotAllowIt_Fails()
+        {
+            CheckPrereqsForNonTls13Tests(1);
+            var a = new ConnectionParams()
+            {
+                CipherSuitesPolicy = BuildPolicy(SupportedNonTls13CipherSuites[0])
+            };
+
+            var b = new ConnectionParams()
+            {
+                SslProtocols = SslProtocols.Tls13
+            };
+
+            for (int i = 0; i < 2; i++)
+            {
+                NegotiatedParams ret = i == 0 ?
+                    ConnectAndGetNegotiatedParams(a, b) :
+                    ConnectAndGetNegotiatedParams(b, a);
+                ret.Failed();
+            }
+        }
+
+        [Fact]
+        public void CipherSuitesPolicy_CtorWithNull_Fails()
+        {
+            Assert.Throws<ArgumentNullException>(() => new CipherSuitesPolicy(null));
+        }
+
+        [ConditionalFact(nameof(CipherSuitesPolicySupported))]
+        public void CipherSuitesPolicy_AllowedCipherSuitesIncludesSubsetOfInput_Success()
+        {
+            TlsCipherSuite[] allCipherSuites = (TlsCipherSuite[])Enum.GetValues(typeof(TlsCipherSuite));
+            var r = new Random(123);
+            int[] numOfCipherSuites = new int[] { 0, 1, 2, 5, 10, 15, 30 };
+
+            foreach (int n in numOfCipherSuites)
+            {
+                HashSet<TlsCipherSuite> cipherSuites = PickRandomValues(allCipherSuites, n, r);
+                var csp = new CipherSuitesPolicy(cipherSuites);
+                Assert.NotNull(csp.AllowedCipherSuites);
+                Assert.InRange(csp.AllowedCipherSuites.Count(), 0, n);
+
+                foreach (var cs in csp.AllowedCipherSuites)
+                {
+                    Assert.True(cipherSuites.Contains(cs));
+                }
+            }
+        }
+
+        private HashSet<TlsCipherSuite> PickRandomValues(TlsCipherSuite[] all, int n, Random r)
+        {
+            var ret = new HashSet<TlsCipherSuite>();
+
+            while (ret.Count != n)
+            {
+                ret.Add(all[r.Next() % n]);
+            }
+
+            return ret;
+        }
+
+        private static void AllowOneOnOneSide(IEnumerable<TlsCipherSuite> cipherSuites,
+                                       Predicate<TlsCipherSuite> mustSucceed,
+                                       Action<TlsCipherSuite> cipherSuitePicked = null)
+        {
+            foreach (TlsCipherSuite cs in cipherSuites)
+            {
+                CipherSuitesPolicy csp = BuildPolicy(cs);
+
+                var paramsA = new ConnectionParams()
+                {
+                    CipherSuitesPolicy = csp,
+                };
+
+                var paramsB = new ConnectionParams();
+                int score = 0; // 1 for success 0 for fail. Sum should be even
+
+                for (int i = 0; i < 2; i++)
+                {
+                    NegotiatedParams ret = i == 0 ?
+                        ConnectAndGetNegotiatedParams(paramsA, paramsB) :
+                        ConnectAndGetNegotiatedParams(paramsB, paramsA);
+
+                    score += ret.HasSucceeded ? 1 : 0;
+                    if (mustSucceed(cs) || ret.HasSucceeded)
+                    {
+                        // we do not always guarantee success but if it succeeds it
+                        // must use the picked cipher suite
+                        ret.Succeeded();
+                        ret.CheckCipherSuite(cs);
+
+                        if (cipherSuitePicked != null && i == 0)
+                        {
+                            cipherSuitePicked(cs);
+                        }
+                    }
+                }
+
+                // we should either get 2 successes or 2 failures
+                Assert.True(score % 2 == 0);
+            }
+        }
+
+        private static void CheckPrereqsForNonTls13Tests(int minCipherSuites)
+        {
+            if (SupportedNonTls13CipherSuites.Count < minCipherSuites)
+            {
+                // We do not want to accidentally make the tests pass due to the bug in the code
+                // This situation is rather unexpected but can happen on i.e. Alpine
+                // Make sure at least some tests run.
+
+                if (Tls13Supported)
+                {
+                    throw new SkipTestException($"Test requires that at least {minCipherSuites} non TLS 1.3 cipher suites are supported.");
+                }
+                else
+                {
+                    throw new Exception($"Less than {minCipherSuites} cipher suites are supported: {string.Join(", ", SupportedNonTls13CipherSuites)}");
+                }
+            }
+        }
+
         private static bool ProtocolsSupported(SslProtocols protocols)
         {
             var defaultParams = new ConnectionParams();
@@ -165,6 +551,42 @@ namespace System.Net.Security.Tests
             yield return TlsCipherSuite.TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384;
         }
 
+        private static IEnumerable<TlsCipherSuite> GetNonTls13CipherSuites()
+        {
+            var tls13cs = new HashSet<TlsCipherSuite>(GetTls13CipherSuites());
+            foreach (TlsCipherSuite cs in typeof(TlsCipherSuite).GetEnumValues())
+            {
+                if (!tls13cs.Contains(cs))
+                {
+                    yield return cs;
+                }
+            }
+        }
+
+        private static IReadOnlyList<TlsCipherSuite> GetSupportedNonTls13CipherSuites()
+        {
+            // This function is used to initialize static property.
+            // We do not want skipped tests to fail because of that.
+            if (!CipherSuitesPolicySupported)
+                return null;
+
+            var ret = new List<TlsCipherSuite>();
+            AllowOneOnOneSide(GetNonTls13CipherSuites(), (cs) => false, (cs) => ret.Add(cs));
+
+            return ret;
+        }
+
+        private static bool RequiredByTls13Spec(TlsCipherSuite cs)
+        {
+            // per spec only one MUST be implemented
+            return cs == TlsCipherSuite.TLS_AES_128_GCM_SHA256;
+        }
+
+        private static CipherSuitesPolicy BuildPolicy(params TlsCipherSuite[] cipherSuites)
+        {
+            return new CipherSuitesPolicy(cipherSuites);
+        }
+
         private static async Task<Exception> WaitForSecureConnection(VirtualNetwork connection, Func<Task> server, Func<Task> client)
         {
             Task serverTask = null;
@@ -253,10 +675,12 @@ namespace System.Net.Security.Tests
                 serverOptions.ServerCertificate = Configuration.Certificates.GetSelfSignedServerCertificate();
                 serverOptions.EncryptionPolicy = serverParams.EncryptionPolicy;
                 serverOptions.EnabledSslProtocols = serverParams.SslProtocols;
+                serverOptions.CipherSuitesPolicy = serverParams.CipherSuitesPolicy;
 
                 var clientOptions = new SslClientAuthenticationOptions();
                 clientOptions.EncryptionPolicy = clientParams.EncryptionPolicy;
                 clientOptions.EnabledSslProtocols = clientParams.SslProtocols;
+                clientOptions.CipherSuitesPolicy = clientParams.CipherSuitesPolicy;
                 clientOptions.TargetHost = "test";
                 clientOptions.RemoteCertificateValidationCallback =
                     new RemoteCertificateValidationCallback((object sender,
@@ -293,6 +717,7 @@ namespace System.Net.Security.Tests
 
         private class ConnectionParams
         {
+            public CipherSuitesPolicy CipherSuitesPolicy = null;
             public EncryptionPolicy EncryptionPolicy = EncryptionPolicy.RequireEncryption;
             public SslProtocols SslProtocols = SslProtocols.None;
         }