diff --git a/src/libraries/System.Net.Quic/src/Resources/Strings.resx b/src/libraries/System.Net.Quic/src/Resources/Strings.resx index 4eb23ece75c923..1f237d53f319e0 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 190275ebbda3c6..d9aa1ec901791a 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 @@ internal enum QUIC_CREDENTIAL_FLAGS : uint 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 681bc94bdb40d0..14d910f2ee9ab5 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 @@ -242,6 +242,7 @@ internal struct CredentialConfig 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)] @@ -254,6 +255,7 @@ public struct Native internal IntPtr Principal; internal IntPtr Reserved; internal IntPtr AsyncHandler; + internal QUIC_ALLOWED_CIPHER_SUITE_FLAGS AllowedCipherSuites; public Native(CredentialConfig managed) { @@ -263,6 +265,7 @@ public Native(CredentialConfig managed) Principal = Marshal.StringToCoTaskMemUTF8(managed.Principal); Reserved = managed.Reserved; AsyncHandler = managed.AsyncHandler; + AllowedCipherSuites = managed.AllowedCipherSuites; } public CredentialConfig ToManaged() @@ -274,7 +277,8 @@ public CredentialConfig ToManaged() 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 1fe4a29004f60e..dba6e6cfb5301f 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 @@ public static SafeMsQuicConfigurationHandle Create(QuicClientConnectionOptions o 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 @@ public static SafeMsQuicConfigurationHandle Create(QuicClientConnectionOptions o } } - 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 @@ public static SafeMsQuicConfigurationHandle Create(QuicOptions options, SslServe 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 @@ public static SafeMsQuicConfigurationHandle Create(QuicOptions options, SslServe } } - 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 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, 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 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, { 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 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, 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 ab4969a817846a..9873a8d5a02160 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 @@ -57,6 +57,7 @@ public State(QuicListenerOptions options) 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 00000000000000..17793a6367b502 --- /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 39e6be421905a3..7e9bb17867284e 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs @@ -113,7 +113,7 @@ internal QuicListener CreateQuicListener(IPEndPoint endpoint) 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)