Skip to content

Kestrel Endpoints' "SslProtocols" settable via config (#22663) #22910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -12,6 +14,7 @@ internal class ConfigurationReader
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";
Expand Down Expand Up @@ -49,13 +52,15 @@ private IDictionary<string, CertificateConfig> ReadCertificates()

// "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))
};
}

Expand All @@ -69,6 +74,7 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
// "EndpointName": {
// "Url": "https://*:5463",
// "Protocols": "Http1AndHttp2",
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
// "Certificate": {
// "Path": "testCert.pfx",
// "Password": "testPassword"
Expand All @@ -88,6 +94,7 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
Protocols = ParseProtocols(endpointConfig[ProtocolsKey]),
ConfigSection = endpointConfig,
Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)),
SslProtocols = ParseSslProcotols(endpointConfig.GetSection(SslProtocolsKey))
};

endpoints.Add(endpoint);
Expand All @@ -105,19 +112,37 @@ private IEnumerable<EndpointConfig> ReadEndpoints()

return null;
}

private static SslProtocols? ParseSslProcotols(IConfigurationSection sslProtocols)
{
var stringProtocols = sslProtocols.Get<string[]>();

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"
Expand All @@ -131,6 +156,7 @@ internal class EndpointConfig
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<EndpointConfiguration> callback.
Expand All @@ -154,6 +180,7 @@ obj is EndpointConfig other &&
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);
Expand Down
8 changes: 8 additions & 0 deletions src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand Down Expand Up @@ -279,9 +280,16 @@ public void Load()
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;
Expand Down
80 changes: 80 additions & 0 deletions src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -173,5 +174,84 @@ public void ReadEndpointsSection_ReturnsCollection()
Assert.Equal("cetlocation", cert4.Location);
Assert.True(cert4.AllowInvalid);
}

[Fact]
public void ReadEndpointWithSingleSslProtocolSet_ReturnsCorrectValue()
{
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("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<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("Endpoints:End1:SslProtocols:0", "Tls11"),
new KeyValuePair<string, string>("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<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("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<string, string>("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<string, string>("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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;
Expand All @@ -18,7 +19,7 @@

namespace Microsoft.AspNetCore.Server.Kestrel.Tests
{
public class KestrelConfigurationBuilderTests
public class KestrelConfigurationLoaderTests
{
private KestrelServerOptions CreateServerOptions()
{
Expand Down Expand Up @@ -456,6 +457,136 @@ private void EndpointConfigSectionCanSetProtocols(string input, HttpProtocols ex
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<string, string>("Endpoints:End1:SslProtocols:0", "Tls11"),
new KeyValuePair<string, string>("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<string, string>("Endpoints:End1:SslProtocols:0", "Tls11"),
new KeyValuePair<string, string>("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<string, string>("EndpointDefaults:SslProtocols:0", "Tls11"),
new KeyValuePair<string, string>("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<string, string>("EndpointDefaults:SslProtocols:0", "Tls11"),
new KeyValuePair<string, string>("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()
{
Expand Down