From f6ca720b4276e30b1494400dd11b3e704286aba3 Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Wed, 6 Apr 2022 10:08:50 +0200 Subject: [PATCH] Add CipherSuitesPolicy support for MsQuic (#67239) * Add CipherSuitesPolicy support for MsQuic * Add tests * Code review feedback * Fix test --- .../System.Net.Quic/src/Resources/Strings.resx | 3 + .../Implementations/MsQuic/Interop/MsQuicEnums.cs | 10 ++++ .../MsQuic/Interop/MsQuicNativeMethods.cs | 6 +- .../Interop/SafeMsQuicConfigurationHandle.cs | 60 ++++++++++++++------ .../Quic/Implementations/MsQuic/MsQuicListener.cs | 1 + .../MsQuicCipherSuitesPolicyTests.cs | 65 ++++++++++++++++++++++ .../tests/FunctionalTests/QuicTestBase.cs | 2 +- 7 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs diff --git a/src/libraries/System.Net.Quic/src/Resources/Strings.resx b/src/libraries/System.Net.Quic/src/Resources/Strings.resx index 4eb23ec..1f237d5 100644 --- a/src/libraries/System.Net.Quic/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Quic/src/Resources/Strings.resx @@ -168,6 +168,9 @@ Could not use a TLS version required by Quic. TLS 1.3 may have been disabled in the registry. + + CipherSuitePolicy must specify at least one cipher supported by QUIC. + The AddressFamily {0} is not valid for the {1} end point, use {2} instead. diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicEnums.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicEnums.cs index 7779732..956c425 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicEnums.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicEnums.cs @@ -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, diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicNativeMethods.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicNativeMethods.cs index ca92b30..6a7e35c 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicNativeMethods.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicNativeMethods.cs @@ -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 }; } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs index 1fe4a290..dba6e6c 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs @@ -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? alpnProtocols) + private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, SslStreamCertificateContext? certificateContext, List? 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; + } } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicListener.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicListener.cs index a5c4e64..7c68313 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicListener.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicListener.cs @@ -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 index 0000000..17793a6 --- /dev/null +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicCipherSuitesPolicyTests.cs @@ -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), nameof(IsSupported))] + [Collection(nameof(DisableParallelization))] + [SkipOnPlatform(TestPlatforms.Windows, "CipherSuitesPolicy is not supported on Windows")] + public class MsQuicCipherSuitesPolicyTests : QuicTestBase + { + 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(() => CreateQuicListener(listenerOptions)); + + var clientOptions = CreateQuicClientOptions(); + clientOptions.ClientAuthenticationOptions.CipherSuitesPolicy = policy; + clientOptions.RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 5000); + Assert.Throws(() => CreateQuicConnection(clientOptions)); + } + + [Fact] + public async Task MismatchedCipherPolicies_ConnectAsync_ThrowsQuicException() + { + await Assert.ThrowsAsync(() => 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 diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs index a9f36cc..c0b8b4a 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs @@ -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) -- 2.7.4