Add CipherSuitesPolicy support for MsQuic (#67239)
authorRadek Zikmund <32671551+rzikm@users.noreply.github.com>
Wed, 6 Apr 2022 08:08:50 +0000 (10:08 +0200)
committerGitHub <noreply@github.com>
Wed, 6 Apr 2022 08:08:50 +0000 (10:08 +0200)
* Add CipherSuitesPolicy support for MsQuic

* Add tests

* Code review feedback

* Fix test

src/libraries/System.Net.Quic/src/Resources/Strings.resx
src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicEnums.cs
src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicNativeMethods.cs
src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs
src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicListener.cs
src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs [new file with mode: 0644]
src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs

index 4eb23ec..1f237d5 100644 (file)
   <data name="net_quic_tls_version_notsupported" xml:space="preserve">
     <value>Could not use a TLS version required by Quic. TLS 1.3 may have been disabled in the registry.</value>
   </data>
+  <data name="net_quic_empty_cipher_suite" xml:space="preserve">
+    <value>CipherSuitePolicy must specify at least one cipher supported by QUIC.</value>
+  </data>
   <!-- Referenced in shared IPEndPointExtensions.cs-->
   <data name="net_InvalidAddressFamily" xml:space="preserve">
     <value>The AddressFamily {0} is not valid for the {1} end point, use {2} instead.</value>
index 7779732..956c425 100644 (file)
@@ -34,9 +34,19 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
         DEFER_CERTIFICATE_VALIDATION = 0x00000020, // Schannel only currently.
         REQUIRE_CLIENT_AUTHENTICATION = 0x00000040, // Schannel only currently.
         USE_TLS_BUILTIN_CERTIFICATE_VALIDATION = 0x00000080,
+        SET_ALLOWED_CIPHER_SUITES = 0x00002000,
         USE_PORTABLE_CERTIFICATES = 0x00004000,
     }
 
+    [Flags]
+    internal enum QUIC_ALLOWED_CIPHER_SUITE_FLAGS : uint
+    {
+        NONE = 0x0,
+        AES_128_GCM_SHA256 = 0x1,
+        AES_256_GCM_SHA384 = 0x2,
+        CHACHA20_POLY1305_SHA256 = 0x4,
+    }
+
     internal enum QUIC_CERTIFICATE_HASH_STORE_FLAGS
     {
         QUIC_CERTIFICATE_HASH_STORE_FLAG_NONE = 0x0000,
index ca92b30..6a7e35c 100644 (file)
@@ -246,6 +246,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
             internal IntPtr Reserved; // Currently unused
             // TODO: define delegate for AsyncHandler and make proper use of it.
             internal IntPtr AsyncHandler;
+            internal QUIC_ALLOWED_CIPHER_SUITE_FLAGS AllowedCipherSuites;
 
             [CustomTypeMarshaller(typeof(CredentialConfig), Features = CustomTypeMarshallerFeatures.UnmanagedResources)]
             [StructLayout(LayoutKind.Sequential)]
@@ -258,6 +259,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
                 internal IntPtr Principal;
                 internal IntPtr Reserved;
                 internal IntPtr AsyncHandler;
+                internal QUIC_ALLOWED_CIPHER_SUITE_FLAGS AllowedCipherSuites;
 
                 public Native(CredentialConfig managed)
                 {
@@ -267,6 +269,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
                     Principal = Marshal.StringToCoTaskMemUTF8(managed.Principal);
                     Reserved = managed.Reserved;
                     AsyncHandler = managed.AsyncHandler;
+                    AllowedCipherSuites = managed.AllowedCipherSuites;
                 }
 
                 public CredentialConfig ToManaged()
@@ -278,7 +281,8 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
                         Certificate = Certificate,
                         Principal = Marshal.PtrToStringUTF8(Principal)!,
                         Reserved = Reserved,
-                        AsyncHandler = AsyncHandler
+                        AsyncHandler = AsyncHandler,
+                        AllowedCipherSuites = AllowedCipherSuites
                     };
                 }
 
index 1fe4a29..dba6e6c 100644 (file)
@@ -38,11 +38,6 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
 
             if (options.ClientAuthenticationOptions != null)
             {
-                if (options.ClientAuthenticationOptions.CipherSuitesPolicy != null)
-                {
-                    throw new PlatformNotSupportedException(SR.Format(SR.net_quic_ssl_option, nameof(options.ClientAuthenticationOptions.CipherSuitesPolicy)));
-                }
-
 #pragma warning disable SYSLIB0040 // NoEncryption and AllowNoEncryption are obsolete
                 if (options.ClientAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.NoEncryption)
                 {
@@ -68,7 +63,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
                 }
             }
 
-            return Create(options, QUIC_CREDENTIAL_FLAGS.CLIENT, certificate: certificate, certificateContext: null, options.ClientAuthenticationOptions?.ApplicationProtocols);
+            return Create(options, QUIC_CREDENTIAL_FLAGS.CLIENT, certificate: certificate, certificateContext: null, options.ClientAuthenticationOptions?.ApplicationProtocols, options.ClientAuthenticationOptions?.CipherSuitesPolicy);
         }
 
         public static SafeMsQuicConfigurationHandle Create(QuicOptions options, SslServerAuthenticationOptions? serverAuthenticationOptions, string? targetHost = null)
@@ -78,11 +73,6 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
 
             if (serverAuthenticationOptions != null)
             {
-                if (serverAuthenticationOptions.CipherSuitesPolicy != null)
-                {
-                    throw new PlatformNotSupportedException(SR.Format(SR.net_quic_ssl_option, nameof(serverAuthenticationOptions.CipherSuitesPolicy)));
-                }
-
 #pragma warning disable SYSLIB0040 // NoEncryption and AllowNoEncryption are obsolete
                 if (serverAuthenticationOptions.EncryptionPolicy == EncryptionPolicy.NoEncryption)
                 {
@@ -101,12 +91,12 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
                 }
             }
 
-            return Create(options, flags, certificate, serverAuthenticationOptions?.ServerCertificateContext, serverAuthenticationOptions?.ApplicationProtocols);
+            return Create(options, flags, certificate, serverAuthenticationOptions?.ServerCertificateContext, serverAuthenticationOptions?.ApplicationProtocols, serverAuthenticationOptions?.CipherSuitesPolicy);
         }
 
         // TODO: this is called from MsQuicListener and when it fails it wreaks havoc in MsQuicListener finalizer.
         //       Consider moving bigger logic like this outside of constructor call chains.
-        private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, SslStreamCertificateContext? certificateContext, List<SslApplicationProtocol>? alpnProtocols)
+        private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, SslStreamCertificateContext? certificateContext, List<SslApplicationProtocol>? alpnProtocols, CipherSuitesPolicy? cipherSuitesPolicy)
         {
             // TODO: some of these checks should be done by the QuicOptions type.
             if (alpnProtocols == null || alpnProtocols.Count == 0)
@@ -190,10 +180,16 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
                 CredentialConfig config = default;
                 config.Flags = flags; // TODO: consider using LOAD_ASYNCHRONOUS with a callback.
 
+                if (cipherSuitesPolicy != null)
+                {
+                    config.Flags |= QUIC_CREDENTIAL_FLAGS.SET_ALLOWED_CIPHER_SUITES;
+                    config.AllowedCipherSuites = CipherSuitePolicyToFlags(cipherSuitesPolicy);
+                }
+
                 if (certificateContext != null)
                 {
-                    certificate = (X509Certificate2?) _contextCertificate.GetValue(certificateContext);
-                    intermediates = (X509Certificate2[]?) _contextChain.GetValue(certificateContext);
+                    certificate = (X509Certificate2?)_contextCertificate.GetValue(certificateContext);
+                    intermediates = (X509Certificate2[]?)_contextChain.GetValue(certificateContext);
 
                     if (certificate == null || intermediates == null)
                     {
@@ -218,7 +214,7 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
                         {
                             X509Certificate2Collection collection = new X509Certificate2Collection();
                             collection.Add(certificate);
-                            for (int i= 0; i < intermediates?.Length; i++)
+                            for (int i = 0; i < intermediates?.Length; i++)
                             {
                                 collection.Add(intermediates[i]);
                             }
@@ -265,5 +261,37 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
 
             return configurationHandle;
         }
+
+        private static QUIC_ALLOWED_CIPHER_SUITE_FLAGS CipherSuitePolicyToFlags(CipherSuitesPolicy cipherSuitesPolicy)
+        {
+            QUIC_ALLOWED_CIPHER_SUITE_FLAGS flags = QUIC_ALLOWED_CIPHER_SUITE_FLAGS.NONE;
+
+            foreach (TlsCipherSuite cipher in cipherSuitesPolicy.AllowedCipherSuites)
+            {
+                switch (cipher)
+                {
+                    case TlsCipherSuite.TLS_AES_128_GCM_SHA256:
+                        flags |= QUIC_ALLOWED_CIPHER_SUITE_FLAGS.AES_128_GCM_SHA256;
+                        break;
+                    case TlsCipherSuite.TLS_AES_256_GCM_SHA384:
+                        flags |= QUIC_ALLOWED_CIPHER_SUITE_FLAGS.AES_256_GCM_SHA384;
+                        break;
+                    case TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256:
+                        flags |= QUIC_ALLOWED_CIPHER_SUITE_FLAGS.CHACHA20_POLY1305_SHA256;
+                        break;
+                    case TlsCipherSuite.TLS_AES_128_CCM_SHA256: // not supported by MsQuic (yet?), but QUIC RFC allows it so we ignore it.
+                    default:
+                        // ignore
+                        break;
+                }
+            }
+
+            if (flags == QUIC_ALLOWED_CIPHER_SUITE_FLAGS.NONE)
+            {
+                throw new ArgumentException(SR.net_quic_empty_cipher_suite, nameof(SslClientAuthenticationOptions.CipherSuitesPolicy));
+            }
+
+            return flags;
+        }
     }
 }
index a5c4e64..7c68313 100644 (file)
@@ -59,6 +59,7 @@ namespace System.Net.Quic.Implementations.MsQuic
                     AuthenticationOptions.RemoteCertificateValidationCallback = options.ServerAuthenticationOptions.RemoteCertificateValidationCallback;
                     AuthenticationOptions.ServerCertificateSelectionCallback = options.ServerAuthenticationOptions.ServerCertificateSelectionCallback;
                     AuthenticationOptions.ApplicationProtocols = options.ServerAuthenticationOptions.ApplicationProtocols;
+                    AuthenticationOptions.CipherSuitesPolicy = options.ServerAuthenticationOptions.CipherSuitesPolicy;
 
                     if (options.ServerAuthenticationOptions.ServerCertificate == null && options.ServerAuthenticationOptions.ServerCertificateContext == null &&
                         options.ServerAuthenticationOptions.ServerCertificateSelectionCallback != null)
diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs
new file mode 100644 (file)
index 0000000..17793a6
--- /dev/null
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+using System.Net.Quic.Implementations;
+using System.Net.Security;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace System.Net.Quic.Tests
+{
+    [ConditionalClass(typeof(QuicTestBase<MsQuicProviderFactory>), nameof(IsSupported))]
+    [Collection(nameof(DisableParallelization))]
+    [SkipOnPlatform(TestPlatforms.Windows, "CipherSuitesPolicy is not supported on Windows")]
+    public class MsQuicCipherSuitesPolicyTests : QuicTestBase<MsQuicProviderFactory>
+    {
+        public MsQuicCipherSuitesPolicyTests(ITestOutputHelper output) : base(output) { }
+
+        private async Task TestConnection(CipherSuitesPolicy serverPolicy, CipherSuitesPolicy clientPolicy)
+        {
+            var listenerOptions = CreateQuicListenerOptions();
+            listenerOptions.ServerAuthenticationOptions.CipherSuitesPolicy = serverPolicy;
+            using QuicListener listener = CreateQuicListener(listenerOptions);
+
+            var clientOptions = CreateQuicClientOptions();
+            clientOptions.ClientAuthenticationOptions.CipherSuitesPolicy = clientPolicy;
+            clientOptions.RemoteEndPoint = listener.ListenEndPoint;
+            using QuicConnection clientConnection = CreateQuicConnection(clientOptions);
+
+            await clientConnection.ConnectAsync();
+            await clientConnection.CloseAsync(0);
+        }
+
+        [Fact]
+        public Task SupportedCipher_Success()
+        {
+            CipherSuitesPolicy policy = new CipherSuitesPolicy(new[] { TlsCipherSuite.TLS_AES_128_GCM_SHA256 });
+            return TestConnection(policy, policy);
+        }
+
+        [Theory]
+        [InlineData(new TlsCipherSuite[] { })]
+        [InlineData(new[] { TlsCipherSuite.TLS_AES_128_CCM_8_SHA256 })]
+        public void NoSupportedCiphers_ThrowsArgumentException(TlsCipherSuite[] ciphers)
+        {
+            CipherSuitesPolicy policy = new CipherSuitesPolicy(ciphers);
+            var listenerOptions = CreateQuicListenerOptions();
+            listenerOptions.ServerAuthenticationOptions.CipherSuitesPolicy = policy;
+            Assert.Throws<ArgumentException>(() => CreateQuicListener(listenerOptions));
+
+            var clientOptions = CreateQuicClientOptions();
+            clientOptions.ClientAuthenticationOptions.CipherSuitesPolicy = policy;
+            clientOptions.RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 5000);
+            Assert.Throws<ArgumentException>(() => CreateQuicConnection(clientOptions));
+        }
+
+        [Fact]
+        public async Task MismatchedCipherPolicies_ConnectAsync_ThrowsQuicException()
+        {
+            await Assert.ThrowsAsync<QuicException>(() => TestConnection(
+               new CipherSuitesPolicy(new[] { TlsCipherSuite.TLS_AES_128_GCM_SHA256 }),
+               new CipherSuitesPolicy(new[] { TlsCipherSuite.TLS_AES_256_GCM_SHA384 })
+            ));
+        }
+    }
+}
\ No newline at end of file
index a9f36cc..c0b8b4a 100644 (file)
@@ -114,7 +114,7 @@ namespace System.Net.Quic.Tests
             return CreateQuicListener(options);
         }
 
-        private QuicListener CreateQuicListener(QuicListenerOptions options) => new QuicListener(ImplementationProvider, options);
+        internal QuicListener CreateQuicListener(QuicListenerOptions options) => new QuicListener(ImplementationProvider, options);
 
         internal Task<(QuicConnection, QuicConnection)> CreateConnectedQuicConnection(QuicListener listener) => CreateConnectedQuicConnection(null, listener);
         internal async Task<(QuicConnection, QuicConnection)> CreateConnectedQuicConnection(QuicClientConnectionOptions? clientOptions, QuicListenerOptions listenerOptions)