Downgrade or throw when HTTP/2 over TLS is configured on older Windows versions (#22859)

HTTP/2 over TLS is not compatible with Windows versions strictly older than Windows 10 or Windows Server 2016. Update kestrel to:
- Downgrade to HTTP/1.1 when Http1AndHttp2 is configured.
- Throw NotSupportedException when Http2 is configured.
- Allow HTTP/2 over TLS to be enabled if AppContext switch Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2 is set. This allows users who have configured cipher suites on Windows 8.1 and Windows Server 2012 R2 to continue using HTTP/2 over TLS.
This commit is contained in:
John Luo 2020-06-17 16:50:48 -07:00 committed by GitHub
parent f48f558f48
commit 45e6571649
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 137 additions and 17 deletions

View File

@ -563,12 +563,9 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="RequestTrailersNotAvailable" xml:space="preserve"> <data name="RequestTrailersNotAvailable" xml:space="preserve">
<value>The request trailers are not available yet. They may not be available until the full request body is read.</value> <value>The request trailers are not available yet. They may not be available until the full request body is read.</value>
</data> </data>
<data name="HTTP2NoTlsOsx" xml:space="preserve"> <data name="Http2NoTlsOsx" xml:space="preserve">
<value>HTTP/2 over TLS is not supported on macOS due to missing ALPN support.</value> <value>HTTP/2 over TLS is not supported on macOS due to missing ALPN support.</value>
</data> </data>
<data name="HTTP2NoTlsWin7" xml:space="preserve">
<value>HTTP/2 over TLS is not supported on Windows 7 due to missing ALPN support.</value>
</data>
<data name="Http2StreamResetByApplication" xml:space="preserve"> <data name="Http2StreamResetByApplication" xml:space="preserve">
<value>The HTTP/2 stream was reset by the application with error code {errorCode}.</value> <value>The HTTP/2 stream was reset by the application with error code {errorCode}.</value>
</data> </data>
@ -605,6 +602,12 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="HttpsConnectionEstablished" xml:space="preserve"> <data name="HttpsConnectionEstablished" xml:space="preserve">
<value>Connection "{connectionId}" established using the following protocol: {protocol}</value> <value>Connection "{connectionId}" established using the following protocol: {protocol}</value>
</data> </data>
<data name="Http2DefaultCiphersInsufficient" xml:space="preserve">
<value>HTTP/2 over TLS is not supported on Windows versions older than Windows 10 and Windows Server 2016 due to incompatible ciphers or missing ALPN support. Falling back to HTTP/1.1 instead.</value>
</data>
<data name="Http2NoTlsWin81" xml:space="preserve">
<value>HTTP/2 over TLS is not supported on Windows versions earlier than Windows 10 and Windows Server 2016 due to incompatible ciphers or missing ALPN support.</value>
</data>
<data name="Http2ErrorKeepAliveTimeout" xml:space="preserve"> <data name="Http2ErrorKeepAliveTimeout" xml:space="preserve">
<value>Timeout while waiting for incoming HTTP/2 frames after a keep alive ping.</value> <value>Timeout while waiting for incoming HTTP/2 frames after a keep alive ping.</value>
</data> </data>

View File

@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
{ {
internal class HttpsConnectionMiddleware internal class HttpsConnectionMiddleware
{ {
private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";
private readonly ConnectionDelegate _next; private readonly ConnectionDelegate _next;
private readonly HttpsConnectionAdapterOptions _options; private readonly HttpsConnectionAdapterOptions _options;
private readonly ILogger _logger; private readonly ILogger _logger;
@ -43,18 +44,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
_options = options;
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
// This configuration will always fail per-request, preemptively fail it here. See HttpConnection.SelectProtocol(). // This configuration will always fail per-request, preemptively fail it here. See HttpConnection.SelectProtocol().
if (options.HttpProtocols == HttpProtocols.Http2) if (options.HttpProtocols == HttpProtocols.Http2)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{ {
throw new NotSupportedException(CoreStrings.HTTP2NoTlsOsx); throw new NotSupportedException(CoreStrings.Http2NoTlsOsx);
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && Environment.OSVersion.Version < new Version(6, 2)) else if (IsWindowsVersionIncompatible())
{ {
throw new NotSupportedException(CoreStrings.HTTP2NoTlsWin7); throw new NotSupportedException(CoreStrings.Http2NoTlsWin81);
} }
} }
else if (options.HttpProtocols == HttpProtocols.Http1AndHttp2 && IsWindowsVersionIncompatible())
{
_logger.Http2DefaultCiphersInsufficient();
options.HttpProtocols = HttpProtocols.Http1;
}
_next = next; _next = next;
// capture the certificate now so it can't be switched after validation // capture the certificate now so it can't be switched after validation
@ -75,9 +84,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
{ {
EnsureCertificateIsAllowedForServerAuth(_serverCertificate); EnsureCertificateIsAllowedForServerAuth(_serverCertificate);
} }
_options = options;
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
} }
public async Task OnConnectionAsync(ConnectionContext context) public async Task OnConnectionAsync(ConnectionContext context)
@ -214,7 +220,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId); KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null); KestrelEventSource.Log.TlsHandshakeStop(context, null);
_logger.LogDebug(2, CoreStrings.AuthenticationTimedOut); _logger.AuthenticationTimedOut();
await sslStream.DisposeAsync(); await sslStream.DisposeAsync();
return; return;
} }
@ -223,7 +229,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId); KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null); KestrelEventSource.Log.TlsHandshakeStop(context, null);
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed); _logger.AuthenticationFailed(ex);
await sslStream.DisposeAsync(); await sslStream.DisposeAsync();
return; return;
} }
@ -232,7 +238,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId); KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null); KestrelEventSource.Log.TlsHandshakeStop(context, null);
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed); _logger.AuthenticationFailed(ex);
await sslStream.DisposeAsync(); await sslStream.DisposeAsync();
return; return;
@ -252,7 +258,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeStop(context, feature); KestrelEventSource.Log.TlsHandshakeStop(context, feature);
_logger.LogDebug(3, CoreStrings.HttpsConnectionEstablished, context.ConnectionId, sslStream.SslProtocol); _logger.HttpsConnectionEstablished(context.ConnectionId, sslStream.SslProtocol);
var originalTransport = context.Transport; var originalTransport = context.Transport;
@ -298,5 +304,57 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
return new X509Certificate2(certificate); return new X509Certificate2(certificate);
} }
private static bool IsWindowsVersionIncompatible()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var enableHttp2OnWindows81 = AppContext.TryGetSwitch(EnableWindows81Http2, out var enabled) && enabled;
if (Environment.OSVersion.Version < new Version(6, 3) // Missing ALPN support
// Win8.1 and 2012 R2 don't support the right cipher configuration by default.
|| (Environment.OSVersion.Version < new Version(10, 0) && !enableHttp2OnWindows81))
{
return true;
}
}
return false;
}
}
internal static class HttpsConnectionMiddlewareLoggerExtensions
{
private static readonly Action<ILogger, Exception> _authenticationFailed =
LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: new EventId(1, "AuthenticationFailed"),
formatString: CoreStrings.AuthenticationFailed);
private static readonly Action<ILogger, Exception> _authenticationTimedOut =
LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: new EventId(2, "AuthenticationTimedOut"),
formatString: CoreStrings.AuthenticationTimedOut);
private static readonly Action<ILogger, string, SslProtocols, Exception> _httpsConnectionEstablished =
LoggerMessage.Define<string, SslProtocols>(
logLevel: LogLevel.Debug,
eventId: new EventId(3, "HttpsConnectionEstablished"),
formatString: CoreStrings.HttpsConnectionEstablished);
private static readonly Action<ILogger, Exception> _http2DefaultCiphersInsufficient =
LoggerMessage.Define(
logLevel: LogLevel.Information,
eventId: new EventId(4, "Http2DefaultCiphersInsufficient"),
formatString: CoreStrings.Http2DefaultCiphersInsufficient);
public static void AuthenticationFailed(this ILogger logger, Exception exception) => _authenticationFailed(logger, exception);
public static void AuthenticationTimedOut(this ILogger logger) => _authenticationTimedOut(logger, null);
public static void HttpsConnectionEstablished(this ILogger logger, string connectionId, SslProtocols sslProtocol) => _httpsConnectionEstablished(logger, connectionId, sslProtocol, null);
public static void Http2DefaultCiphersInsufficient(this ILogger logger) => _http2DefaultCiphersInsufficient(logger, null);
} }
} }

View File

@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
[ConditionalFact] [ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")] [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2 [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task TlsAlpnHandshakeSelectsHttp2From1and2() public async Task TlsAlpnHandshakeSelectsHttp2From1and2()
{ {
using (var server = new TestServer(context => using (var server = new TestServer(context =>
@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
[ConditionalFact] [ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")] [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2 [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task TlsAlpnHandshakeSelectsHttp2() public async Task TlsAlpnHandshakeSelectsHttp2()
{ {
using (var server = new TestServer(context => using (var server = new TestServer(context =>

View File

@ -594,7 +594,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
[InlineData(HttpProtocols.Http1AndHttp2)] [InlineData(HttpProtocols.Http1AndHttp2)]
[OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")] [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Missing SslStream ALPN support: https://github.com/dotnet/corefx/issues/30492")]
[SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2 [SkipOnHelix("https://github.com/dotnet/aspnetcore/issues/10428", Queues = "Debian.8.Amd64;Debian.8.Amd64.Open")] // Debian 8 uses OpenSSL 1.0.1 which does not support HTTP/2
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)] [MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task ListenOptionsProtolsCanBeSetAfterUseHttps(HttpProtocols httpProtocols) public async Task ListenOptionsProtolsCanBeSetAfterUseHttps(HttpProtocols httpProtocols)
{ {
void ConfigureListenOptions(ListenOptions listenOptions) void ConfigureListenOptions(ListenOptions listenOptions)
@ -623,6 +623,65 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
stream.NegotiatedApplicationProtocol); stream.NegotiatedApplicationProtocol);
} }
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
public void Http1AndHttp2DowngradeToHttp1ForHttpsOnIncompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http1AndHttp2
};
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
Assert.Equal(HttpProtocols.Http1, httpConnectionAdapterOptions.HttpProtocols);
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Downgrade logic only applies on Windows")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public void Http1AndHttp2DoesNotDowngradeOnCompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http1AndHttp2
};
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
Assert.Equal(HttpProtocols.Http1AndHttp2, httpConnectionAdapterOptions.HttpProtocols);
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
[MaximumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
public void Http2ThrowsOnIncompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http2
};
Assert.Throws<NotSupportedException>(() => new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions));
}
[ConditionalFact]
[OSSkipCondition(OperatingSystems.MacOSX | OperatingSystems.Linux, SkipReason = "Error logic only applies on Windows")]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public void Http2DoesNotThrowOnCompatibleWindowsVersions()
{
var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
{
ServerCertificate = _x509Certificate2,
HttpProtocols = HttpProtocols.Http2
};
// Does not throw
new HttpsConnectionMiddleware(context => Task.CompletedTask, httpConnectionAdapterOptions);
}
private static async Task App(HttpContext httpContext) private static async Task App(HttpContext httpContext)
{ {
var request = httpContext.Request; var request = httpContext.Request;