From 8b7972064f068f76452eff7994263a5e4ee3ca46 Mon Sep 17 00:00:00 2001 From: Kahbazi Date: Thu, 23 Jul 2020 06:27:06 +0430 Subject: [PATCH] Load ClientCertificateMode from config #18660 (#24076) --- .../Core/src/Internal/ConfigurationReader.cs | 28 +++- .../Core/src/KestrelConfigurationLoader.cs | 6 + .../Kestrel/test/ConfigurationReaderTests.cs | 48 ++++++- .../test/KestrelConfigurationLoaderTests.cs | 130 ++++++++++++++++++ 4 files changed, 206 insertions(+), 6 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs index 2843bb014c..894cd0fa01 100644 --- a/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs +++ b/src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs @@ -5,6 +5,7 @@ using System; 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 @@ -18,6 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal private const string EndpointDefaultsKey = "EndpointDefaults"; private const string EndpointsKey = "Endpoints"; private const string UrlKey = "Url"; + private const string ClientCertificateModeKey = "ClientCertificateMode"; private readonly IConfiguration _configuration; @@ -50,6 +52,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal // "EndpointDefaults": { // "Protocols": "Http1AndHttp2", // "SslProtocols": [ "Tls11", "Tls12", "Tls13"], + // "ClientCertificateMode" : "NoCertificate" // } private EndpointDefaults ReadEndpointDefaults() { @@ -57,7 +60,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal return new EndpointDefaults { Protocols = ParseProtocols(configSection[ProtocolsKey]), - SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey)) + SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey)), + ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey]) }; } @@ -75,7 +79,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal // "Certificate": { // "Path": "testCert.pfx", // "Password": "testPassword" - // } + // }, + // "ClientCertificateMode" : "NoCertificate" // } var url = endpointConfig[UrlKey]; @@ -91,7 +96,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal 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); @@ -100,6 +106,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal return endpoints; } + private ClientCertificateMode? ParseClientCertificateMode(string clientCertificateMode) + { + if (Enum.TryParse(clientCertificateMode, ignoreCase: true, out var result)) + { + return result; + } + + return null; + } + private static HttpProtocols? ParseProtocols(string protocols) { if (Enum.TryParse(protocols, ignoreCase: true, out var result)) @@ -129,11 +145,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal // "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": { @@ -143,7 +161,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal // "Certificate": { // "Path": "testCert.pfx", // "Password": "testPassword" - // } + // }, + // "ClientCertificateMode" : "NoCertificate" // } internal class EndpointConfig { @@ -155,6 +174,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal 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 callback. // We cannot rely entirely on comparing config sections for equality, because KestrelConfigurationLoader.Reload() sets diff --git a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs index 04349031e2..86e63ddc6a 100644 --- a/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs +++ b/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs @@ -280,6 +280,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel if (https) { httpsOptions.SslProtocols = ConfigurationReader.EndpointDefaults.SslProtocols ?? SslProtocols.None; + httpsOptions.ClientCertificateMode = ConfigurationReader.EndpointDefaults.ClientCertificateMode ?? ClientCertificateMode.NoCertificate; // Defaults Options.ApplyHttpsDefaults(httpsOptions); @@ -289,6 +290,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel httpsOptions.SslProtocols = endpoint.SslProtocols.Value; } + if (endpoint.ClientCertificateMode.HasValue) + { + httpsOptions.ClientCertificateMode = endpoint.ClientCertificateMode.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 06345cc462..6b24b86b62 100644 --- a/src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs @@ -7,6 +7,7 @@ using System.Linq; 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; @@ -92,7 +93,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests [Fact] public void ReadCertificatesSection_ThrowsOnCaseInsensitiveDuplicate() { - var exception = Assert.Throws(() => + var exception = Assert.Throws(() => new ConfigurationBuilder().AddInMemoryCollection(new[] { new KeyValuePair("Certificates:filecert:Password", "certpassword"), @@ -154,10 +155,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests { new KeyValuePair("Endpoints:End1:Url", "http://*:5001"), new KeyValuePair("Endpoints:End2:Url", "https://*:5002"), + new KeyValuePair("Endpoints:End2:ClientCertificateMode", "AllowCertificate"), new KeyValuePair("Endpoints:End3:Url", "https://*:5003"), + new KeyValuePair("Endpoints:End3:ClientCertificateMode", "RequireCertificate"), new KeyValuePair("Endpoints:End3:Certificate:Path", "/path/cert.pfx"), new KeyValuePair("Endpoints:End3:Certificate:Password", "certpassword"), new KeyValuePair("Endpoints:End4:Url", "https://*:5004"), + new KeyValuePair("Endpoints:End4:ClientCertificateMode", "NoCertificate"), new KeyValuePair("Endpoints:End4:Certificate:Subject", "certsubject"), new KeyValuePair("Endpoints:End4:Certificate:Store", "certstore"), new KeyValuePair("Endpoints:End4:Certificate:Location", "cetlocation"), @@ -171,6 +175,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests 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()); @@ -178,6 +183,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests 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()); @@ -185,6 +191,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests 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()); @@ -197,6 +204,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests 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()); @@ -235,7 +243,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests 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] @@ -287,5 +295,41 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests var endpoint = reader.EndpointDefaults; Assert.Null(endpoint.SslProtocols); } + + [Fact] + public void ReadEndpointWithNoClientCertificateModeSettings_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.ClientCertificateMode); + } + + [Fact] + public void ReadEndpointDefaultsWithClientCertificateModeSet_ReturnsCorrectValue() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("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); + } } } diff --git a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs index 5053b3cffb..2e71cdb223 100644 --- a/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs +++ b/src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs @@ -704,6 +704,136 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Tests 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("Endpoints:End1:ClientCertificateMode", "AllowCertificate"), + new KeyValuePair("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("Endpoints:End1:ClientCertificateMode", "AllowCertificate"), + new KeyValuePair("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("EndpointDefaults:ClientCertificateMode", "AllowCertificate"), + new KeyValuePair("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("EndpointDefaults:ClientCertificateMode", "AllowCertificate"), + new KeyValuePair("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() {