From 01b7e73cd378145264a7cb7a09365b41ed42b240 Mon Sep 17 00:00:00 2001 From: Tomas Weinfurt Date: Fri, 9 Apr 2021 09:29:05 -0700 Subject: [PATCH] basic certificate handling for quic (#50613) * basic certificate handling for quic * fix linux * fix macOS * feedback from review * feedback from review --- .../src/System.Net.Quic.csproj | 5 + .../MsQuic/Interop/MsQuicEnums.cs | 2 +- .../MsQuic/Interop/MsQuicNativeMethods.cs | 42 +++++++-- .../MsQuic/Interop/MsQuicStatusCodes.cs | 12 ++- .../MsQuic/Interop/MsQuicStatusHelper.cs | 2 +- .../Interop/SafeMsQuicConfigurationHandle.cs | 49 +++++++--- .../MsQuic/MsQuicConnection.cs | 92 ++++++++++++++++++- .../tests/FunctionalTests/MsQuicTestBase.cs | 3 +- .../tests/FunctionalTests/QuicTestBase.cs | 3 +- 9 files changed, 176 insertions(+), 34 deletions(-) diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index 6ad41a39fdf03..8ef23c8a93b4f 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -52,6 +52,7 @@ + @@ -66,6 +67,10 @@ PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest + PreserveNewest PreserveNewest 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 34dd31bf70ce8..33ffea25d1cec 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 @@ -19,7 +19,7 @@ internal enum QUIC_CREDENTIAL_TYPE : uint CONTEXT, FILE, FILE_PROTECTED, - STUB_NULL = 0xF0000000, // Pass as server cert to stubtls implementation. + PKCS12, } [Flags] 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 131227746c3ad..f3ea17bd0360c 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 @@ -215,30 +215,33 @@ internal struct CredentialConfig internal struct CredentialConfigCertificateUnion { [FieldOffset(0)] - internal CredentialConfigCertificateCertificateHash CertificateHash; + internal CredentialConfigCertificateHash CertificateHash; [FieldOffset(0)] - internal CredentialConfigCertificateCertificateHashStore CertificateHashStore; + internal CredentialConfigCertificateHashStore CertificateHashStore; [FieldOffset(0)] internal IntPtr CertificateContext; [FieldOffset(0)] - internal CredentialConfigCertificateCertificateFile CertificateFile; + internal CredentialConfigCertificateFile CertificateFile; [FieldOffset(0)] - internal CredentialConfigCertificateCertificateFileProtected CertificateFileProtected; + internal CredentialConfigCertificateFileProtected CertificateFileProtected; + + [FieldOffset(0)] + internal CredentialConfigCertificatePkcs12 CertificatePkcs12; } [StructLayout(LayoutKind.Sequential)] - internal struct CredentialConfigCertificateCertificateHash + internal struct CredentialConfigCertificateHash { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] internal byte[] ShaHash; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - internal struct CredentialConfigCertificateCertificateHashStore + internal struct CredentialConfigCertificateHashStore { internal QUIC_CERTIFICATE_HASH_STORE_FLAGS Flags; @@ -250,7 +253,7 @@ internal struct CredentialConfigCertificateCertificateHashStore } [StructLayout(LayoutKind.Sequential)] - internal struct CredentialConfigCertificateCertificateFile + internal struct CredentialConfigCertificateFile { [MarshalAs(UnmanagedType.LPUTF8Str)] internal string PrivateKeyFile; @@ -260,7 +263,7 @@ internal struct CredentialConfigCertificateCertificateFile } [StructLayout(LayoutKind.Sequential)] - internal struct CredentialConfigCertificateCertificateFileProtected + internal struct CredentialConfigCertificateFileProtected { [MarshalAs(UnmanagedType.LPUTF8Str)] internal string PrivateKeyFile; @@ -272,6 +275,16 @@ internal struct CredentialConfigCertificateCertificateFileProtected internal string PrivateKeyPassword; } + [StructLayout(LayoutKind.Sequential)] + internal struct CredentialConfigCertificatePkcs12 + { + internal IntPtr Asn1Blob; + + internal uint Asn1BlobLength; + + internal IntPtr PrivateKeyPassword; + } + [StructLayout(LayoutKind.Sequential)] internal struct ListenerEvent { @@ -407,6 +420,14 @@ internal struct ConnectionEventDataStreamsAvailable internal ushort UniDirectionalCount; } + [StructLayout(LayoutKind.Sequential)] + internal struct ConnectionEventPeerCertificateReceived + { + internal IntPtr PlatformCertificateHandle; + internal uint DeferredErrorFlags; + internal uint DeferredStatus; + } + [StructLayout(LayoutKind.Explicit)] internal struct ConnectionEventDataUnion { @@ -434,7 +455,10 @@ internal struct ConnectionEventDataUnion [FieldOffset(0)] internal ConnectionEventDataStreamsAvailable StreamsAvailable; - // TODO: missing IDEAL_PROCESSOR_CHANGED, ..., PEER_CERTIFICATE_RECEIVED (7 total) + [FieldOffset(0)] + internal ConnectionEventPeerCertificateReceived PeerCertificateReceived; + + // TODO: missing IDEAL_PROCESSOR_CHANGED, ..., (6 total) } [StructLayout(LayoutKind.Sequential)] diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusCodes.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusCodes.cs index c48fac85f98e6..50f736d429f7f 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusCodes.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusCodes.cs @@ -5,12 +5,14 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal { internal static class MsQuicStatusCodes { - internal static uint Success => OperatingSystem.IsWindows() ? Windows.Success : Linux.Success; - internal static uint Pending => OperatingSystem.IsWindows() ? Windows.Pending : Linux.Pending; - internal static uint InternalError => OperatingSystem.IsWindows() ? Windows.InternalError : Linux.InternalError; + internal static uint Success => OperatingSystem.IsWindows() ? Windows.Success : Posix.Success; + internal static uint Pending => OperatingSystem.IsWindows() ? Windows.Pending : Posix.Pending; + internal static uint InternalError => OperatingSystem.IsWindows() ? Windows.InternalError : Posix.InternalError; + internal static uint InvalidState => OperatingSystem.IsWindows() ? Windows.InvalidState : Posix.InvalidState; + internal static uint HandshakeFailure => OperatingSystem.IsWindows() ? Windows.HandshakeFailure : Posix.HandshakeFailure; // TODO return better error messages here. - public static string GetError(uint status) => OperatingSystem.IsWindows() ? Windows.GetError(status) : Linux.GetError(status); + public static string GetError(uint status) => OperatingSystem.IsWindows() ? Windows.GetError(status) : Posix.GetError(status); private static class Windows { @@ -69,7 +71,7 @@ public static string GetError(uint status) } } - private static class Linux + private static class Posix { internal const uint Success = 0; internal const uint Pending = unchecked((uint)-2); diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusHelper.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusHelper.cs index 2f64fa241582d..4f2bedb141036 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusHelper.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusHelper.cs @@ -12,7 +12,7 @@ internal static bool SuccessfulStatusCode(uint status) return status < 0x80000000; } - if (OperatingSystem.IsLinux()) + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) { return (int)status <= 0; } 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 24a5b2fa5f921..cc3d21140ec47 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 @@ -7,6 +7,7 @@ using System.Net.Security; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Threading; using static System.Net.Quic.Implementations.MsQuic.Internal.MsQuicNativeMethods; @@ -59,6 +60,18 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, throw new Exception("MaxBidirectionalStreams overflow."); } + if ((flags & QUIC_CREDENTIAL_FLAGS.CLIENT) == 0) + { + if (certificate == null) + { + throw new Exception("Server must provide certificate"); + } + } + else + { + flags |= QUIC_CREDENTIAL_FLAGS.INDICATE_CERTIFICATE_RECEIVED | QUIC_CREDENTIAL_FLAGS.NO_CERTIFICATE_VALIDATION; + } + Debug.Assert(!MsQuicApi.Api.Registration.IsInvalid); var settings = new QuicSettings @@ -99,31 +112,39 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, try { - // TODO: find out what to do for OpenSSL here -- passing handle won't work, because - // MsQuic has a private copy of OpenSSL so the SSL_CTX will be incompatible. - CredentialConfig config = default; - config.Flags = flags; // TODO: consider using LOAD_ASYNCHRONOUS with a callback. if (certificate != null) { -#if true - // If using stub TLS. - config.Type = QUIC_CREDENTIAL_TYPE.STUB_NULL; -#else - // TODO: doesn't work on non-Windows - config.Type = QUIC_CREDENTIAL_TYPE.CONTEXT; - config.Certificate = certificate.Handle; -#endif + if (OperatingSystem.IsWindows()) + { + config.Type = QUIC_CREDENTIAL_TYPE.CONTEXT; + config.Certificate = certificate.Handle; + status = MsQuicApi.Api.ConfigurationLoadCredentialDelegate(configurationHandle, ref config); + } + else + { + CredentialConfigCertificatePkcs12 pkcs12Config; + byte[] asn1 = certificate.Export(X509ContentType.Pkcs12); + fixed (void* ptr = asn1) + { + pkcs12Config.Asn1Blob = (IntPtr)ptr; + pkcs12Config.Asn1BlobLength = (uint)asn1.Length; + pkcs12Config.PrivateKeyPassword = IntPtr.Zero; + + config.Type = QUIC_CREDENTIAL_TYPE.PKCS12; + config.Certificate = (IntPtr)(&pkcs12Config); + status = MsQuicApi.Api.ConfigurationLoadCredentialDelegate(configurationHandle, ref config); + } + } } else { - // TODO: not allowed for OpenSSL and server config.Type = QUIC_CREDENTIAL_TYPE.NONE; + status = MsQuicApi.Api.ConfigurationLoadCredentialDelegate(configurationHandle, ref config); } - status = MsQuicApi.Api.ConfigurationLoadCredentialDelegate(configurationHandle, ref config); QuicExceptionHelpers.ThrowIfFailed(status, "ConfigurationLoadCredential failed."); } 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 87897c1951159..24392320c923c 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 @@ -7,6 +7,8 @@ using System.Net.Sockets; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -16,6 +18,9 @@ namespace System.Net.Quic.Implementations.MsQuic { internal sealed class MsQuicConnection : QuicConnectionProvider { + private static readonly Oid s_clientAuthOid = new Oid("1.3.6.1.5.5.7.3.2", "1.3.6.1.5.5.7.3.2"); + private static readonly Oid s_serverAuthOid = new Oid("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.1"); + // Delegate that wraps the static function that will be called when receiving an event. private static readonly ConnectionCallbackDelegate s_connectionDelegate = new ConnectionCallbackDelegate(NativeCallbackHandler); @@ -30,6 +35,10 @@ internal sealed class MsQuicConnection : QuicConnectionProvider private IPEndPoint? _localEndPoint; private readonly EndPoint _remoteEndPoint; private SslApplicationProtocol _negotiatedAlpnProtocol; + private bool _isServer; + private bool _remoteCertificateRequired; + private X509RevocationMode _revocationMode = X509RevocationMode.Offline; + private RemoteCertificateValidationCallback? _remoteCertificateValidationCallback; private sealed class State { @@ -61,6 +70,8 @@ public MsQuicConnection(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, Saf _state.Connected = true; _localEndPoint = localEndPoint; _remoteEndPoint = remoteEndPoint; + _remoteCertificateRequired = false; + _isServer = true; _stateHandle = GCHandle.Alloc(_state); @@ -83,6 +94,13 @@ public MsQuicConnection(QuicClientConnectionOptions options) { _remoteEndPoint = options.RemoteEndPoint!; _configuration = SafeMsQuicConfigurationHandle.Create(options); + _isServer = false; + _remoteCertificateRequired = true; + if (options.ClientAuthenticationOptions != null) + { + _revocationMode = options.ClientAuthenticationOptions.CertificateRevocationCheckMode; + _remoteCertificateValidationCallback = options.ClientAuthenticationOptions.RemoteCertificateValidationCallback; + } _stateHandle = GCHandle.Alloc(_state); try @@ -181,6 +199,75 @@ private static uint HandleEventStreamsAvailable(State state, ref ConnectionEvent return MsQuicStatusCodes.Success; } + private static uint HandleEventPeerCertificateReceived(State state, ref ConnectionEvent connectionEvent) + { + SslPolicyErrors sslPolicyErrors = SslPolicyErrors.None; + X509Chain? chain = null; + X509Certificate2? certificate = null; + + if (!OperatingSystem.IsWindows()) + { + // TODO fix validation with OpenSSL + return MsQuicStatusCodes.Success; + } + + MsQuicConnection? connection = state.Connection; + if (connection == null) + { + return MsQuicStatusCodes.InvalidState; + } + + if (connectionEvent.Data.PeerCertificateReceived.PlatformCertificateHandle != IntPtr.Zero) + { + certificate = new X509Certificate2(connectionEvent.Data.PeerCertificateReceived.PlatformCertificateHandle); + } + + try + { + if (certificate == null) + { + if (NetEventSource.Log.IsEnabled() && connection._remoteCertificateRequired) NetEventSource.Error(state.Connection, $"Remote certificate required, but no remote certificate received"); + sslPolicyErrors |= SslPolicyErrors.RemoteCertificateNotAvailable; + } + else + { + chain = new X509Chain(); + chain.ChainPolicy.RevocationMode = connection._revocationMode; + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; + chain.ChainPolicy.ApplicationPolicy.Add(connection._isServer ? s_clientAuthOid : s_serverAuthOid); + + if (!chain.Build(certificate)) + { + sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors; + } + } + + if (!connection._remoteCertificateRequired) + { + sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNotAvailable; + } + + if (connection._remoteCertificateValidationCallback != null) + { + bool success = connection._remoteCertificateValidationCallback(connection, certificate, chain, sslPolicyErrors); + if (!success && NetEventSource.Log.IsEnabled()) + NetEventSource.Error(state.Connection, "Remote certificate rejected by verification callback"); + return success ? MsQuicStatusCodes.Success : MsQuicStatusCodes.HandshakeFailure; + } + + if (NetEventSource.Log.IsEnabled()) + NetEventSource.Info(state.Connection, $"Certificate validation for '${certificate?.Subject}' finished with ${sslPolicyErrors}"); + + return (sslPolicyErrors == SslPolicyErrors.None) ? MsQuicStatusCodes.Success : MsQuicStatusCodes.HandshakeFailure; + } + catch (Exception ex) + { + if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(state.Connection, $"Certificate validation failed ${ex.Message}"); + } + + return MsQuicStatusCodes.InternalError; + } + internal override async ValueTask AcceptStreamAsync(CancellationToken cancellationToken = default) { ThrowIfDisposed(); @@ -312,10 +399,9 @@ private static uint NativeCallbackHandler( ref ConnectionEvent connectionEvent) { var state = (State)GCHandle.FromIntPtr(context).Target!; - try { - switch ((QUIC_CONNECTION_EVENT_TYPE)connectionEvent.Type) + switch (connectionEvent.Type) { case QUIC_CONNECTION_EVENT_TYPE.CONNECTED: return HandleEventConnected(state, ref connectionEvent); @@ -329,6 +415,8 @@ private static uint NativeCallbackHandler( return HandleEventNewStream(state, ref connectionEvent); case QUIC_CONNECTION_EVENT_TYPE.STREAMS_AVAILABLE: return HandleEventStreamsAvailable(state, ref connectionEvent); + case QUIC_CONNECTION_EVENT_TYPE.PEER_CERTIFICATE_RECEIVED: + return HandleEventPeerCertificateReceived(state, ref connectionEvent); default: return MsQuicStatusCodes.Success; } diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTestBase.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTestBase.cs index 2178ec12c46fd..d4ab76c0dcfa7 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTestBase.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTestBase.cs @@ -24,7 +24,8 @@ public SslClientAuthenticationOptions GetSslClientAuthenticationOptions() { return new SslClientAuthenticationOptions() { - ApplicationProtocols = new List() { new SslApplicationProtocol("quictest") } + ApplicationProtocols = new List() { new SslApplicationProtocol("quictest") }, + RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => { return true; } }; } diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs index 6fbcc41bdc6d5..53ddb009fa7bc 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicTestBase.cs @@ -31,7 +31,8 @@ public SslClientAuthenticationOptions GetSslClientAuthenticationOptions() { return new SslClientAuthenticationOptions() { - ApplicationProtocols = new List() { ApplicationProtocol } + ApplicationProtocols = new List() { ApplicationProtocol }, + RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => { return true; } }; }