Skip to content

Commit bb6cc63

Browse files
Tratcherhalter73
andauthored
[7.0] Close connections on client cert failures (#43958)
* Close connections on client cert failures #41369 * Update TlsConnectionFeature.cs * Update src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs Co-authored-by: Stephen Halter <[email protected]>
1 parent eb817ca commit bb6cc63

File tree

3 files changed

+78
-55
lines changed

3 files changed

+78
-55
lines changed

src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Net.Security;
55
using System.Security.Authentication;
66
using System.Security.Cryptography.X509Certificates;
7+
using Microsoft.AspNetCore.Connections;
78
using Microsoft.AspNetCore.Connections.Features;
89
using Microsoft.AspNetCore.Http.Features;
910
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
@@ -13,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
1314
internal sealed class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicationProtocolFeature, ITlsHandshakeFeature
1415
{
1516
private readonly SslStream _sslStream;
17+
private readonly ConnectionContext _context;
1618
private X509Certificate2? _clientCert;
1719
private ReadOnlyMemory<byte>? _applicationProtocol;
1820
private SslProtocols? _protocol;
@@ -24,14 +26,19 @@ internal sealed class TlsConnectionFeature : ITlsConnectionFeature, ITlsApplicat
2426
private int? _keyExchangeStrength;
2527
private Task<X509Certificate2?>? _clientCertTask;
2628

27-
public TlsConnectionFeature(SslStream sslStream)
29+
public TlsConnectionFeature(SslStream sslStream, ConnectionContext context)
2830
{
2931
if (sslStream is null)
3032
{
3133
throw new ArgumentNullException(nameof(sslStream));
3234
}
35+
if (context is null)
36+
{
37+
throw new ArgumentNullException(nameof(context));
38+
}
3339

3440
_sslStream = sslStream;
41+
_context = context;
3542
}
3643

3744
internal bool AllowDelayedClientCertificateNegotation { get; set; }
@@ -122,7 +129,20 @@ public int KeyExchangeStrength
122129

123130
private async Task<X509Certificate2?> GetClientCertificateAsyncCore(CancellationToken cancellationToken)
124131
{
125-
await _sslStream.NegotiateClientCertificateAsync(cancellationToken);
132+
try
133+
{
134+
await _sslStream.NegotiateClientCertificateAsync(cancellationToken);
135+
}
136+
catch
137+
{
138+
// We can't tell which exceptions are fatal or recoverable. Consider them all recoverable only given a new connection
139+
// and close the connection gracefully to avoid over-caching and affecting future requests on this connection.
140+
// This allows recovery by starting a new connection. The close is graceful to allow the server to
141+
// send an error response like 401. https://github.com/dotnet/aspnetcore/issues/41369
142+
_context.Features.Get<IConnectionLifetimeNotificationFeature>()?.RequestClose();
143+
throw;
144+
}
145+
126146
return ClientCertificate;
127147
}
128148

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ public async Task OnConnectionAsync(ConnectionContext context)
141141
context.Features.Get<IMemoryPoolFeature>()?.MemoryPool ?? MemoryPool<byte>.Shared);
142142
var sslStream = sslDuplexPipe.Stream;
143143

144-
var feature = new Core.Internal.TlsConnectionFeature(sslStream);
144+
var feature = new Core.Internal.TlsConnectionFeature(sslStream, context);
145145
// Set the mode if options were used. If the callback is used it will set the mode later.
146146
feature.AllowDelayedClientCertificateNegotation =
147147
_options?.ClientCertificateMode == ClientCertificateMode.DelayCertificate;

src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs

Lines changed: 55 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,6 @@ void ConfigureListenOptions(ListenOptions listenOptions)
539539
listenOptions.UseHttps(options =>
540540
{
541541
options.ServerCertificate = _x509Certificate2;
542-
options.SslProtocols = SslProtocols.Tls12; // Linux doesn't support renegotiate on TLS1.3 yet. https://github.com/dotnet/runtime/issues/55757
543542
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
544543
options.AllowAnyClientCertificate();
545544
});
@@ -626,7 +625,6 @@ void ConfigureListenOptions(ListenOptions listenOptions)
626625
return ValueTask.FromResult(new SslServerAuthenticationOptions()
627626
{
628627
ServerCertificate = _x509Certificate2,
629-
EnabledSslProtocols = SslProtocols.Tls12, // Linux doesn't support renegotiate on TLS1.3 yet. https://github.com/dotnet/runtime/issues/55757
630628
ClientCertificateRequired = false,
631629
RemoteCertificateValidationCallback = (_, _, _, _) => true,
632630
});
@@ -670,7 +668,6 @@ void ConfigureListenOptions(ListenOptions listenOptions)
670668
listenOptions.UseHttps(options =>
671669
{
672670
options.ServerCertificate = _x509Certificate2;
673-
options.SslProtocols = SslProtocols.Tls12; // Linux doesn't support renegotiate on TLS1.3 yet. https://github.com/dotnet/runtime/issues/55757
674671
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
675672
options.AllowAnyClientCertificate();
676673
});
@@ -786,60 +783,13 @@ void ConfigureListenOptions(ListenOptions listenOptions)
786783
// then the connection is aborted.
787784
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing platform support.")]
788785
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.RedhatAmd64)] // Outdated OpenSSL client
789-
public async Task RenegotiateForClientCertificateOnPostWithoutBufferingThrows_TLS12()
786+
public async Task RenegotiateForClientCertificateOnPostWithoutBufferingThrows()
790787
{
791788
void ConfigureListenOptions(ListenOptions listenOptions)
792789
{
793790
listenOptions.Protocols = HttpProtocols.Http1;
794791
listenOptions.UseHttps(options =>
795792
{
796-
options.SslProtocols = SslProtocols.Tls12;
797-
options.ServerCertificate = _x509Certificate2;
798-
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
799-
options.AllowAnyClientCertificate();
800-
});
801-
}
802-
803-
// Under 4kb can sometimes work because it fits into Kestrel's header parsing buffer.
804-
var expectedBody = new string('a', 1024 * 4);
805-
806-
await using var server = new TestServer(async context =>
807-
{
808-
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
809-
Assert.NotNull(tlsFeature);
810-
Assert.Null(tlsFeature.ClientCertificate);
811-
Assert.Null(context.Connection.ClientCertificate);
812-
813-
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.Connection.GetClientCertificateAsync());
814-
Assert.Equal("Client stream needs to be drained before renegotiation.", ex.Message);
815-
Assert.Null(tlsFeature.ClientCertificate);
816-
Assert.Null(context.Connection.ClientCertificate);
817-
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
818-
819-
using var connection = server.CreateConnection();
820-
// SslStream is used to ensure the certificate is actually passed to the server
821-
// HttpClient might not send the certificate because it is invalid or it doesn't match any
822-
// of the certificate authorities sent by the server in the SSL handshake.
823-
// Use a random host name to avoid the TLS session resumption cache.
824-
var stream = OpenSslStreamWithCert(connection.Stream);
825-
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
826-
await AssertConnectionResult(stream, true, expectedBody);
827-
}
828-
829-
[ConditionalFact]
830-
// TLS 1.3 uses a new client cert negotiation extension that doesn't cause the connection to abort
831-
// for this error.
832-
[MinimumOSVersion(OperatingSystems.Windows, "10.0.20145")] // Needs a preview version with TLS 1.3 enabled.
833-
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "https://github.com/dotnet/runtime/issues/55757")]
834-
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.RedhatAmd64)] // Outdated OpenSSL client
835-
public async Task RenegotiateForClientCertificateOnPostWithoutBufferingThrows_TLS13()
836-
{
837-
void ConfigureListenOptions(ListenOptions listenOptions)
838-
{
839-
listenOptions.Protocols = HttpProtocols.Http1;
840-
listenOptions.UseHttps(options =>
841-
{
842-
options.SslProtocols = SslProtocols.Tls13;
843793
options.ServerCertificate = _x509Certificate2;
844794
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
845795
options.AllowAnyClientCertificate();
@@ -977,7 +927,6 @@ void ConfigureListenOptions(ListenOptions listenOptions)
977927
listenOptions.UseHttps(options =>
978928
{
979929
options.ServerCertificate = _x509Certificate2;
980-
options.SslProtocols = SslProtocols.Tls12; // Linux doesn't support renegotiate on TLS1.3 yet. https://github.com/dotnet/runtime/issues/55757
981930
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
982931
options.AllowAnyClientCertificate();
983932
});
@@ -1013,6 +962,60 @@ void ConfigureListenOptions(ListenOptions listenOptions)
1013962
await AssertConnectionResult(stream, true, expectedBody);
1014963
}
1015964

965+
[ConditionalFact]
966+
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing platform support.")]
967+
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/33566#issuecomment-892031659", Queues = HelixConstants.RedhatAmd64)] // Outdated OpenSSL client
968+
public async Task RenegotationFailureCausesConnectionClose()
969+
{
970+
void ConfigureListenOptions(ListenOptions listenOptions)
971+
{
972+
listenOptions.Protocols = HttpProtocols.Http1;
973+
listenOptions.UseHttps(options =>
974+
{
975+
options.ServerCertificate = _x509Certificate2;
976+
options.ClientCertificateMode = ClientCertificateMode.DelayCertificate;
977+
options.AllowAnyClientCertificate();
978+
});
979+
}
980+
981+
var expectedBody = new string('a', 1024 * 4);
982+
983+
await using var server = new TestServer(async context =>
984+
{
985+
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
986+
Assert.NotNull(tlsFeature);
987+
Assert.Null(tlsFeature.ClientCertificate);
988+
Assert.Null(context.Connection.ClientCertificate);
989+
990+
// Request the client cert while there's still body data in the buffers
991+
var ioe = await Assert.ThrowsAsync<InvalidOperationException>(() => context.Connection.GetClientCertificateAsync());
992+
Assert.Equal("Client stream needs to be drained before renegotiation.", ioe.Message);
993+
994+
context.Response.ContentLength = 11;
995+
await context.Response.WriteAsync("hello world");
996+
997+
}, new TestServiceContext(LoggerFactory), ConfigureListenOptions);
998+
999+
using var connection = server.CreateConnection();
1000+
// SslStream is used to ensure the certificate is actually passed to the server
1001+
// HttpClient might not send the certificate because it is invalid or it doesn't match any
1002+
// of the certificate authorities sent by the server in the SSL handshake.
1003+
// Use a random host name to avoid the TLS session resumption cache.
1004+
var stream = OpenSslStreamWithCert(connection.Stream);
1005+
await stream.AuthenticateAsClientAsync(Guid.NewGuid().ToString());
1006+
1007+
var request = Encoding.UTF8.GetBytes($"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: {expectedBody.Length}\r\n\r\n{expectedBody}");
1008+
await stream.WriteAsync(request, 0, request.Length).DefaultTimeout();
1009+
var reader = new StreamReader(stream);
1010+
Assert.Equal("HTTP/1.1 200 OK", await reader.ReadLineAsync().DefaultTimeout());
1011+
Assert.Equal("Content-Length: 11", await reader.ReadLineAsync().DefaultTimeout());
1012+
Assert.Equal("Connection: close", await reader.ReadLineAsync().DefaultTimeout());
1013+
Assert.StartsWith("Date: ", await reader.ReadLineAsync().DefaultTimeout());
1014+
Assert.Equal("", await reader.ReadLineAsync().DefaultTimeout());
1015+
Assert.Equal("hello world", await reader.ReadLineAsync().DefaultTimeout());
1016+
Assert.Null(await reader.ReadLineAsync().DefaultTimeout());
1017+
}
1018+
10161019
[Fact]
10171020
public async Task HttpsSchemePassedToRequestFeature()
10181021
{

0 commit comments

Comments
 (0)