Skip to content

Commit 01b7e73

Browse files
authored
basic certificate handling for quic (#50613)
* basic certificate handling for quic * fix linux * fix macOS * feedback from review * feedback from review
1 parent d472365 commit 01b7e73

File tree

9 files changed

+176
-34
lines changed

9 files changed

+176
-34
lines changed

src/libraries/System.Net.Quic/src/System.Net.Quic.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<Reference Include="System.Net.Sockets" />
5353
<Reference Include="System.Runtime" />
5454
<Reference Include="System.Runtime.InteropServices" />
55+
<Reference Include="System.Security.Cryptography.Encoding" />
5556
<Reference Include="System.Security.Cryptography.X509Certificates" />
5657
<Reference Include="System.Threading" />
5758
<Reference Include="System.Threading.Channels" />
@@ -66,6 +67,10 @@
6667
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6768
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
6869
</Content>
70+
<Content Include="libmsquic.dylib" Condition="Exists('libmsquic.dylib')">
71+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
72+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
73+
</Content>
6974
<Content Include="libmsquic.so" Condition="Exists('libmsquic.so')">
7075
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
7176
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>

src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicEnums.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal enum QUIC_CREDENTIAL_TYPE : uint
1919
CONTEXT,
2020
FILE,
2121
FILE_PROTECTED,
22-
STUB_NULL = 0xF0000000, // Pass as server cert to stubtls implementation.
22+
PKCS12,
2323
}
2424

2525
[Flags]

src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicNativeMethods.cs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -215,30 +215,33 @@ internal struct CredentialConfig
215215
internal struct CredentialConfigCertificateUnion
216216
{
217217
[FieldOffset(0)]
218-
internal CredentialConfigCertificateCertificateHash CertificateHash;
218+
internal CredentialConfigCertificateHash CertificateHash;
219219

220220
[FieldOffset(0)]
221-
internal CredentialConfigCertificateCertificateHashStore CertificateHashStore;
221+
internal CredentialConfigCertificateHashStore CertificateHashStore;
222222

223223
[FieldOffset(0)]
224224
internal IntPtr CertificateContext;
225225

226226
[FieldOffset(0)]
227-
internal CredentialConfigCertificateCertificateFile CertificateFile;
227+
internal CredentialConfigCertificateFile CertificateFile;
228228

229229
[FieldOffset(0)]
230-
internal CredentialConfigCertificateCertificateFileProtected CertificateFileProtected;
230+
internal CredentialConfigCertificateFileProtected CertificateFileProtected;
231+
232+
[FieldOffset(0)]
233+
internal CredentialConfigCertificatePkcs12 CertificatePkcs12;
231234
}
232235

233236
[StructLayout(LayoutKind.Sequential)]
234-
internal struct CredentialConfigCertificateCertificateHash
237+
internal struct CredentialConfigCertificateHash
235238
{
236239
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
237240
internal byte[] ShaHash;
238241
}
239242

240243
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
241-
internal struct CredentialConfigCertificateCertificateHashStore
244+
internal struct CredentialConfigCertificateHashStore
242245
{
243246
internal QUIC_CERTIFICATE_HASH_STORE_FLAGS Flags;
244247

@@ -250,7 +253,7 @@ internal struct CredentialConfigCertificateCertificateHashStore
250253
}
251254

252255
[StructLayout(LayoutKind.Sequential)]
253-
internal struct CredentialConfigCertificateCertificateFile
256+
internal struct CredentialConfigCertificateFile
254257
{
255258
[MarshalAs(UnmanagedType.LPUTF8Str)]
256259
internal string PrivateKeyFile;
@@ -260,7 +263,7 @@ internal struct CredentialConfigCertificateCertificateFile
260263
}
261264

262265
[StructLayout(LayoutKind.Sequential)]
263-
internal struct CredentialConfigCertificateCertificateFileProtected
266+
internal struct CredentialConfigCertificateFileProtected
264267
{
265268
[MarshalAs(UnmanagedType.LPUTF8Str)]
266269
internal string PrivateKeyFile;
@@ -272,6 +275,16 @@ internal struct CredentialConfigCertificateCertificateFileProtected
272275
internal string PrivateKeyPassword;
273276
}
274277

278+
[StructLayout(LayoutKind.Sequential)]
279+
internal struct CredentialConfigCertificatePkcs12
280+
{
281+
internal IntPtr Asn1Blob;
282+
283+
internal uint Asn1BlobLength;
284+
285+
internal IntPtr PrivateKeyPassword;
286+
}
287+
275288
[StructLayout(LayoutKind.Sequential)]
276289
internal struct ListenerEvent
277290
{
@@ -407,6 +420,14 @@ internal struct ConnectionEventDataStreamsAvailable
407420
internal ushort UniDirectionalCount;
408421
}
409422

423+
[StructLayout(LayoutKind.Sequential)]
424+
internal struct ConnectionEventPeerCertificateReceived
425+
{
426+
internal IntPtr PlatformCertificateHandle;
427+
internal uint DeferredErrorFlags;
428+
internal uint DeferredStatus;
429+
}
430+
410431
[StructLayout(LayoutKind.Explicit)]
411432
internal struct ConnectionEventDataUnion
412433
{
@@ -434,7 +455,10 @@ internal struct ConnectionEventDataUnion
434455
[FieldOffset(0)]
435456
internal ConnectionEventDataStreamsAvailable StreamsAvailable;
436457

437-
// TODO: missing IDEAL_PROCESSOR_CHANGED, ..., PEER_CERTIFICATE_RECEIVED (7 total)
458+
[FieldOffset(0)]
459+
internal ConnectionEventPeerCertificateReceived PeerCertificateReceived;
460+
461+
// TODO: missing IDEAL_PROCESSOR_CHANGED, ..., (6 total)
438462
}
439463

440464
[StructLayout(LayoutKind.Sequential)]

src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusCodes.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
55
{
66
internal static class MsQuicStatusCodes
77
{
8-
internal static uint Success => OperatingSystem.IsWindows() ? Windows.Success : Linux.Success;
9-
internal static uint Pending => OperatingSystem.IsWindows() ? Windows.Pending : Linux.Pending;
10-
internal static uint InternalError => OperatingSystem.IsWindows() ? Windows.InternalError : Linux.InternalError;
8+
internal static uint Success => OperatingSystem.IsWindows() ? Windows.Success : Posix.Success;
9+
internal static uint Pending => OperatingSystem.IsWindows() ? Windows.Pending : Posix.Pending;
10+
internal static uint InternalError => OperatingSystem.IsWindows() ? Windows.InternalError : Posix.InternalError;
11+
internal static uint InvalidState => OperatingSystem.IsWindows() ? Windows.InvalidState : Posix.InvalidState;
12+
internal static uint HandshakeFailure => OperatingSystem.IsWindows() ? Windows.HandshakeFailure : Posix.HandshakeFailure;
1113

1214
// TODO return better error messages here.
13-
public static string GetError(uint status) => OperatingSystem.IsWindows() ? Windows.GetError(status) : Linux.GetError(status);
15+
public static string GetError(uint status) => OperatingSystem.IsWindows() ? Windows.GetError(status) : Posix.GetError(status);
1416

1517
private static class Windows
1618
{
@@ -69,7 +71,7 @@ public static string GetError(uint status)
6971
}
7072
}
7173

72-
private static class Linux
74+
private static class Posix
7375
{
7476
internal const uint Success = 0;
7577
internal const uint Pending = unchecked((uint)-2);

src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/MsQuicStatusHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ internal static bool SuccessfulStatusCode(uint status)
1212
return status < 0x80000000;
1313
}
1414

15-
if (OperatingSystem.IsLinux())
15+
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
1616
{
1717
return (int)status <= 0;
1818
}

src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/Interop/SafeMsQuicConfigurationHandle.cs

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Net.Security;
88
using System.Runtime.InteropServices;
99
using System.Security.Cryptography.X509Certificates;
10+
using System.Text;
1011
using System.Threading;
1112
using static System.Net.Quic.Implementations.MsQuic.Internal.MsQuicNativeMethods;
1213

@@ -59,6 +60,18 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options,
5960
throw new Exception("MaxBidirectionalStreams overflow.");
6061
}
6162

63+
if ((flags & QUIC_CREDENTIAL_FLAGS.CLIENT) == 0)
64+
{
65+
if (certificate == null)
66+
{
67+
throw new Exception("Server must provide certificate");
68+
}
69+
}
70+
else
71+
{
72+
flags |= QUIC_CREDENTIAL_FLAGS.INDICATE_CERTIFICATE_RECEIVED | QUIC_CREDENTIAL_FLAGS.NO_CERTIFICATE_VALIDATION;
73+
}
74+
6275
Debug.Assert(!MsQuicApi.Api.Registration.IsInvalid);
6376

6477
var settings = new QuicSettings
@@ -99,31 +112,39 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options,
99112

100113
try
101114
{
102-
// TODO: find out what to do for OpenSSL here -- passing handle won't work, because
103-
// MsQuic has a private copy of OpenSSL so the SSL_CTX will be incompatible.
104-
105115
CredentialConfig config = default;
106-
107116
config.Flags = flags; // TODO: consider using LOAD_ASYNCHRONOUS with a callback.
108117

109118
if (certificate != null)
110119
{
111-
#if true
112-
// If using stub TLS.
113-
config.Type = QUIC_CREDENTIAL_TYPE.STUB_NULL;
114-
#else
115-
// TODO: doesn't work on non-Windows
116-
config.Type = QUIC_CREDENTIAL_TYPE.CONTEXT;
117-
config.Certificate = certificate.Handle;
118-
#endif
120+
if (OperatingSystem.IsWindows())
121+
{
122+
config.Type = QUIC_CREDENTIAL_TYPE.CONTEXT;
123+
config.Certificate = certificate.Handle;
124+
status = MsQuicApi.Api.ConfigurationLoadCredentialDelegate(configurationHandle, ref config);
125+
}
126+
else
127+
{
128+
CredentialConfigCertificatePkcs12 pkcs12Config;
129+
byte[] asn1 = certificate.Export(X509ContentType.Pkcs12);
130+
fixed (void* ptr = asn1)
131+
{
132+
pkcs12Config.Asn1Blob = (IntPtr)ptr;
133+
pkcs12Config.Asn1BlobLength = (uint)asn1.Length;
134+
pkcs12Config.PrivateKeyPassword = IntPtr.Zero;
135+
136+
config.Type = QUIC_CREDENTIAL_TYPE.PKCS12;
137+
config.Certificate = (IntPtr)(&pkcs12Config);
138+
status = MsQuicApi.Api.ConfigurationLoadCredentialDelegate(configurationHandle, ref config);
139+
}
140+
}
119141
}
120142
else
121143
{
122-
// TODO: not allowed for OpenSSL and server
123144
config.Type = QUIC_CREDENTIAL_TYPE.NONE;
145+
status = MsQuicApi.Api.ConfigurationLoadCredentialDelegate(configurationHandle, ref config);
124146
}
125147

126-
status = MsQuicApi.Api.ConfigurationLoadCredentialDelegate(configurationHandle, ref config);
127148
QuicExceptionHelpers.ThrowIfFailed(status, "ConfigurationLoadCredential failed.");
128149
}
129150
catch

src/libraries/System.Net.Quic/src/System/Net/Quic/Implementations/MsQuic/MsQuicConnection.cs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using System.Net.Sockets;
88
using System.Runtime.ExceptionServices;
99
using System.Runtime.InteropServices;
10+
using System.Security.Cryptography;
11+
using System.Security.Cryptography.X509Certificates;
1012
using System.Threading;
1113
using System.Threading.Channels;
1214
using System.Threading.Tasks;
@@ -16,6 +18,9 @@ namespace System.Net.Quic.Implementations.MsQuic
1618
{
1719
internal sealed class MsQuicConnection : QuicConnectionProvider
1820
{
21+
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");
22+
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");
23+
1924
// Delegate that wraps the static function that will be called when receiving an event.
2025
private static readonly ConnectionCallbackDelegate s_connectionDelegate = new ConnectionCallbackDelegate(NativeCallbackHandler);
2126

@@ -30,6 +35,10 @@ internal sealed class MsQuicConnection : QuicConnectionProvider
3035
private IPEndPoint? _localEndPoint;
3136
private readonly EndPoint _remoteEndPoint;
3237
private SslApplicationProtocol _negotiatedAlpnProtocol;
38+
private bool _isServer;
39+
private bool _remoteCertificateRequired;
40+
private X509RevocationMode _revocationMode = X509RevocationMode.Offline;
41+
private RemoteCertificateValidationCallback? _remoteCertificateValidationCallback;
3342

3443
private sealed class State
3544
{
@@ -61,6 +70,8 @@ public MsQuicConnection(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, Saf
6170
_state.Connected = true;
6271
_localEndPoint = localEndPoint;
6372
_remoteEndPoint = remoteEndPoint;
73+
_remoteCertificateRequired = false;
74+
_isServer = true;
6475

6576
_stateHandle = GCHandle.Alloc(_state);
6677

@@ -83,6 +94,13 @@ public MsQuicConnection(QuicClientConnectionOptions options)
8394
{
8495
_remoteEndPoint = options.RemoteEndPoint!;
8596
_configuration = SafeMsQuicConfigurationHandle.Create(options);
97+
_isServer = false;
98+
_remoteCertificateRequired = true;
99+
if (options.ClientAuthenticationOptions != null)
100+
{
101+
_revocationMode = options.ClientAuthenticationOptions.CertificateRevocationCheckMode;
102+
_remoteCertificateValidationCallback = options.ClientAuthenticationOptions.RemoteCertificateValidationCallback;
103+
}
86104

87105
_stateHandle = GCHandle.Alloc(_state);
88106
try
@@ -181,6 +199,75 @@ private static uint HandleEventStreamsAvailable(State state, ref ConnectionEvent
181199
return MsQuicStatusCodes.Success;
182200
}
183201

202+
private static uint HandleEventPeerCertificateReceived(State state, ref ConnectionEvent connectionEvent)
203+
{
204+
SslPolicyErrors sslPolicyErrors = SslPolicyErrors.None;
205+
X509Chain? chain = null;
206+
X509Certificate2? certificate = null;
207+
208+
if (!OperatingSystem.IsWindows())
209+
{
210+
// TODO fix validation with OpenSSL
211+
return MsQuicStatusCodes.Success;
212+
}
213+
214+
MsQuicConnection? connection = state.Connection;
215+
if (connection == null)
216+
{
217+
return MsQuicStatusCodes.InvalidState;
218+
}
219+
220+
if (connectionEvent.Data.PeerCertificateReceived.PlatformCertificateHandle != IntPtr.Zero)
221+
{
222+
certificate = new X509Certificate2(connectionEvent.Data.PeerCertificateReceived.PlatformCertificateHandle);
223+
}
224+
225+
try
226+
{
227+
if (certificate == null)
228+
{
229+
if (NetEventSource.Log.IsEnabled() && connection._remoteCertificateRequired) NetEventSource.Error(state.Connection, $"Remote certificate required, but no remote certificate received");
230+
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateNotAvailable;
231+
}
232+
else
233+
{
234+
chain = new X509Chain();
235+
chain.ChainPolicy.RevocationMode = connection._revocationMode;
236+
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
237+
chain.ChainPolicy.ApplicationPolicy.Add(connection._isServer ? s_clientAuthOid : s_serverAuthOid);
238+
239+
if (!chain.Build(certificate))
240+
{
241+
sslPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors;
242+
}
243+
}
244+
245+
if (!connection._remoteCertificateRequired)
246+
{
247+
sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNotAvailable;
248+
}
249+
250+
if (connection._remoteCertificateValidationCallback != null)
251+
{
252+
bool success = connection._remoteCertificateValidationCallback(connection, certificate, chain, sslPolicyErrors);
253+
if (!success && NetEventSource.Log.IsEnabled())
254+
NetEventSource.Error(state.Connection, "Remote certificate rejected by verification callback");
255+
return success ? MsQuicStatusCodes.Success : MsQuicStatusCodes.HandshakeFailure;
256+
}
257+
258+
if (NetEventSource.Log.IsEnabled())
259+
NetEventSource.Info(state.Connection, $"Certificate validation for '${certificate?.Subject}' finished with ${sslPolicyErrors}");
260+
261+
return (sslPolicyErrors == SslPolicyErrors.None) ? MsQuicStatusCodes.Success : MsQuicStatusCodes.HandshakeFailure;
262+
}
263+
catch (Exception ex)
264+
{
265+
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(state.Connection, $"Certificate validation failed ${ex.Message}");
266+
}
267+
268+
return MsQuicStatusCodes.InternalError;
269+
}
270+
184271
internal override async ValueTask<QuicStreamProvider> AcceptStreamAsync(CancellationToken cancellationToken = default)
185272
{
186273
ThrowIfDisposed();
@@ -312,10 +399,9 @@ private static uint NativeCallbackHandler(
312399
ref ConnectionEvent connectionEvent)
313400
{
314401
var state = (State)GCHandle.FromIntPtr(context).Target!;
315-
316402
try
317403
{
318-
switch ((QUIC_CONNECTION_EVENT_TYPE)connectionEvent.Type)
404+
switch (connectionEvent.Type)
319405
{
320406
case QUIC_CONNECTION_EVENT_TYPE.CONNECTED:
321407
return HandleEventConnected(state, ref connectionEvent);
@@ -329,6 +415,8 @@ private static uint NativeCallbackHandler(
329415
return HandleEventNewStream(state, ref connectionEvent);
330416
case QUIC_CONNECTION_EVENT_TYPE.STREAMS_AVAILABLE:
331417
return HandleEventStreamsAvailable(state, ref connectionEvent);
418+
case QUIC_CONNECTION_EVENT_TYPE.PEER_CERTIFICATE_RECEIVED:
419+
return HandleEventPeerCertificateReceived(state, ref connectionEvent);
332420
default:
333421
return MsQuicStatusCodes.Success;
334422
}

0 commit comments

Comments
 (0)