Skip to content

Load ClientCertificateMode from config #24076

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 6 commits into from
Jul 23, 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
28 changes: 24 additions & 4 deletions src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Authentication;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Configuration;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
Expand All @@ -18,6 +19,7 @@ internal class ConfigurationReader
private const string EndpointDefaultsKey = "EndpointDefaults";
private const string EndpointsKey = "Endpoints";
private const string UrlKey = "Url";
private const string ClientCertificateModeKey = "ClientCertificateMode";

private readonly IConfiguration _configuration;

Expand Down Expand Up @@ -50,14 +52,16 @@ private IDictionary<string, CertificateConfig> ReadCertificates()
// "EndpointDefaults": {
// "Protocols": "Http1AndHttp2",
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
// "ClientCertificateMode" : "NoCertificate"
// }
private EndpointDefaults ReadEndpointDefaults()
{
var configSection = _configuration.GetSection(EndpointDefaultsKey);
return new EndpointDefaults
{
Protocols = ParseProtocols(configSection[ProtocolsKey]),
SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey))
SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey)),
ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey])
};
}

Expand All @@ -75,7 +79,8 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
// "Certificate": {
// "Path": "testCert.pfx",
// "Password": "testPassword"
// }
// },
// "ClientCertificateMode" : "NoCertificate"
// }

var url = endpointConfig[UrlKey];
Expand All @@ -91,7 +96,8 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
Protocols = ParseProtocols(endpointConfig[ProtocolsKey]),
ConfigSection = endpointConfig,
Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)),
SslProtocols = ParseSslProcotols(endpointConfig.GetSection(SslProtocolsKey))
SslProtocols = ParseSslProcotols(endpointConfig.GetSection(SslProtocolsKey)),
ClientCertificateMode = ParseClientCertificateMode(endpointConfig[ClientCertificateModeKey])
};

endpoints.Add(endpoint);
Expand All @@ -100,6 +106,16 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
return endpoints;
}

private ClientCertificateMode? ParseClientCertificateMode(string clientCertificateMode)
{
if (Enum.TryParse<ClientCertificateMode>(clientCertificateMode, ignoreCase: true, out var result))
{
return result;
}

return null;
}

private static HttpProtocols? ParseProtocols(string protocols)
{
if (Enum.TryParse<HttpProtocols>(protocols, ignoreCase: true, out var result))
Expand Down Expand Up @@ -129,11 +145,13 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
// "EndpointDefaults": {
// "Protocols": "Http1AndHttp2",
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
// "ClientCertificateMode" : "NoCertificate"
// }
internal class EndpointDefaults
{
public HttpProtocols? Protocols { get; set; }
public SslProtocols? SslProtocols { get; set; }
public ClientCertificateMode? ClientCertificateMode { get; set; }
}

// "EndpointName": {
Expand All @@ -143,7 +161,8 @@ internal class EndpointDefaults
// "Certificate": {
// "Path": "testCert.pfx",
// "Password": "testPassword"
// }
// },
// "ClientCertificateMode" : "NoCertificate"
// }
internal class EndpointConfig
{
Expand All @@ -155,6 +174,7 @@ internal class EndpointConfig
public HttpProtocols? Protocols { get; set; }
public SslProtocols? SslProtocols { get; set; }
public CertificateConfig Certificate { get; set; }
public ClientCertificateMode? ClientCertificateMode { get; set; }

// Compare config sections because it's accessible to app developers via an Action<EndpointConfiguration> callback.
// We cannot rely entirely on comparing config sections for equality, because KestrelConfigurationLoader.Reload() sets
Expand Down
6 changes: 6 additions & 0 deletions src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ public void Load()
if (https)
{
httpsOptions.SslProtocols = ConfigurationReader.EndpointDefaults.SslProtocols ?? SslProtocols.None;
httpsOptions.ClientCertificateMode = ConfigurationReader.EndpointDefaults.ClientCertificateMode ?? ClientCertificateMode.NoCertificate;

// Defaults
Options.ApplyHttpsDefaults(httpsOptions);
Expand All @@ -289,6 +290,11 @@ public void Load()
httpsOptions.SslProtocols = endpoint.SslProtocols.Value;
}

if (endpoint.ClientCertificateMode.HasValue)
{
httpsOptions.ClientCertificateMode = endpoint.ClientCertificateMode.Value;
}

// Specified
httpsOptions.ServerCertificate = LoadCertificate(endpoint.Certificate, endpoint.Name)
?? httpsOptions.ServerCertificate;
Expand Down
48 changes: 46 additions & 2 deletions src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Security.Authentication;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Configuration;
using Xunit;

Expand Down Expand Up @@ -92,7 +93,7 @@ public void ReadCertificatesSection_IsCaseInsensitive()
[Fact]
public void ReadCertificatesSection_ThrowsOnCaseInsensitiveDuplicate()
{
var exception = Assert.Throws<ArgumentException>(() =>
var exception = Assert.Throws<ArgumentException>(() =>
new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Certificates:filecert:Password", "certpassword"),
Expand Down Expand Up @@ -154,10 +155,13 @@ public void ReadEndpointsSection_ReturnsCollection()
{
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
new KeyValuePair<string, string>("Endpoints:End2:Url", "https://*:5002"),
new KeyValuePair<string, string>("Endpoints:End2:ClientCertificateMode", "AllowCertificate"),
new KeyValuePair<string, string>("Endpoints:End3:Url", "https://*:5003"),
new KeyValuePair<string, string>("Endpoints:End3:ClientCertificateMode", "RequireCertificate"),
new KeyValuePair<string, string>("Endpoints:End3:Certificate:Path", "/path/cert.pfx"),
new KeyValuePair<string, string>("Endpoints:End3:Certificate:Password", "certpassword"),
new KeyValuePair<string, string>("Endpoints:End4:Url", "https://*:5004"),
new KeyValuePair<string, string>("Endpoints:End4:ClientCertificateMode", "NoCertificate"),
new KeyValuePair<string, string>("Endpoints:End4:Certificate:Subject", "certsubject"),
new KeyValuePair<string, string>("Endpoints:End4:Certificate:Store", "certstore"),
new KeyValuePair<string, string>("Endpoints:End4:Certificate:Location", "cetlocation"),
Expand All @@ -171,20 +175,23 @@ public void ReadEndpointsSection_ReturnsCollection()
var end1 = endpoints.First();
Assert.Equal("End1", end1.Name);
Assert.Equal("http://*:5001", end1.Url);
Assert.Null(end1.ClientCertificateMode);
Assert.NotNull(end1.ConfigSection);
Assert.NotNull(end1.Certificate);
Assert.False(end1.Certificate.ConfigSection.Exists());

var end2 = endpoints.Skip(1).First();
Assert.Equal("End2", end2.Name);
Assert.Equal("https://*:5002", end2.Url);
Assert.Equal(ClientCertificateMode.AllowCertificate, end2.ClientCertificateMode);
Assert.NotNull(end2.ConfigSection);
Assert.NotNull(end2.Certificate);
Assert.False(end2.Certificate.ConfigSection.Exists());

var end3 = endpoints.Skip(2).First();
Assert.Equal("End3", end3.Name);
Assert.Equal("https://*:5003", end3.Url);
Assert.Equal(ClientCertificateMode.RequireCertificate, end3.ClientCertificateMode);
Assert.NotNull(end3.ConfigSection);
Assert.NotNull(end3.Certificate);
Assert.True(end3.Certificate.ConfigSection.Exists());
Expand All @@ -197,6 +204,7 @@ public void ReadEndpointsSection_ReturnsCollection()
var end4 = endpoints.Skip(3).First();
Assert.Equal("End4", end4.Name);
Assert.Equal("https://*:5004", end4.Url);
Assert.Equal(ClientCertificateMode.NoCertificate, end4.ClientCertificateMode);
Assert.NotNull(end4.ConfigSection);
Assert.NotNull(end4.Certificate);
Assert.True(end4.Certificate.ConfigSection.Exists());
Expand Down Expand Up @@ -235,7 +243,7 @@ public void ReadEndpointWithMultipleSslProtocolsSet_ReturnsCorrectValue()
var reader = new ConfigurationReader(config);

var endpoint = reader.Endpoints.First();
Assert.Equal(SslProtocols.Tls11|SslProtocols.Tls12, endpoint.SslProtocols);
Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, endpoint.SslProtocols);
}

[Fact]
Expand Down Expand Up @@ -287,5 +295,41 @@ public void ReadEndpointDefaultsWithNoSslProtocolSettings_ReturnsCorrectValue()
var endpoint = reader.EndpointDefaults;
Assert.Null(endpoint.SslProtocols);
}

[Fact]
public void ReadEndpointWithNoClientCertificateModeSettings_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.ClientCertificateMode);
}

[Fact]
public void ReadEndpointDefaultsWithClientCertificateModeSet_ReturnsCorrectValue()
{
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", "AllowCertificate"),
}).Build();
var reader = new ConfigurationReader(config);

var endpoint = reader.EndpointDefaults;
Assert.Equal(ClientCertificateMode.AllowCertificate, endpoint.ClientCertificateMode);
}

[Fact]
public void ReadEndpointDefaultsWithNoAllowCertificateSettings_ReturnsCorrectValue()
{
var config = new ConfigurationBuilder().Build();
var reader = new ConfigurationReader(config);

var endpoint = reader.EndpointDefaults;
Assert.Null(endpoint.ClientCertificateMode);
}
}
}
130 changes: 130 additions & 0 deletions src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,136 @@ public void DefaultEndpointConfigureSection_ConfigureHttpsDefaultsCanOverrideSsl
Assert.True(ran1);
}

[Fact]
public void EndpointConfigureSection_CanSetClientCertificateMode()
{
var serverOptions = CreateServerOptions();
var ranDefault = false;

serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();

// Kestrel default
Assert.Equal(ClientCertificateMode.NoCertificate, opt.ClientCertificateMode);
ranDefault = true;
});

var ran1 = false;
var ran2 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:ClientCertificateMode", "AllowCertificate"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.HttpsOptions.ClientCertificateMode);
ran1 = true;
})
.Load();
serverOptions.ListenAnyIP(0, opt =>
{
opt.UseHttps(httpsOptions =>
{
// Kestrel default.
Assert.Equal(ClientCertificateMode.NoCertificate, httpsOptions.ClientCertificateMode);
ran2 = true;
});
});

Assert.True(ranDefault);
Assert.True(ran1);
Assert.True(ran2);
}

[Fact]
public void EndpointConfigureSection_CanOverrideClientCertificateModeFromConfigureHttpsDefaults()
{
var serverOptions = CreateServerOptions();

serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});

var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:End1:ClientCertificateMode", "AllowCertificate"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.HttpsOptions.ClientCertificateMode);
ran1 = true;
})
.Load();

Assert.True(ran1);
}

[Fact]
public void DefaultEndpointConfigureSection_CanSetClientCertificateMode()
{
var serverOptions = CreateServerOptions();

serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();
});

var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", "AllowCertificate"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.HttpsOptions.ClientCertificateMode);
ran1 = true;
})
.Load();

Assert.True(ran1);
}


[Fact]
public void DefaultEndpointConfigureSection_ConfigureHttpsDefaultsCanOverrideClientCertificateMode()
{
var serverOptions = CreateServerOptions();

serverOptions.ConfigureHttpsDefaults(opt =>
{
opt.ServerCertificate = TestResources.GetTestCertificate();

Assert.Equal(ClientCertificateMode.AllowCertificate, opt.ClientCertificateMode);
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});

var ran1 = false;
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", "AllowCertificate"),
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
}).Build();
serverOptions.Configure(config)
.Endpoint("End1", opt =>
{
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
ran1 = true;
})
.Load();

Assert.True(ran1);
}

[Fact]
public void Reload_IdentifiesEndpointsToStartAndStop()
{
Expand Down