Skip to content

Commit

Permalink
Load ClientCertificateMode from config #18660 (#24076)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kahbazi committed Jul 23, 2020
1 parent 7a9707e commit 8b79720
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 6 deletions.
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 @@ public ConfigurationReader(IConfiguration configuration)
// "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

0 comments on commit 8b79720

Please sign in to comment.