Skip to content

Commit

Permalink
basic certificate handling for quic (#50613)
Browse files Browse the repository at this point in the history
* basic certificate handling for quic

* fix linux

* fix macOS

* feedback from review

* feedback from review
  • Loading branch information
wfurt committed Apr 9, 2021
1 parent d472365 commit 01b7e73
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 34 deletions.
5 changes: 5 additions & 0 deletions src/libraries/System.Net.Quic/src/System.Net.Quic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
<Reference Include="System.Net.Sockets" />
<Reference Include="System.Runtime" />
<Reference Include="System.Runtime.InteropServices" />
<Reference Include="System.Security.Cryptography.Encoding" />
<Reference Include="System.Security.Cryptography.X509Certificates" />
<Reference Include="System.Threading" />
<Reference Include="System.Threading.Channels" />
Expand All @@ -66,6 +67,10 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="libmsquic.dylib" Condition="Exists('libmsquic.dylib')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="libmsquic.so" Condition="Exists('libmsquic.so')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -250,7 +253,7 @@ internal struct CredentialConfigCertificateCertificateHashStore
}

[StructLayout(LayoutKind.Sequential)]
internal struct CredentialConfigCertificateCertificateFile
internal struct CredentialConfigCertificateFile
{
[MarshalAs(UnmanagedType.LPUTF8Str)]
internal string PrivateKeyFile;
Expand All @@ -260,7 +263,7 @@ internal struct CredentialConfigCertificateCertificateFile
}

[StructLayout(LayoutKind.Sequential)]
internal struct CredentialConfigCertificateCertificateFileProtected
internal struct CredentialConfigCertificateFileProtected
{
[MarshalAs(UnmanagedType.LPUTF8Str)]
internal string PrivateKeyFile;
Expand All @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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
{
Expand Down Expand Up @@ -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);

Expand All @@ -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
Expand Down Expand Up @@ -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<QuicStreamProvider> AcceptStreamAsync(CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
Expand Down Expand Up @@ -312,10 +399,9 @@ internal void SetNegotiatedAlpn(IntPtr alpn, int alpnLength)
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);
Expand All @@ -329,6 +415,8 @@ internal void SetNegotiatedAlpn(IntPtr alpn, int alpnLength)
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;
}
Expand Down
Loading

0 comments on commit 01b7e73

Please sign in to comment.