Skip to content

Commit

Permalink
support ServerCertificateContext in quic (#53175)
Browse files Browse the repository at this point in the history
  • Loading branch information
wfurt authored May 31, 2021
1 parent d3ed5a9 commit 57fc4c2
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Security;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
Expand All @@ -15,6 +16,9 @@ namespace System.Net.Quic.Implementations.MsQuic.Internal
{
internal sealed class SafeMsQuicConfigurationHandle : SafeHandle
{
private static readonly FieldInfo _contextCertificate = typeof(SslStreamCertificateContext).GetField("Certificate", BindingFlags.NonPublic | BindingFlags.Instance)!;
private static readonly FieldInfo _contextChain= typeof(SslStreamCertificateContext).GetField("IntermediateCertificates", BindingFlags.NonPublic | BindingFlags.Instance)!;

public override bool IsInvalid => handle == IntPtr.Zero;

private SafeMsQuicConfigurationHandle()
Expand All @@ -31,18 +35,18 @@ protected override bool ReleaseHandle()
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, options.ClientAuthenticationOptions?.ApplicationProtocols);
return Create(options, QUIC_CREDENTIAL_FLAGS.CLIENT, certificate: null, 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?.ApplicationProtocols);
return Create(options, QUIC_CREDENTIAL_FLAGS.NONE, 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.
// Consider moving bigger logic like this outside of constructor call chains.
private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, List<SslApplicationProtocol>? alpnProtocols)
private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options, QUIC_CREDENTIAL_FLAGS flags, X509Certificate? certificate, SslStreamCertificateContext? certificateContext, List<SslApplicationProtocol>? alpnProtocols)
{
// TODO: some of these checks should be done by the QuicOptions type.
if (alpnProtocols == null || alpnProtocols.Count == 0)
Expand All @@ -62,7 +66,7 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options,

if ((flags & QUIC_CREDENTIAL_FLAGS.CLIENT) == 0)
{
if (certificate == null)
if (certificate == null && certificateContext == null)
{
throw new Exception("Server must provide certificate");
}
Expand Down Expand Up @@ -101,6 +105,7 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options,

uint status;
SafeMsQuicConfigurationHandle? configurationHandle;
X509Certificate2[]? intermediates = null;

MemoryHandle[]? handles = null;
QuicBuffer[]? buffers = null;
Expand All @@ -121,6 +126,17 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options,
CredentialConfig config = default;
config.Flags = flags; // TODO: consider using LOAD_ASYNCHRONOUS with a callback.

if (certificateContext != null)
{
certificate = (X509Certificate2?) _contextCertificate.GetValue(certificateContext);
intermediates = (X509Certificate2[]?) _contextChain.GetValue(certificateContext);

if (certificate == null || intermediates == null)
{
throw new ArgumentException(nameof(certificateContext));
}
}

if (certificate != null)
{
if (OperatingSystem.IsWindows())
Expand All @@ -132,7 +148,24 @@ private static unsafe SafeMsQuicConfigurationHandle Create(QuicOptions options,
else
{
CredentialConfigCertificatePkcs12 pkcs12Config;
byte[] asn1 = certificate.Export(X509ContentType.Pkcs12);
byte[] asn1;

if (intermediates?.Length > 0)
{
X509Certificate2Collection collection = new X509Certificate2Collection();
collection.Add(certificate);
for (int i= 0; i < intermediates?.Length; i++)

This comment has been minimized.

Copy link
@campersau

campersau May 31, 2021

Contributor

This null check (intermediates?.Length) can be removed as it is already checked above.

{
collection.Add(intermediates[i]);
}

asn1 = collection.Export(X509ContentType.Pkcs12)!;
}
else
{
asn1 = certificate.Export(X509ContentType.Pkcs12);
}

fixed (void* ptr = asn1)
{
pkcs12Config.Asn1Blob = (IntPtr)ptr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ private static uint HandleEventPeerCertificateReceived(State state, ref Connecti
additionalCertificates.Import(asn1);
if (additionalCertificates.Count > 0)
{
certificate = additionalCertificates[0];
certificate = additionalCertificates[additionalCertificates.Count - 1];
}
}
}
Expand Down Expand Up @@ -273,7 +273,7 @@ private static uint HandleEventPeerCertificateReceived(State state, ref Connecti

if (additionalCertificates != null && additionalCertificates.Count > 1)
{
for (int i = 1; i < additionalCertificates.Count; i++)
for (int i = 0; i < additionalCertificates.Count - 1; i++)
{
chain.ChainPolicy.ExtraStore.Add(additionalCertificates[i]);
}
Expand Down
42 changes: 42 additions & 0 deletions src/libraries/System.Net.Quic/tests/FunctionalTests/MsQuicTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Xunit;
Expand Down Expand Up @@ -53,6 +55,46 @@ public async Task UnidirectionalAndBidirectionalChangeValues()
Assert.Equal(20, serverConnection.GetRemoteAvailableUnidirectionalStreamCount());
}

[Fact]
public async Task ConnectWithCertificateChain()
{
(X509Certificate2 certificate, X509Certificate2Collection chain) = System.Net.Security.Tests.TestHelper.GenerateCertificates("localhost", longChain: true);
X509Certificate2 rootCA = chain[chain.Count - 1];

var quicOptions = new QuicListenerOptions();
quicOptions.ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0);
quicOptions.ServerAuthenticationOptions = GetSslServerAuthenticationOptions();
quicOptions.ServerAuthenticationOptions.ServerCertificateContext = SslStreamCertificateContext.Create(certificate, chain);
quicOptions.ServerAuthenticationOptions.ServerCertificate = null;

using QuicListener listener = new QuicListener(QuicImplementationProviders.MsQuic, quicOptions);

QuicClientConnectionOptions options = new QuicClientConnectionOptions()
{
RemoteEndPoint = listener.ListenEndPoint,
ClientAuthenticationOptions = GetSslClientAuthenticationOptions(),
};

options.ClientAuthenticationOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) =>
{
Assert.Equal(certificate.Subject, cert.Subject);
Assert.Equal(certificate.Issuer, cert.Issuer);
// We should get full chain without root CA.
// With trusted root, we should be able to build chain.
chain.ChainPolicy.CustomTrustStore.Add(rootCA);
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
Assert.True(chain.Build(certificate));

return true;
};

using QuicConnection clientConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
ValueTask clientTask = clientConnection.ConnectAsync();

using QuicConnection serverConnection = await listener.AcceptConnectionAsync();
await clientTask;
}

[Fact]
[OuterLoop("May take several seconds")]
public async Task SetListenerTimeoutWorksWithSmallTimeout()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@
<Compile Include="*.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(CommonTestPath)System\Net\Configuration.Certificates.cs" Link="TestCommon\System\Net\Configuration.Certificates.cs" />
<Compile Include="$(CommonTestPath)System\Security\Cryptography\PlatformSupport.cs" Link="TestCommon\System\Security\Cryptography\PlatformSupport.cs" />
<Compile Include="$(CommonTestPath)System\Threading\Tasks\TaskTimeoutExtensions.cs" Link="TestCommon\System\Threading\Tasks\TaskTimeoutExtensions.cs" />
<Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
<Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
<Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
<Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs" Link="Common\System\Threading\Tasks\TaskToApm.cs" />
<Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
<Compile Include="$(CommonTestPath)System\Net\Capability.Security.cs" Link="Common\System\Net\Capability.Security.cs" />
<Compile Include="$(CommonTestPath)System\Net\Configuration.cs" Link="Common\System\Net\Configuration.cs" />
<Compile Include="$(CommonTestPath)System\Net\Configuration.Certificates.cs" Link="TestCommon\System\Net\Configuration.Certificates.cs" />
<Compile Include="$(CommonTestPath)System\Net\Configuration.Http.cs" Link="Common\System\Net\Configuration.Http.cs" />
<Compile Include="$(CommonTestPath)System\Net\Configuration.Security.cs" Link="Common\System\Net\Configuration.Security.cs" />
<Compile Include="$(CommonTestPath)System\Security\Cryptography\PlatformSupport.cs" Link="TestCommon\System\Security\Cryptography\PlatformSupport.cs" />
<Compile Include="$(CommonTestPath)System\Security\Cryptography\X509Certificates\CertificateAuthority.cs" Link="CommonTest\System\Security\Cryptography\X509Certificates\CertificateAuthority.cs" />
<Compile Include="$(CommonTestPath)System\Security\Cryptography\X509Certificates\RevocationResponder.cs" Link="CommonTest\System\Security\Cryptography\X509Certificates\RevocationResponder.cs" />
<Compile Include="$(CommonTestPath)System\Threading\Tasks\TaskTimeoutExtensions.cs" Link="TestCommon\System\Threading\Tasks\TaskTimeoutExtensions.cs" />
<Compile Include="..\..\..\System.Net.Security\tests\FunctionalTests\TestHelper.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(CommonTestPath)StreamConformanceTests\StreamConformanceTests.csproj" />
Expand Down

0 comments on commit 57fc4c2

Please sign in to comment.