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