From a77e68f1c525760eb83cb83e5bab5847df3dcf9c Mon Sep 17 00:00:00 2001 From: Filip Staffa Date: Fri, 19 Jun 2020 17:09:56 +0200 Subject: [PATCH] Kestrel Endpoints' "SslProtocols" settable via config (#22663) (#22910) --- .../Core/src/Internal/ConfigurationReader.cs | 29 +++- .../Core/src/KestrelConfigurationLoader.cs | 8 ++ .../Kestrel/test/ConfigurationReaderTests.cs | 80 +++++++++++ ....cs => KestrelConfigurationLoaderTests.cs} | 133 +++++++++++++++++- 4 files changed, 248 insertions(+), 2 deletions(-) rename src/Servers/Kestrel/Kestrel/test/{KestrelConfigurationBuilderTests.cs => KestrelConfigurationLoaderTests.cs} (84%) diff --git a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs index cf37f5ec73..19ec02774a 100644 --- a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs +++ b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Security.Authentication; using Microsoft.Extensions.Configuration; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal @@ -12,6 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal private const string ProtocolsKey = "Protocols"; private const string CertificatesKey = "Certificates"; private const string CertificateKey = "Certificate"; + private const string SslProtocolsKey = "SslProtocols"; private const string EndpointDefaultsKey = "EndpointDefaults"; private const string EndpointsKey = "Endpoints"; private const string UrlKey = "Url"; @@ -49,13 +52,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal // "EndpointDefaults": { // "Protocols": "Http1AndHttp2", + // "SslProtocols": [ "Tls11", "Tls12", "Tls13"], // } private EndpointDefaults ReadEndpointDefaults() { var configSection = _configuration.GetSection(EndpointDefaultsKey); return new EndpointDefaults { - Protocols = ParseProtocols(configSection[ProtocolsKey]) + Protocols = ParseProtocols(configSection[ProtocolsKey]), + SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey)) }; } @@ -69,6 +74,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal // "EndpointName": { // "Url": "https://*:5463", // "Protocols": "Http1AndHttp2", + // "SslProtocols": [ "Tls11", "Tls12", "Tls13"], // "Certificate": { // "Path": "testCert.pfx", // "Password": "testPassword" @@ -88,6 +94,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal Protocols = ParseProtocols(endpointConfig[ProtocolsKey]), ConfigSection = endpointConfig, Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)), + SslProtocols = ParseSslProcotols(endpointConfig.GetSection(SslProtocolsKey)) }; endpoints.Add(endpoint); @@ -105,19 +112,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal return null; } + + private static SslProtocols? ParseSslProcotols(IConfigurationSection sslProtocols) + { + var stringProtocols = sslProtocols.Get(); + + return stringProtocols?.Aggregate(SslProtocols.None, (acc, current) => + { + if (Enum.TryParse(current, ignoreCase: true, out SslProtocols parsed)) + { + return acc | parsed; + } + + return acc; + }); + } } // "EndpointDefaults": { // "Protocols": "Http1AndHttp2", + // "SslProtocols": [ "Tls11", "Tls12", "Tls13"], // } internal class EndpointDefaults { public HttpProtocols? Protocols { get; set; } + public SslProtocols? SslProtocols { get; set; } } // "EndpointName": { // "Url": "https://*:5463", // "Protocols": "Http1AndHttp2", + // "SslProtocols": [ "Tls11", "Tls12", "Tls13"], // "Certificate": { // "Path": "testCert.pfx", // "Password": "testPassword" @@ -131,6 +156,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public string Name { get; set; } public string Url { get; set; } public HttpProtocols? Protocols { get; set; } + public SslProtocols? SslProtocols { get; set; } public CertificateConfig Certificate { get; set; } // Compare config sections because it's accessible to app developers via an Action callback. @@ -154,6 +180,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal Url == other.Url && (Protocols ?? ListenOptions.DefaultHttpProtocols) == (other.Protocols ?? ListenOptions.DefaultHttpProtocols) && Certificate == other.Certificate && + (SslProtocols ?? System.Security.Authentication.SslProtocols.None) == (other.SslProtocols ?? System.Security.Authentication.SslProtocols.None) && _configSectionClone == other._configSectionClone; public override int GetHashCode() => HashCode.Combine(Name, Url, Protocols ?? ListenOptions.DefaultHttpProtocols, Certificate, _configSectionClone); diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs index f68b324053..193d07ca32 100644 --- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Certificates.Generation; @@ -279,9 +280,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel var httpsOptions = new HttpsConnectionAdapterOptions(); if (https) { + httpsOptions.SslProtocols = ConfigurationReader.EndpointDefaults.SslProtocols ?? SslProtocols.None; + // Defaults Options.ApplyHttpsDefaults(httpsOptions); + if (endpoint.SslProtocols.HasValue) + { + httpsOptions.SslProtocols = endpoint.SslProtocols.Value; + } + // Specified httpsOptions.ServerCertificate = LoadCertificate(endpoint.Certificate, endpoint.Name) ?? httpsOptions.ServerCertificate; diff --git a/src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs b/src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs index 7097c16553..a83e90818d 100644 --- a/src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Authentication; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.Extensions.Configuration; using Xunit; @@ -173,5 +174,84 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests Assert.Equal("cetlocation", cert4.Location); Assert.True(cert4.AllowInvalid); } + + [Fact] + public void ReadEndpointWithSingleSslProtocolSet_ReturnsCorrectValue() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", "http://*:5001"), + new KeyValuePair("Endpoints:End1:SslProtocols:0", "Tls11"), + }).Build(); + var reader = new ConfigurationReader(config); + + var endpoint = reader.Endpoints.First(); + Assert.Equal(SslProtocols.Tls11, endpoint.SslProtocols); + } + + [Fact] + public void ReadEndpointWithMultipleSslProtocolsSet_ReturnsCorrectValue() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", "http://*:5001"), + new KeyValuePair("Endpoints:End1:SslProtocols:0", "Tls11"), + new KeyValuePair("Endpoints:End1:SslProtocols:1", "Tls12"), + }).Build(); + var reader = new ConfigurationReader(config); + + var endpoint = reader.Endpoints.First(); + Assert.Equal(SslProtocols.Tls11|SslProtocols.Tls12, endpoint.SslProtocols); + } + + [Fact] + public void ReadEndpointWithSslProtocolSet_ReadsCaseInsensitive() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", "http://*:5001"), + new KeyValuePair("Endpoints:End1:SslProtocols:0", "TLS11"), + }).Build(); + var reader = new ConfigurationReader(config); + + var endpoint = reader.Endpoints.First(); + Assert.Equal(SslProtocols.Tls11, endpoint.SslProtocols); + } + + [Fact] + public void ReadEndpointWithNoSslProtocolSettings_ReturnsNull() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", "http://*:5001"), + }).Build(); + var reader = new ConfigurationReader(config); + + var endpoint = reader.Endpoints.First(); + Assert.Null(endpoint.SslProtocols); + } + + [Fact] + public void ReadEndpointDefaultsWithSingleSslProtocolSet_ReturnsCorrectValue() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("EndpointDefaults:SslProtocols:0", "Tls11"), + }).Build(); + var reader = new ConfigurationReader(config); + + var endpoint = reader.EndpointDefaults; + Assert.Equal(SslProtocols.Tls11, endpoint.SslProtocols); + } + + [Fact] + public void ReadEndpointDefaultsWithNoSslProtocolSettings_ReturnsCorrectValue() + { + var config = new ConfigurationBuilder().Build(); + var reader = new ConfigurationReader(config); + + var endpoint = reader.EndpointDefaults; + Assert.Null(endpoint.SslProtocols); + } } } diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs similarity index 84% rename from src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs rename to src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs index 2b38083cfd..8d93023a46 100644 --- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationBuilderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -18,7 +19,7 @@ using Xunit; namespace Microsoft.AspNetCore.Server.Kestrel.Tests { - public class KestrelConfigurationBuilderTests + public class KestrelConfigurationLoaderTests { private KestrelServerOptions CreateServerOptions() { @@ -456,6 +457,136 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests Assert.True(ran3); } + [Fact] + public void EndpointConfigureSection_CanSetSslProtocol() + { + var serverOptions = CreateServerOptions(); + var ranDefault = false; + + serverOptions.ConfigureHttpsDefaults(opt => + { + opt.ServerCertificate = TestResources.GetTestCertificate(); + + // Kestrel default + Assert.Equal(SslProtocols.None, opt.SslProtocols); + ranDefault = true; + }); + + var ran1 = false; + var ran2 = false; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:SslProtocols:0", "Tls11"), + new KeyValuePair("Endpoints:End1:Url", "https://*:5001"), + }).Build(); + serverOptions.Configure(config) + .Endpoint("End1", opt => + { + Assert.Equal(SslProtocols.Tls11, opt.HttpsOptions.SslProtocols); + ran1 = true; + }) + .Load(); + serverOptions.ListenAnyIP(0, opt => + { + opt.UseHttps(httpsOptions => + { + // Kestrel default. + Assert.Equal(SslProtocols.None, httpsOptions.SslProtocols); + ran2 = true; + }); + }); + + Assert.True(ranDefault); + Assert.True(ran1); + Assert.True(ran2); + } + + [Fact] + public void EndpointConfigureSection_CanOverrideSslProtocolsFromConfigureHttpsDefaults() + { + var serverOptions = CreateServerOptions(); + + serverOptions.ConfigureHttpsDefaults(opt => + { + opt.ServerCertificate = TestResources.GetTestCertificate(); + opt.SslProtocols = SslProtocols.Tls12; + }); + + var ran1 = false; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:SslProtocols:0", "Tls11"), + new KeyValuePair("Endpoints:End1:Url", "https://*:5001"), + }).Build(); + serverOptions.Configure(config) + .Endpoint("End1", opt => + { + Assert.Equal(SslProtocols.Tls11, opt.HttpsOptions.SslProtocols); + ran1 = true; + }) + .Load(); + + Assert.True(ran1); + } + + [Fact] + public void DefaultEndpointConfigureSection_CanSetSslProtocols() + { + var serverOptions = CreateServerOptions(); + + serverOptions.ConfigureHttpsDefaults(opt => + { + opt.ServerCertificate = TestResources.GetTestCertificate(); + }); + + var ran1 = false; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("EndpointDefaults:SslProtocols:0", "Tls11"), + new KeyValuePair("Endpoints:End1:Url", "https://*:5001"), + }).Build(); + serverOptions.Configure(config) + .Endpoint("End1", opt => + { + Assert.Equal(SslProtocols.Tls11, opt.HttpsOptions.SslProtocols); + ran1 = true; + }) + .Load(); + + Assert.True(ran1); + } + + + [Fact] + public void DefaultEndpointConfigureSection_ConfigureHttpsDefaultsCanOverrideSslProtocols() + { + var serverOptions = CreateServerOptions(); + + serverOptions.ConfigureHttpsDefaults(opt => + { + opt.ServerCertificate = TestResources.GetTestCertificate(); + + Assert.Equal(SslProtocols.Tls11, opt.SslProtocols); + opt.SslProtocols = SslProtocols.Tls12; + }); + + var ran1 = false; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("EndpointDefaults:SslProtocols:0", "Tls11"), + new KeyValuePair("Endpoints:End1:Url", "https://*:5001"), + }).Build(); + serverOptions.Configure(config) + .Endpoint("End1", opt => + { + Assert.Equal(SslProtocols.Tls12, opt.HttpsOptions.SslProtocols); + ran1 = true; + }) + .Load(); + + Assert.True(ran1); + } + [Fact] public void Latin1RequestHeadersReadFromConfig() {