Skip to content

Commit 8b79720

Browse files
authored
Load ClientCertificateMode from config #18660 (#24076)
1 parent 7a9707e commit 8b79720

File tree

4 files changed

+206
-6
lines changed

4 files changed

+206
-6
lines changed

src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Security.Authentication;
8+
using Microsoft.AspNetCore.Server.Kestrel.Https;
89
using Microsoft.Extensions.Configuration;
910

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

2224
private readonly IConfiguration _configuration;
2325

@@ -50,14 +52,16 @@ private IDictionary<string, CertificateConfig> ReadCertificates()
5052
// "EndpointDefaults": {
5153
// "Protocols": "Http1AndHttp2",
5254
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
55+
// "ClientCertificateMode" : "NoCertificate"
5356
// }
5457
private EndpointDefaults ReadEndpointDefaults()
5558
{
5659
var configSection = _configuration.GetSection(EndpointDefaultsKey);
5760
return new EndpointDefaults
5861
{
5962
Protocols = ParseProtocols(configSection[ProtocolsKey]),
60-
SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey))
63+
SslProtocols = ParseSslProcotols(configSection.GetSection(SslProtocolsKey)),
64+
ClientCertificateMode = ParseClientCertificateMode(configSection[ClientCertificateModeKey])
6165
};
6266
}
6367

@@ -75,7 +79,8 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
7579
// "Certificate": {
7680
// "Path": "testCert.pfx",
7781
// "Password": "testPassword"
78-
// }
82+
// },
83+
// "ClientCertificateMode" : "NoCertificate"
7984
// }
8085

8186
var url = endpointConfig[UrlKey];
@@ -91,7 +96,8 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
9196
Protocols = ParseProtocols(endpointConfig[ProtocolsKey]),
9297
ConfigSection = endpointConfig,
9398
Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)),
94-
SslProtocols = ParseSslProcotols(endpointConfig.GetSection(SslProtocolsKey))
99+
SslProtocols = ParseSslProcotols(endpointConfig.GetSection(SslProtocolsKey)),
100+
ClientCertificateMode = ParseClientCertificateMode(endpointConfig[ClientCertificateModeKey])
95101
};
96102

97103
endpoints.Add(endpoint);
@@ -100,6 +106,16 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
100106
return endpoints;
101107
}
102108

109+
private ClientCertificateMode? ParseClientCertificateMode(string clientCertificateMode)
110+
{
111+
if (Enum.TryParse<ClientCertificateMode>(clientCertificateMode, ignoreCase: true, out var result))
112+
{
113+
return result;
114+
}
115+
116+
return null;
117+
}
118+
103119
private static HttpProtocols? ParseProtocols(string protocols)
104120
{
105121
if (Enum.TryParse<HttpProtocols>(protocols, ignoreCase: true, out var result))
@@ -129,11 +145,13 @@ private IEnumerable<EndpointConfig> ReadEndpoints()
129145
// "EndpointDefaults": {
130146
// "Protocols": "Http1AndHttp2",
131147
// "SslProtocols": [ "Tls11", "Tls12", "Tls13"],
148+
// "ClientCertificateMode" : "NoCertificate"
132149
// }
133150
internal class EndpointDefaults
134151
{
135152
public HttpProtocols? Protocols { get; set; }
136153
public SslProtocols? SslProtocols { get; set; }
154+
public ClientCertificateMode? ClientCertificateMode { get; set; }
137155
}
138156

139157
// "EndpointName": {
@@ -143,7 +161,8 @@ internal class EndpointDefaults
143161
// "Certificate": {
144162
// "Path": "testCert.pfx",
145163
// "Password": "testPassword"
146-
// }
164+
// },
165+
// "ClientCertificateMode" : "NoCertificate"
147166
// }
148167
internal class EndpointConfig
149168
{
@@ -155,6 +174,7 @@ internal class EndpointConfig
155174
public HttpProtocols? Protocols { get; set; }
156175
public SslProtocols? SslProtocols { get; set; }
157176
public CertificateConfig Certificate { get; set; }
177+
public ClientCertificateMode? ClientCertificateMode { get; set; }
158178

159179
// Compare config sections because it's accessible to app developers via an Action<EndpointConfiguration> callback.
160180
// We cannot rely entirely on comparing config sections for equality, because KestrelConfigurationLoader.Reload() sets

src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ public void Load()
280280
if (https)
281281
{
282282
httpsOptions.SslProtocols = ConfigurationReader.EndpointDefaults.SslProtocols ?? SslProtocols.None;
283+
httpsOptions.ClientCertificateMode = ConfigurationReader.EndpointDefaults.ClientCertificateMode ?? ClientCertificateMode.NoCertificate;
283284

284285
// Defaults
285286
Options.ApplyHttpsDefaults(httpsOptions);
@@ -289,6 +290,11 @@ public void Load()
289290
httpsOptions.SslProtocols = endpoint.SslProtocols.Value;
290291
}
291292

293+
if (endpoint.ClientCertificateMode.HasValue)
294+
{
295+
httpsOptions.ClientCertificateMode = endpoint.ClientCertificateMode.Value;
296+
}
297+
292298
// Specified
293299
httpsOptions.ServerCertificate = LoadCertificate(endpoint.Certificate, endpoint.Name)
294300
?? httpsOptions.ServerCertificate;

src/Servers/Kestrel/Kestrel/test/ConfigurationReaderTests.cs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Security.Authentication;
88
using Microsoft.AspNetCore.Server.Kestrel.Core;
99
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
10+
using Microsoft.AspNetCore.Server.Kestrel.Https;
1011
using Microsoft.Extensions.Configuration;
1112
using Xunit;
1213

@@ -92,7 +93,7 @@ public void ReadCertificatesSection_IsCaseInsensitive()
9293
[Fact]
9394
public void ReadCertificatesSection_ThrowsOnCaseInsensitiveDuplicate()
9495
{
95-
var exception = Assert.Throws<ArgumentException>(() =>
96+
var exception = Assert.Throws<ArgumentException>(() =>
9697
new ConfigurationBuilder().AddInMemoryCollection(new[]
9798
{
9899
new KeyValuePair<string, string>("Certificates:filecert:Password", "certpassword"),
@@ -154,10 +155,13 @@ public void ReadEndpointsSection_ReturnsCollection()
154155
{
155156
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
156157
new KeyValuePair<string, string>("Endpoints:End2:Url", "https://*:5002"),
158+
new KeyValuePair<string, string>("Endpoints:End2:ClientCertificateMode", "AllowCertificate"),
157159
new KeyValuePair<string, string>("Endpoints:End3:Url", "https://*:5003"),
160+
new KeyValuePair<string, string>("Endpoints:End3:ClientCertificateMode", "RequireCertificate"),
158161
new KeyValuePair<string, string>("Endpoints:End3:Certificate:Path", "/path/cert.pfx"),
159162
new KeyValuePair<string, string>("Endpoints:End3:Certificate:Password", "certpassword"),
160163
new KeyValuePair<string, string>("Endpoints:End4:Url", "https://*:5004"),
164+
new KeyValuePair<string, string>("Endpoints:End4:ClientCertificateMode", "NoCertificate"),
161165
new KeyValuePair<string, string>("Endpoints:End4:Certificate:Subject", "certsubject"),
162166
new KeyValuePair<string, string>("Endpoints:End4:Certificate:Store", "certstore"),
163167
new KeyValuePair<string, string>("Endpoints:End4:Certificate:Location", "cetlocation"),
@@ -171,20 +175,23 @@ public void ReadEndpointsSection_ReturnsCollection()
171175
var end1 = endpoints.First();
172176
Assert.Equal("End1", end1.Name);
173177
Assert.Equal("http://*:5001", end1.Url);
178+
Assert.Null(end1.ClientCertificateMode);
174179
Assert.NotNull(end1.ConfigSection);
175180
Assert.NotNull(end1.Certificate);
176181
Assert.False(end1.Certificate.ConfigSection.Exists());
177182

178183
var end2 = endpoints.Skip(1).First();
179184
Assert.Equal("End2", end2.Name);
180185
Assert.Equal("https://*:5002", end2.Url);
186+
Assert.Equal(ClientCertificateMode.AllowCertificate, end2.ClientCertificateMode);
181187
Assert.NotNull(end2.ConfigSection);
182188
Assert.NotNull(end2.Certificate);
183189
Assert.False(end2.Certificate.ConfigSection.Exists());
184190

185191
var end3 = endpoints.Skip(2).First();
186192
Assert.Equal("End3", end3.Name);
187193
Assert.Equal("https://*:5003", end3.Url);
194+
Assert.Equal(ClientCertificateMode.RequireCertificate, end3.ClientCertificateMode);
188195
Assert.NotNull(end3.ConfigSection);
189196
Assert.NotNull(end3.Certificate);
190197
Assert.True(end3.Certificate.ConfigSection.Exists());
@@ -197,6 +204,7 @@ public void ReadEndpointsSection_ReturnsCollection()
197204
var end4 = endpoints.Skip(3).First();
198205
Assert.Equal("End4", end4.Name);
199206
Assert.Equal("https://*:5004", end4.Url);
207+
Assert.Equal(ClientCertificateMode.NoCertificate, end4.ClientCertificateMode);
200208
Assert.NotNull(end4.ConfigSection);
201209
Assert.NotNull(end4.Certificate);
202210
Assert.True(end4.Certificate.ConfigSection.Exists());
@@ -235,7 +243,7 @@ public void ReadEndpointWithMultipleSslProtocolsSet_ReturnsCorrectValue()
235243
var reader = new ConfigurationReader(config);
236244

237245
var endpoint = reader.Endpoints.First();
238-
Assert.Equal(SslProtocols.Tls11|SslProtocols.Tls12, endpoint.SslProtocols);
246+
Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, endpoint.SslProtocols);
239247
}
240248

241249
[Fact]
@@ -287,5 +295,41 @@ public void ReadEndpointDefaultsWithNoSslProtocolSettings_ReturnsCorrectValue()
287295
var endpoint = reader.EndpointDefaults;
288296
Assert.Null(endpoint.SslProtocols);
289297
}
298+
299+
[Fact]
300+
public void ReadEndpointWithNoClientCertificateModeSettings_ReturnsNull()
301+
{
302+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
303+
{
304+
new KeyValuePair<string, string>("Endpoints:End1:Url", "http://*:5001"),
305+
}).Build();
306+
var reader = new ConfigurationReader(config);
307+
308+
var endpoint = reader.Endpoints.First();
309+
Assert.Null(endpoint.ClientCertificateMode);
310+
}
311+
312+
[Fact]
313+
public void ReadEndpointDefaultsWithClientCertificateModeSet_ReturnsCorrectValue()
314+
{
315+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
316+
{
317+
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", "AllowCertificate"),
318+
}).Build();
319+
var reader = new ConfigurationReader(config);
320+
321+
var endpoint = reader.EndpointDefaults;
322+
Assert.Equal(ClientCertificateMode.AllowCertificate, endpoint.ClientCertificateMode);
323+
}
324+
325+
[Fact]
326+
public void ReadEndpointDefaultsWithNoAllowCertificateSettings_ReturnsCorrectValue()
327+
{
328+
var config = new ConfigurationBuilder().Build();
329+
var reader = new ConfigurationReader(config);
330+
331+
var endpoint = reader.EndpointDefaults;
332+
Assert.Null(endpoint.ClientCertificateMode);
333+
}
290334
}
291335
}

src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,136 @@ public void DefaultEndpointConfigureSection_ConfigureHttpsDefaultsCanOverrideSsl
704704
Assert.True(ran1);
705705
}
706706

707+
[Fact]
708+
public void EndpointConfigureSection_CanSetClientCertificateMode()
709+
{
710+
var serverOptions = CreateServerOptions();
711+
var ranDefault = false;
712+
713+
serverOptions.ConfigureHttpsDefaults(opt =>
714+
{
715+
opt.ServerCertificate = TestResources.GetTestCertificate();
716+
717+
// Kestrel default
718+
Assert.Equal(ClientCertificateMode.NoCertificate, opt.ClientCertificateMode);
719+
ranDefault = true;
720+
});
721+
722+
var ran1 = false;
723+
var ran2 = false;
724+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
725+
{
726+
new KeyValuePair<string, string>("Endpoints:End1:ClientCertificateMode", "AllowCertificate"),
727+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
728+
}).Build();
729+
serverOptions.Configure(config)
730+
.Endpoint("End1", opt =>
731+
{
732+
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.HttpsOptions.ClientCertificateMode);
733+
ran1 = true;
734+
})
735+
.Load();
736+
serverOptions.ListenAnyIP(0, opt =>
737+
{
738+
opt.UseHttps(httpsOptions =>
739+
{
740+
// Kestrel default.
741+
Assert.Equal(ClientCertificateMode.NoCertificate, httpsOptions.ClientCertificateMode);
742+
ran2 = true;
743+
});
744+
});
745+
746+
Assert.True(ranDefault);
747+
Assert.True(ran1);
748+
Assert.True(ran2);
749+
}
750+
751+
[Fact]
752+
public void EndpointConfigureSection_CanOverrideClientCertificateModeFromConfigureHttpsDefaults()
753+
{
754+
var serverOptions = CreateServerOptions();
755+
756+
serverOptions.ConfigureHttpsDefaults(opt =>
757+
{
758+
opt.ServerCertificate = TestResources.GetTestCertificate();
759+
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
760+
});
761+
762+
var ran1 = false;
763+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
764+
{
765+
new KeyValuePair<string, string>("Endpoints:End1:ClientCertificateMode", "AllowCertificate"),
766+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
767+
}).Build();
768+
serverOptions.Configure(config)
769+
.Endpoint("End1", opt =>
770+
{
771+
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.HttpsOptions.ClientCertificateMode);
772+
ran1 = true;
773+
})
774+
.Load();
775+
776+
Assert.True(ran1);
777+
}
778+
779+
[Fact]
780+
public void DefaultEndpointConfigureSection_CanSetClientCertificateMode()
781+
{
782+
var serverOptions = CreateServerOptions();
783+
784+
serverOptions.ConfigureHttpsDefaults(opt =>
785+
{
786+
opt.ServerCertificate = TestResources.GetTestCertificate();
787+
});
788+
789+
var ran1 = false;
790+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
791+
{
792+
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", "AllowCertificate"),
793+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
794+
}).Build();
795+
serverOptions.Configure(config)
796+
.Endpoint("End1", opt =>
797+
{
798+
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.HttpsOptions.ClientCertificateMode);
799+
ran1 = true;
800+
})
801+
.Load();
802+
803+
Assert.True(ran1);
804+
}
805+
806+
807+
[Fact]
808+
public void DefaultEndpointConfigureSection_ConfigureHttpsDefaultsCanOverrideClientCertificateMode()
809+
{
810+
var serverOptions = CreateServerOptions();
811+
812+
serverOptions.ConfigureHttpsDefaults(opt =>
813+
{
814+
opt.ServerCertificate = TestResources.GetTestCertificate();
815+
816+
Assert.Equal(ClientCertificateMode.AllowCertificate, opt.ClientCertificateMode);
817+
opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
818+
});
819+
820+
var ran1 = false;
821+
var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
822+
{
823+
new KeyValuePair<string, string>("EndpointDefaults:ClientCertificateMode", "AllowCertificate"),
824+
new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
825+
}).Build();
826+
serverOptions.Configure(config)
827+
.Endpoint("End1", opt =>
828+
{
829+
Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode);
830+
ran1 = true;
831+
})
832+
.Load();
833+
834+
Assert.True(ran1);
835+
}
836+
707837
[Fact]
708838
public void Reload_IdentifiesEndpointsToStartAndStop()
709839
{

0 commit comments

Comments
 (0)