Skip to content

Http/3 client certificates #35308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@

#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
Expand Down Expand Up @@ -58,32 +52,11 @@ public async Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDe

var features = new FeatureCollection();

// This should always be set in production, but it's not set for InMemory tests.
// The transport will check if the feature is missing.
if (listenOptions.HttpsOptions != null)
{
// TODO Set other relevant values on options
var sslServerAuthenticationOptions = new SslServerAuthenticationOptions
{
ServerCertificate = listenOptions.HttpsOptions.ServerCertificate,
ApplicationProtocols = new List<SslApplicationProtocol>() { new SslApplicationProtocol("h3"), new SslApplicationProtocol("h3-29") }
};

if (listenOptions.HttpsOptions.ServerCertificateSelector != null)
{
// We can't set both
sslServerAuthenticationOptions.ServerCertificate = null;
sslServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, host) =>
{
// There is no ConnectionContext available durring the QUIC handshake.
var cert = listenOptions.HttpsOptions.ServerCertificateSelector(null, host);
if (cert != null)
{
HttpsConnectionMiddleware.EnsureCertificateIsAllowedForServerAuth(cert);
}
return cert!;
};
}

features.Set(sslServerAuthenticationOptions);
features.Set(HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions));
}

var transport = await _multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,47 @@ private static bool IsWindowsVersionIncompatibleWithHttp2()

return false;
}

internal static SslServerAuthenticationOptions CreateHttp3Options(HttpsConnectionAdapterOptions httpsOptions)
{
// TODO Set other relevant values on options
var sslServerAuthenticationOptions = new SslServerAuthenticationOptions
{
ServerCertificate = httpsOptions.ServerCertificate,
ApplicationProtocols = new List<SslApplicationProtocol>() { new SslApplicationProtocol("h3"), new SslApplicationProtocol("h3-29") },
CertificateRevocationCheckMode = httpsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
};

if (httpsOptions.ServerCertificateSelector != null)
{
// We can't set both
sslServerAuthenticationOptions.ServerCertificate = null;
sslServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, host) =>
{
// There is no ConnectionContext available durring the QUIC handshake.
var cert = httpsOptions.ServerCertificateSelector(null, host);
if (cert != null)
{
EnsureCertificateIsAllowedForServerAuth(cert);
}
return cert!;
};
}

// DelayCertificate is prohibited by the HTTP/2 and HTTP/3 protocols, ignore it here.
if (httpsOptions.ClientCertificateMode == ClientCertificateMode.AllowCertificate
|| httpsOptions.ClientCertificateMode == ClientCertificateMode.RequireCertificate)
{
sslServerAuthenticationOptions.ClientCertificateRequired = true; // We have to set this to prompt the client for a cert.
// For AllowCertificate we override the missing cert error in RemoteCertificateValidationCallback,
// except QuicListener doesn't call the callback for missing certs https://github.com/dotnet/runtime/issues/57308.
sslServerAuthenticationOptions.RemoteCertificateValidationCallback
= (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) =>
RemoteCertificateValidationCallback(httpsOptions.ClientCertificateMode, httpsOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
}

return sslServerAuthenticationOptions;
}
}

internal static partial class HttpsConnectionMiddlewareLoggerExtensions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Buffers;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Quic;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Connections.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal;
using Microsoft.AspNetCore.Testing;
Expand Down Expand Up @@ -649,46 +644,6 @@ public async Task PersistentState_StreamsReused_StatePersisted()
Assert.Equal(true, state);
}

[ConditionalFact]
[MsQuicSupported]
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
public async Task TlsConnectionFeature_ClientSendsCertificate_PopulatedOnFeature()
{
// Arrange
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, clientCertificateRequired: true);

var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
var testCert = TestResources.GetTestCertificate();
options.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection { testCert };

// Act
using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
await quicConnection.ConnectAsync().DefaultTimeout();

var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
// Server waits for stream from client
var serverStreamTask = serverConnection.AcceptAsync().DefaultTimeout();

// Client creates stream
using var clientStream = quicConnection.OpenBidirectionalStream();
await clientStream.WriteAsync(TestData).DefaultTimeout();

// Server finishes accepting
var serverStream = await serverStreamTask.DefaultTimeout();

// Assert
AssertTlsConnectionFeature(serverConnection.Features, testCert);
AssertTlsConnectionFeature(serverStream.Features, testCert);

static void AssertTlsConnectionFeature(IFeatureCollection features, X509Certificate2 testCert)
{
var tlsFeature = features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.NotNull(tlsFeature.ClientCertificate);
Assert.Equal(testCert, tlsFeature.ClientCertificate);
}
}

private record RequestState(
QuicConnection QuicConnection,
MultiplexedConnectionContext ServerConnection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Net;
using System.Net.Quic;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
using Microsoft.AspNetCore.Testing;
using Xunit;
Expand All @@ -13,6 +18,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/35070")]
public class QuicConnectionListenerTests : TestApplicationErrorLoggerLoggedTest
{
private static readonly byte[] TestData = Encoding.UTF8.GetBytes("Hello world");

[ConditionalFact]
[MsQuicSupported]
public async Task AcceptAsync_AfterUnbind_Error()
Expand Down Expand Up @@ -51,5 +58,66 @@ public async Task AcceptAsync_ClientCreatesConnection_ServerAccepts()
// ConnectionClosed isn't triggered because the server initiated close.
Assert.False(serverConnection.ConnectionClosed.IsCancellationRequested);
}

[ConditionalFact]
[MsQuicSupported]
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
public async Task ClientCertificate_Required_Sent_Populated()
{
// Arrange
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, clientCertificateRequired: true);

var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
var testCert = TestResources.GetTestCertificate();
options.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection { testCert };

// Act
using var quicConnection = new QuicConnection(options);
await quicConnection.ConnectAsync().DefaultTimeout();

var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
// Server waits for stream from client
var serverStreamTask = serverConnection.AcceptAsync().DefaultTimeout();

// Client creates stream
using var clientStream = quicConnection.OpenBidirectionalStream();
await clientStream.WriteAsync(TestData).DefaultTimeout();

// Server finishes accepting
var serverStream = await serverStreamTask.DefaultTimeout();

// Assert
AssertTlsConnectionFeature(serverConnection.Features, testCert);
AssertTlsConnectionFeature(serverStream.Features, testCert);

static void AssertTlsConnectionFeature(IFeatureCollection features, X509Certificate2 testCert)
{
var tlsFeature = features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.NotNull(tlsFeature.ClientCertificate);
Assert.Equal(testCert, tlsFeature.ClientCertificate);
}
}

[ConditionalFact]
[MsQuicSupported]
// https://github.com/dotnet/runtime/issues/57308, RemoteCertificateValidationCallback should allow us to accept a null cert,
// but it doesn't right now.
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
public async Task ClientCertificate_Required_NotSent_ConnectionAborted()
{
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, clientCertificateRequired: true);

var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
using var clientConnection = new QuicConnection(options);

var qex = await Assert.ThrowsAsync<QuicException>(async () => await clientConnection.ConnectAsync().DefaultTimeout());
Assert.Equal("Connection has been shutdown by transport. Error Code: 0x80410100", qex.Message);

// https://github.com/dotnet/runtime/issues/57246 The accept still completes even though the connection was rejected, but it's already failed.
var serverContext = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
qex = await Assert.ThrowsAsync<QuicException>(() => serverContext.ConnectAsync().DefaultTimeout());
Assert.Equal("Failed to open stream to peer. Error Code: INVALID_STATE", qex.Message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public async Task BindAsync_NoServerCertificate_Error()
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => quicTransportFactory.BindAsync(new IPEndPoint(0, 0), features: features, cancellationToken: CancellationToken.None).AsTask()).DefaultTimeout();

// Assert
Assert.Equal("SslServerAuthenticationOptions.ServerCertificate must be configured with a value.", ex.Message);
Assert.Equal("SslServerAuthenticationOptions must provide a server certificate using ServerCertificate, ServerCertificateContext, or ServerCertificateSelectionCallback.", ex.Message);
}
}
}
2 changes: 2 additions & 0 deletions src/Servers/Kestrel/samples/Http3SampleApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public static void Main(string[] args)
options.ConfigureHttpsDefaults(httpsOptions =>
{
httpsOptions.ServerCertificate = cert;
// httpsOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
// httpsOptions.AllowAnyClientCertificate();
Comment on lines +32 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left them intentionally for anyone working with client certs.

});

options.ListenAnyIP(5000, listenOptions =>
Expand Down
2 changes: 1 addition & 1 deletion src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void Configure(IApplicationBuilder app)
var length = await context.Request.Body.ReadAsync(memory);
context.Response.Headers["test"] = "foo";
// for testing
await context.Response.WriteAsync("Hello World! " + context.Request.Protocol);
await context.Response.WriteAsync($"Hello World! {context.Request.Protocol} {context.Connection.ClientCertificate?.Subject}");
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@
<RuntimeHostConfigurationOption Include="System.Net.SocketsHttpHandler.Http3Support" Value="true" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(KestrelSharedSourceRoot)test\TestResources.cs" LinkBase="shared" />
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
16 changes: 13 additions & 3 deletions src/Servers/Kestrel/samples/HttpClientApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@

using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Testing;

// Console.WriteLine("Ready");
// Console.ReadKey();
Comment on lines +9 to +10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meant to still be here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, these are useful if you try to debug the Http3SampleApp. You either have to start the client first and then debug the server, or start both with debugging enabled on the server, and this pause lets you wait until the server is ready.


var handler = new SocketsHttpHandler();
handler.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true;
handler.SslOptions.ClientCertificates = new X509CertificateCollection(new[] { TestResources.GetTestCertificate("eku.client.pfx") });

using var client = new HttpClient(handler);
client.DefaultRequestVersion = HttpVersion.Version20;
client.DefaultRequestVersion =
HttpVersion.Version20;
// HttpVersion.Version30;
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;

var response = await client.GetAsync("https://localhost:5001");
var response = await client.GetAsync("https://localhost:5003");
Console.WriteLine(response);
Console.WriteLine(await response.Content.ReadAsStringAsync());

// Alt-svc enables an upgrade after the first request.
response = await client.GetAsync("https://localhost:5001");
response = await client.GetAsync("https://localhost:5003");
Console.WriteLine(response);
Console.WriteLine(await response.Content.ReadAsStringAsync());
8 changes: 2 additions & 6 deletions src/Servers/Kestrel/shared/test/TestResources.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using Xunit;

namespace Microsoft.AspNetCore.Testing
{
Expand All @@ -25,9 +21,9 @@ public static X509Certificate2 GetTestCertificate(string certName = "testCert.pf
{
// On Windows, applications should not import PFX files in parallel to avoid a known system-level
// race condition bug in native code which can cause crashes/corruption of the certificate state.
if (importPfxMutex != null)
if (importPfxMutex != null && !importPfxMutex.WaitOne(MutexTimeout))
{
Assert.True(importPfxMutex.WaitOne(MutexTimeout), "Cannot acquire the global certificate mutex.");
throw new InvalidOperationException("Cannot acquire the global certificate mutex.");
}

try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
Expand All @@ -17,13 +19,14 @@ namespace Interop.FunctionalTests.Http3
{
public static class Http3Helpers
{
public static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null)
public static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null, bool includeClientCert = false)
{
var handler = new SocketsHttpHandler();
handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (_, __, ___, ____) => true,
TargetHost = "targethost"
TargetHost = "targethost",
ClientCertificates = !includeClientCert ? null : new X509CertificateCollection() { TestResources.GetTestCertificate() },
};
if (idleTimeout != null)
{
Expand Down
Loading