From 3abf03e165dee85dda0c2923f81e897652cfc41b Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 17 Jun 2021 00:27:58 +0200 Subject: [PATCH 1/2] add basic support for client certificte --- .../Interop/SafeMsQuicConfigurationHandle.cs | 30 ++++++++++++-- .../MsQuic/MsQuicConnection.cs | 19 +++++++-- .../Implementations/MsQuic/MsQuicListener.cs | 13 +++++- .../tests/FunctionalTests/MsQuicTests.cs | 41 +++++++++++++++++++ .../tests/FunctionalTests/QuicTestBase.cs | 33 +++++++++++++++ 5 files changed, 128 insertions(+), 8 deletions(-) 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 a51927e915d48..988d5ef57459e 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 @@ -34,14 +34,36 @@ protected override bool ReleaseHandle() // TODO: consider moving the static code from here to keep all the handle classes small and simple. public static unsafe SafeMsQuicConfigurationHandle Create(QuicClientConnectionOptions options) { - // TODO: lots of ClientAuthenticationOptions are not yet supported by MsQuic. - return Create(options, QUIC_CREDENTIAL_FLAGS.CLIENT, certificate: null, certificateContext: null, options.ClientAuthenticationOptions?.ApplicationProtocols); + X509Certificate? certificate = null; + if (options.ClientAuthenticationOptions?.ClientCertificates != null) + { + foreach (var cert in options.ClientAuthenticationOptions.ClientCertificates) + { + try + { + if (((X509Certificate2)cert).HasPrivateKey) + { + // Pick first certificate with private key. + certificate = cert; + break; + } + } + catch { }; + } + } + + return Create(options, QUIC_CREDENTIAL_FLAGS.CLIENT, certificate: certificate, certificateContext: null, options.ClientAuthenticationOptions?.ApplicationProtocols); } public static unsafe SafeMsQuicConfigurationHandle Create(QuicListenerOptions options) { - // TODO: lots of ServerAuthenticationOptions are not yet supported by MsQuic. - return Create(options, QUIC_CREDENTIAL_FLAGS.NONE, options.ServerAuthenticationOptions?.ServerCertificate, options.ServerAuthenticationOptions?.ServerCertificateContext, options.ServerAuthenticationOptions?.ApplicationProtocols); + QUIC_CREDENTIAL_FLAGS flags = QUIC_CREDENTIAL_FLAGS.NONE; + if (options.ServerAuthenticationOptions != null && options.ServerAuthenticationOptions.ClientCertificateRequired) + { + flags |= QUIC_CREDENTIAL_FLAGS.REQUIRE_CLIENT_AUTHENTICATION | QUIC_CREDENTIAL_FLAGS.INDICATE_CERTIFICATE_RECEIVED | QUIC_CREDENTIAL_FLAGS.NO_CERTIFICATE_VALIDATION; + } + + return Create(options, flags, options.ServerAuthenticationOptions?.ServerCertificate, options.ServerAuthenticationOptions?.ServerCertificateContext, options.ServerAuthenticationOptions?.ApplicationProtocols); } // TODO: this is called from MsQuicListener and when it fails it wreaks havoc in MsQuicListener finalizer. diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs index f31dfc36fd5bf..cbfe540ea4baa 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs @@ -126,15 +126,23 @@ public void SetClosing() } // constructor for inbound connections - public MsQuicConnection(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SafeMsQuicConnectionHandle handle) + public MsQuicConnection(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, SafeMsQuicConnectionHandle handle, bool remoteCertificateRequired = false, X509RevocationMode revocationMode = X509RevocationMode.Offline, RemoteCertificateValidationCallback? remoteCertificateValidationCallback = null) { _state.Handle = handle; _state.StateGCHandle = GCHandle.Alloc(_state); _state.Connected = true; + _isServer = true; _localEndPoint = localEndPoint; _remoteEndPoint = remoteEndPoint; - _remoteCertificateRequired = false; - _isServer = true; + _remoteCertificateRequired = remoteCertificateRequired; + _revocationMode = revocationMode; + _remoteCertificateValidationCallback = remoteCertificateValidationCallback; + + if (_remoteCertificateRequired) + { + // We need to link connection for the validation callback. + _state.Connection = this; + } try { @@ -333,6 +341,11 @@ private static uint HandleEventPeerCertificateReceived(State state, ref Connecti return MsQuicStatusCodes.InvalidState; } + if (connection._isServer) + { + state.Connection = null; + } + try { if (connectionEvent.Data.PeerCertificateReceived.PlatformCertificateHandle != IntPtr.Zero) 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 7d18fa08a479d..569ce23727d1f 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 @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Net.Quic.Implementations.MsQuic.Internal; using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Channels; @@ -31,9 +32,19 @@ private sealed class State public readonly SafeMsQuicConfigurationHandle ConnectionConfiguration; public readonly Channel AcceptConnectionQueue; + public bool RemoteCertificateRequired; + public X509RevocationMode RevocationMode = X509RevocationMode.Offline; + public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback; + public State(QuicListenerOptions options) { ConnectionConfiguration = SafeMsQuicConfigurationHandle.Create(options); + if (options.ServerAuthenticationOptions != null) + { + RemoteCertificateRequired = options.ServerAuthenticationOptions.ClientCertificateRequired; + RevocationMode = options.ServerAuthenticationOptions.CertificateRevocationCheckMode; + RemoteCertificateValidationCallback = options.ServerAuthenticationOptions.RemoteCertificateValidationCallback; + } AcceptConnectionQueue = Channel.CreateBounded(new BoundedChannelOptions(options.ListenBacklog) { @@ -182,7 +193,7 @@ private static unsafe uint NativeCallbackHandler( uint status = MsQuicApi.Api.ConnectionSetConfigurationDelegate(connectionHandle, state.ConnectionConfiguration); QuicExceptionHelpers.ThrowIfFailed(status, "ConnectionSetConfiguration failed."); - var msQuicConnection = new MsQuicConnection(localEndPoint, remoteEndPoint, connectionHandle); + var msQuicConnection = new MsQuicConnection(localEndPoint, remoteEndPoint, connectionHandle, state.RemoteCertificateRequired, state.RevocationMode, state.RemoteCertificateValidationCallback); msQuicConnection.SetNegotiatedAlpn(connectionInfo.NegotiatedAlpn, connectionInfo.NegotiatedAlpnLength); if (!state.AcceptConnectionQueue.Writer.TryWrite(msQuicConnection)) diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs index 612da52b26172..22571b14e8976 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs @@ -115,6 +115,47 @@ public async Task ConnectWithCertificateChain() await clientTask; } + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public async Task ConnectWithClientCertificate() + { + bool clientCertificateOK = false; + + var serverOptions = new QuicListenerOptions(); + serverOptions.ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + serverOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions(); + serverOptions.ServerAuthenticationOptions.ClientCertificateRequired = true; + serverOptions.ServerAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => + { + _output.WriteLine("client certificate {0}", cert); + Assert.NotNull(cert); + Assert.Equal(ClientCertificate.Thumbprint, ((X509Certificate2)cert).Thumbprint); + + clientCertificateOK = true; + return true; + }; + using QuicListener listener = new QuicListener(QuicImplementationProviders.MsQuic, serverOptions); + + QuicClientConnectionOptions clientOptions = new QuicClientConnectionOptions() + { + RemoteEndPoint = listener.ListenEndPoint, + ClientAuthenticationOptions = GetSslClientAuthenticationOptions(), + }; + clientOptions.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection() { ClientCertificate }; + + using QuicConnection clientConnection = new QuicConnection(QuicImplementationProviders.MsQuic, clientOptions); + ValueTask clientTask = clientConnection.ConnectAsync(); + + using QuicConnection serverConnection = await listener.AcceptConnectionAsync(); + await clientTask; + // Verify functionality of the connections. + await PingPong(clientConnection, serverConnection); + // check we completed the client certificate verification. + Assert.True(clientCertificateOK); + + await serverConnection.CloseAsync(0); + } + [Fact] [ActiveIssue("https://github.com/dotnet/runtime/issues/52048")] public async Task WaitForAvailableUnidirectionStreamsAsyncWorks() diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs index ee7501868beba..9d40012711c49 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs @@ -15,6 +15,8 @@ namespace System.Net.Quic.Tests public abstract class QuicTestBase where T : IQuicImplProviderFactory, new() { + private static readonly byte[] s_ping = Encoding.UTF8.GetBytes("PING"); + private static readonly byte[] s_pong = Encoding.UTF8.GetBytes("PONG"); private static readonly IQuicImplProviderFactory s_factory = new T(); public static QuicImplementationProvider ImplementationProvider { get; } = s_factory.GetProvider(); @@ -23,6 +25,7 @@ public abstract class QuicTestBase public static SslApplicationProtocol ApplicationProtocol { get; } = new SslApplicationProtocol("quictest"); public X509Certificate2 ServerCertificate = System.Net.Test.Common.Configuration.Certificates.GetServerCertificate(); + public X509Certificate2 ClientCertificate = System.Net.Test.Common.Configuration.Certificates.GetClientCertificate(); public bool RemoteCertificateValidationCallback(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) { @@ -75,6 +78,36 @@ internal QuicListener CreateQuicListener(IPEndPoint endpoint) return CreateQuicListener(options); } + internal async Task PingPong(QuicConnection client, QuicConnection server) + { + using QuicStream clientStream = client.OpenBidirectionalStream(); + ValueTask t = clientStream.WriteAsync(s_ping); + using QuicStream serverStream = await server.AcceptStreamAsync(); + + byte[] buffer = new byte[s_ping.Length]; + int remains = s_ping.Length; + while (remains > 0) + { + int readLength = await serverStream.ReadAsync(buffer, buffer.Length - remains, remains); + Assert.True(readLength > 0); + remains -= readLength; + } + Assert.Equal(s_ping, buffer); + await t; + + t = serverStream.WriteAsync(s_pong); + remains = s_pong.Length; + while (remains > 0) + { + int readLength = await clientStream.ReadAsync(buffer, buffer.Length - remains, remains); + Assert.True(readLength > 0); + remains -= readLength; + } + + Assert.Equal(s_pong, buffer); + await t; + } + private QuicListener CreateQuicListener(QuicListenerOptions options) => new QuicListener(ImplementationProvider, options); internal async Task RunClientServer(Func clientFunction, Func serverFunction, int iterations = 1, int millisecondsTimeout = 10_000) From 6509f9f1d52b84caa75580f75cc711f729c1763d Mon Sep 17 00:00:00 2001 From: wfurt Date: Thu, 8 Jul 2021 17:56:35 -0700 Subject: [PATCH 2/2] feedback from review --- .../MsQuic/Interop/SafeMsQuicConfigurationHandle.cs | 2 +- .../System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) 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 988d5ef57459e..e1b49ce2753c1 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 @@ -48,7 +48,7 @@ public static unsafe SafeMsQuicConfigurationHandle Create(QuicClientConnectionOp break; } } - catch { }; + catch { } } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs index cbfe540ea4baa..1107c22656437 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs @@ -141,6 +141,9 @@ public MsQuicConnection(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, Saf if (_remoteCertificateRequired) { // We need to link connection for the validation callback. + // We need to be able to find the connection in HandleEventPeerCertificateReceived + // and dispatch it as sender to validation callback. + // After that Connection will be set back to null. _state.Connection = this; }