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">
<value>The request trailers are not available yet. They may not be available until the full request body is read.</value>
</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>
</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">
<value>The HTTP/2 stream was reset by the application with error code {errorCode}.</value>
</data>
@ -605,6 +602,12 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="HttpsConnectionEstablished" xml:space="preserve">
<value>Connection "{connectionId}" established using the following protocol: {protocol}</value>
</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">
<value>Timeout while waiting for incoming HTTP/2 frames after a keep alive ping.</value>
</data>

View File

@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
{
internal class HttpsConnectionMiddleware
{
private const string EnableWindows81Http2 = "Microsoft.AspNetCore.Server.Kestrel.EnableWindows81Http2";
private readonly ConnectionDelegate _next;
private readonly HttpsConnectionAdapterOptions _options;
private readonly ILogger _logger;
@ -43,18 +44,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
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().
if (options.HttpProtocols == HttpProtocols.Http2)
{
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;
// 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);
}
_options = options;
_logger = loggerFactory.CreateLogger<HttpsConnectionMiddleware>();
}
public async Task OnConnectionAsync(ConnectionContext context)
@ -214,7 +220,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null);
_logger.LogDebug(2, CoreStrings.AuthenticationTimedOut);
_logger.AuthenticationTimedOut();
await sslStream.DisposeAsync();
return;
}
@ -223,7 +229,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null);
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
_logger.AuthenticationFailed(ex);
await sslStream.DisposeAsync();
return;
}
@ -232,7 +238,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeFailed(context.ConnectionId);
KestrelEventSource.Log.TlsHandshakeStop(context, null);
_logger.LogDebug(1, ex, CoreStrings.AuthenticationFailed);
_logger.AuthenticationFailed(ex);
await sslStream.DisposeAsync();
return;
@ -252,7 +258,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
KestrelEventSource.Log.TlsHandshakeStop(context, feature);
_logger.LogDebug(3, CoreStrings.HttpsConnectionEstablished, context.ConnectionId, sslStream.SslProtocol);
_logger.HttpsConnectionEstablished(context.ConnectionId, sslStream.SslProtocol);
var originalTransport = context.Transport;
@ -298,5 +304,57 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
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]
[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
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task TlsAlpnHandshakeSelectsHttp2From1and2()
{
using (var server = new TestServer(context =>
@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests.Http2
[ConditionalFact]
[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
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task TlsAlpnHandshakeSelectsHttp2()
{
using (var server = new TestServer(context =>

View File

@ -594,7 +594,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
[InlineData(HttpProtocols.Http1AndHttp2)]
[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
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win81)]
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10)]
public async Task ListenOptionsProtolsCanBeSetAfterUseHttps(HttpProtocols httpProtocols)
{
void ConfigureListenOptions(ListenOptions listenOptions)
@ -623,6 +623,65 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
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)
{
var request = httpContext.Request;