Skip to content

Commit 5dc3a5c

Browse files
authored
Http/3 client certificates #34858 (#35308)
1 parent ad3ab1e commit 5dc3a5c

File tree

11 files changed

+308
-88
lines changed

11 files changed

+308
-88
lines changed

src/Servers/Kestrel/Core/src/Internal/Infrastructure/TransportManager.cs

+3-30
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@
33

44
#nullable enable
55

6-
using System;
7-
using System.Collections.Generic;
8-
using System.Linq;
96
using System.Net;
10-
using System.Net.Security;
11-
using System.Threading;
12-
using System.Threading.Tasks;
137
using Microsoft.AspNetCore.Connections;
148
using Microsoft.AspNetCore.Http.Features;
159
using Microsoft.AspNetCore.Server.Kestrel.Https.Internal;
@@ -58,32 +52,11 @@ public async Task<EndPoint> BindAsync(EndPoint endPoint, MultiplexedConnectionDe
5852

5953
var features = new FeatureCollection();
6054

55+
// This should always be set in production, but it's not set for InMemory tests.
56+
// The transport will check if the feature is missing.
6157
if (listenOptions.HttpsOptions != null)
6258
{
63-
// TODO Set other relevant values on options
64-
var sslServerAuthenticationOptions = new SslServerAuthenticationOptions
65-
{
66-
ServerCertificate = listenOptions.HttpsOptions.ServerCertificate,
67-
ApplicationProtocols = new List<SslApplicationProtocol>() { new SslApplicationProtocol("h3"), new SslApplicationProtocol("h3-29") }
68-
};
69-
70-
if (listenOptions.HttpsOptions.ServerCertificateSelector != null)
71-
{
72-
// We can't set both
73-
sslServerAuthenticationOptions.ServerCertificate = null;
74-
sslServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, host) =>
75-
{
76-
// There is no ConnectionContext available durring the QUIC handshake.
77-
var cert = listenOptions.HttpsOptions.ServerCertificateSelector(null, host);
78-
if (cert != null)
79-
{
80-
HttpsConnectionMiddleware.EnsureCertificateIsAllowedForServerAuth(cert);
81-
}
82-
return cert!;
83-
};
84-
}
85-
86-
features.Set(sslServerAuthenticationOptions);
59+
features.Set(HttpsConnectionMiddleware.CreateHttp3Options(listenOptions.HttpsOptions));
8760
}
8861

8962
var transport = await _multiplexedTransportFactory.BindAsync(endPoint, features, cancellationToken).ConfigureAwait(false);

src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

+41
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,47 @@ private static bool IsWindowsVersionIncompatibleWithHttp2()
507507

508508
return false;
509509
}
510+
511+
internal static SslServerAuthenticationOptions CreateHttp3Options(HttpsConnectionAdapterOptions httpsOptions)
512+
{
513+
// TODO Set other relevant values on options
514+
var sslServerAuthenticationOptions = new SslServerAuthenticationOptions
515+
{
516+
ServerCertificate = httpsOptions.ServerCertificate,
517+
ApplicationProtocols = new List<SslApplicationProtocol>() { new SslApplicationProtocol("h3"), new SslApplicationProtocol("h3-29") },
518+
CertificateRevocationCheckMode = httpsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
519+
};
520+
521+
if (httpsOptions.ServerCertificateSelector != null)
522+
{
523+
// We can't set both
524+
sslServerAuthenticationOptions.ServerCertificate = null;
525+
sslServerAuthenticationOptions.ServerCertificateSelectionCallback = (sender, host) =>
526+
{
527+
// There is no ConnectionContext available durring the QUIC handshake.
528+
var cert = httpsOptions.ServerCertificateSelector(null, host);
529+
if (cert != null)
530+
{
531+
EnsureCertificateIsAllowedForServerAuth(cert);
532+
}
533+
return cert!;
534+
};
535+
}
536+
537+
// DelayCertificate is prohibited by the HTTP/2 and HTTP/3 protocols, ignore it here.
538+
if (httpsOptions.ClientCertificateMode == ClientCertificateMode.AllowCertificate
539+
|| httpsOptions.ClientCertificateMode == ClientCertificateMode.RequireCertificate)
540+
{
541+
sslServerAuthenticationOptions.ClientCertificateRequired = true; // We have to set this to prompt the client for a cert.
542+
// For AllowCertificate we override the missing cert error in RemoteCertificateValidationCallback,
543+
// except QuicListener doesn't call the callback for missing certs https://github.com/dotnet/runtime/issues/57308.
544+
sslServerAuthenticationOptions.RemoteCertificateValidationCallback
545+
= (object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) =>
546+
RemoteCertificateValidationCallback(httpsOptions.ClientCertificateMode, httpsOptions.ClientCertificateValidation, certificate, chain, sslPolicyErrors);
547+
}
548+
549+
return sslServerAuthenticationOptions;
550+
}
510551
}
511552

512553
internal static partial class HttpsConnectionMiddlewareLoggerExtensions

src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs

-45
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
54
using System.Buffers;
6-
using System.Collections.Generic;
75
using System.Net.Http;
86
using System.Net.Quic;
9-
using System.Security.Cryptography.X509Certificates;
107
using System.Text;
11-
using System.Threading.Tasks;
128
using Microsoft.AspNetCore.Connections;
139
using Microsoft.AspNetCore.Connections.Features;
14-
using Microsoft.AspNetCore.Http.Features;
1510
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
1611
using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal;
1712
using Microsoft.AspNetCore.Testing;
@@ -649,46 +644,6 @@ public async Task PersistentState_StreamsReused_StatePersisted()
649644
Assert.Equal(true, state);
650645
}
651646

652-
[ConditionalFact]
653-
[MsQuicSupported]
654-
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
655-
public async Task TlsConnectionFeature_ClientSendsCertificate_PopulatedOnFeature()
656-
{
657-
// Arrange
658-
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, clientCertificateRequired: true);
659-
660-
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
661-
var testCert = TestResources.GetTestCertificate();
662-
options.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection { testCert };
663-
664-
// Act
665-
using var quicConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
666-
await quicConnection.ConnectAsync().DefaultTimeout();
667-
668-
var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
669-
// Server waits for stream from client
670-
var serverStreamTask = serverConnection.AcceptAsync().DefaultTimeout();
671-
672-
// Client creates stream
673-
using var clientStream = quicConnection.OpenBidirectionalStream();
674-
await clientStream.WriteAsync(TestData).DefaultTimeout();
675-
676-
// Server finishes accepting
677-
var serverStream = await serverStreamTask.DefaultTimeout();
678-
679-
// Assert
680-
AssertTlsConnectionFeature(serverConnection.Features, testCert);
681-
AssertTlsConnectionFeature(serverStream.Features, testCert);
682-
683-
static void AssertTlsConnectionFeature(IFeatureCollection features, X509Certificate2 testCert)
684-
{
685-
var tlsFeature = features.Get<ITlsConnectionFeature>();
686-
Assert.NotNull(tlsFeature);
687-
Assert.NotNull(tlsFeature.ClientCertificate);
688-
Assert.Equal(testCert, tlsFeature.ClientCertificate);
689-
}
690-
}
691-
692647
private record RequestState(
693648
QuicConnection QuicConnection,
694649
MultiplexedConnectionContext ServerConnection,

src/Servers/Kestrel/Transport.Quic/test/QuicConnectionListenerTests.cs

+68
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Net;
56
using System.Net.Quic;
7+
using System.Net.Security;
8+
using System.Security.Cryptography.X509Certificates;
9+
using System.Text;
610
using System.Threading.Tasks;
11+
using Microsoft.AspNetCore.Http.Features;
712
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
813
using Microsoft.AspNetCore.Testing;
914
using Xunit;
@@ -13,6 +18,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests
1318
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/35070")]
1419
public class QuicConnectionListenerTests : TestApplicationErrorLoggerLoggedTest
1520
{
21+
private static readonly byte[] TestData = Encoding.UTF8.GetBytes("Hello world");
22+
1623
[ConditionalFact]
1724
[MsQuicSupported]
1825
public async Task AcceptAsync_AfterUnbind_Error()
@@ -51,5 +58,66 @@ public async Task AcceptAsync_ClientCreatesConnection_ServerAccepts()
5158
// ConnectionClosed isn't triggered because the server initiated close.
5259
Assert.False(serverConnection.ConnectionClosed.IsCancellationRequested);
5360
}
61+
62+
[ConditionalFact]
63+
[MsQuicSupported]
64+
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
65+
public async Task ClientCertificate_Required_Sent_Populated()
66+
{
67+
// Arrange
68+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, clientCertificateRequired: true);
69+
70+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
71+
var testCert = TestResources.GetTestCertificate();
72+
options.ClientAuthenticationOptions.ClientCertificates = new X509CertificateCollection { testCert };
73+
74+
// Act
75+
using var quicConnection = new QuicConnection(options);
76+
await quicConnection.ConnectAsync().DefaultTimeout();
77+
78+
var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
79+
// Server waits for stream from client
80+
var serverStreamTask = serverConnection.AcceptAsync().DefaultTimeout();
81+
82+
// Client creates stream
83+
using var clientStream = quicConnection.OpenBidirectionalStream();
84+
await clientStream.WriteAsync(TestData).DefaultTimeout();
85+
86+
// Server finishes accepting
87+
var serverStream = await serverStreamTask.DefaultTimeout();
88+
89+
// Assert
90+
AssertTlsConnectionFeature(serverConnection.Features, testCert);
91+
AssertTlsConnectionFeature(serverStream.Features, testCert);
92+
93+
static void AssertTlsConnectionFeature(IFeatureCollection features, X509Certificate2 testCert)
94+
{
95+
var tlsFeature = features.Get<ITlsConnectionFeature>();
96+
Assert.NotNull(tlsFeature);
97+
Assert.NotNull(tlsFeature.ClientCertificate);
98+
Assert.Equal(testCert, tlsFeature.ClientCertificate);
99+
}
100+
}
101+
102+
[ConditionalFact]
103+
[MsQuicSupported]
104+
// https://github.com/dotnet/runtime/issues/57308, RemoteCertificateValidationCallback should allow us to accept a null cert,
105+
// but it doesn't right now.
106+
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
107+
public async Task ClientCertificate_Required_NotSent_ConnectionAborted()
108+
{
109+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory, clientCertificateRequired: true);
110+
111+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
112+
using var clientConnection = new QuicConnection(options);
113+
114+
var qex = await Assert.ThrowsAsync<QuicException>(async () => await clientConnection.ConnectAsync().DefaultTimeout());
115+
Assert.Equal("Connection has been shutdown by transport. Error Code: 0x80410100", qex.Message);
116+
117+
// https://github.com/dotnet/runtime/issues/57246 The accept still completes even though the connection was rejected, but it's already failed.
118+
var serverContext = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
119+
qex = await Assert.ThrowsAsync<QuicException>(() => serverContext.ConnectAsync().DefaultTimeout());
120+
Assert.Equal("Failed to open stream to peer. Error Code: INVALID_STATE", qex.Message);
121+
}
54122
}
55123
}

src/Servers/Kestrel/samples/Http3SampleApp/Program.cs

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public static void Main(string[] args)
2929
options.ConfigureHttpsDefaults(httpsOptions =>
3030
{
3131
httpsOptions.ServerCertificate = cert;
32+
// httpsOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
33+
// httpsOptions.AllowAnyClientCertificate();
3234
});
3335

3436
options.ListenAnyIP(5000, listenOptions =>

src/Servers/Kestrel/samples/Http3SampleApp/Startup.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public void Configure(IApplicationBuilder app)
2020
var length = await context.Request.Body.ReadAsync(memory);
2121
context.Response.Headers["test"] = "foo";
2222
// for testing
23-
await context.Response.WriteAsync("Hello World! " + context.Request.Protocol);
23+
await context.Response.WriteAsync($"Hello World! {context.Request.Protocol} {context.Connection.ClientCertificate?.Subject}");
2424
});
2525
}
2626
}

src/Servers/Kestrel/samples/HttpClientApp/HttpClientApp.csproj

+5
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@
99
<RuntimeHostConfigurationOption Include="System.Net.SocketsHttpHandler.Http3Support" Value="true" />
1010
</ItemGroup>
1111

12+
<ItemGroup>
13+
<Compile Include="$(KestrelSharedSourceRoot)test\TestResources.cs" LinkBase="shared" />
14+
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
15+
</ItemGroup>
16+
1217
</Project>

src/Servers/Kestrel/samples/HttpClientApp/Program.cs

+13-3
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,27 @@
33

44
using System.Net;
55
using System.Net.Http;
6+
using System.Security.Cryptography.X509Certificates;
7+
using Microsoft.AspNetCore.Testing;
8+
9+
// Console.WriteLine("Ready");
10+
// Console.ReadKey();
611

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

1016
using var client = new HttpClient(handler);
11-
client.DefaultRequestVersion = HttpVersion.Version20;
17+
client.DefaultRequestVersion =
18+
HttpVersion.Version20;
19+
// HttpVersion.Version30;
1220
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
1321

14-
var response = await client.GetAsync("https://localhost:5001");
22+
var response = await client.GetAsync("https://localhost:5003");
1523
Console.WriteLine(response);
24+
Console.WriteLine(await response.Content.ReadAsStringAsync());
1625

1726
// Alt-svc enables an upgrade after the first request.
18-
response = await client.GetAsync("https://localhost:5001");
27+
response = await client.GetAsync("https://localhost:5003");
1928
Console.WriteLine(response);
29+
Console.WriteLine(await response.Content.ReadAsStringAsync());

src/Servers/Kestrel/shared/test/TestResources.cs

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.IO;
64
using System.Security.Cryptography.X509Certificates;
7-
using System.Threading;
8-
using Xunit;
95

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

3329
try

src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3Helpers.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
using System.Diagnostics;
55
using System.Net;
66
using System.Net.Http;
7+
using System.Security.Cryptography.X509Certificates;
78
using Microsoft.AspNetCore.Builder;
89
using Microsoft.AspNetCore.Connections;
910
using Microsoft.AspNetCore.Hosting;
1011
using Microsoft.AspNetCore.Http;
1112
using Microsoft.AspNetCore.Server.Kestrel.Core;
13+
using Microsoft.AspNetCore.Testing;
1214
using Microsoft.Extensions.DependencyInjection;
1315
using Microsoft.Extensions.Hosting;
1416
using Microsoft.Extensions.Logging;
@@ -17,13 +19,14 @@ namespace Interop.FunctionalTests.Http3
1719
{
1820
public static class Http3Helpers
1921
{
20-
public static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null)
22+
public static HttpMessageInvoker CreateClient(TimeSpan? idleTimeout = null, bool includeClientCert = false)
2123
{
2224
var handler = new SocketsHttpHandler();
2325
handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions
2426
{
2527
RemoteCertificateValidationCallback = (_, __, ___, ____) => true,
26-
TargetHost = "targethost"
28+
TargetHost = "targethost",
29+
ClientCertificates = !includeClientCert ? null : new X509CertificateCollection() { TestResources.GetTestCertificate() },
2730
};
2831
if (idleTimeout != null)
2932
{

0 commit comments

Comments
 (0)