Skip to content

Commit e00b3e3

Browse files
committed
Add support for loading certificate chains from configuration.
- Adds a ServerCertificateIntermediates property to HttpsConnectionAdapterOptions - Adds a Chain property to configuration. This only supports PEM certificates. - Import intermediates if chain path specified
1 parent d85ac16 commit e00b3e3

8 files changed

+47
-32
lines changed

src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public HttpsConnectionAdapterOptions()
3939
/// </summary>
4040
public X509Certificate2 ServerCertificate { get; set; }
4141

42+
/// <summary>
43+
/// Specifies the intermediate certificates in the chain.
44+
/// </summary>
45+
public X509Certificate2Collection ServerCertificateIntermediates { get; set; }
46+
4247
/// <summary>
4348
/// <para>
4449
/// A callback that will be invoked to dynamically select a server certificate. This is higher priority than ServerCertificate.

src/Servers/Kestrel/Core/src/Internal/Certificates/CertificateConfigLoader.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,11 @@ public CertificateConfigLoader(IHostEnvironment hostEnvironment, ILogger<Kestrel
2323
public IHostEnvironment HostEnvironment { get; }
2424
public ILogger<KestrelServer> Logger { get; }
2525

26-
public bool IsTestMock => false;
27-
28-
public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
26+
public (X509Certificate2, X509Certificate2Collection) LoadCertificate(CertificateConfig certInfo, string endpointName)
2927
{
3028
if (certInfo is null)
3129
{
32-
return null;
30+
return (null, null);
3331
}
3432

3533
if (certInfo.IsFileCert && certInfo.IsStoreCert)
@@ -39,9 +37,21 @@ public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpo
3937
else if (certInfo.IsFileCert)
4038
{
4139
var certificatePath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path);
40+
41+
X509Certificate2Collection intermediates = null;
42+
43+
if (certInfo.ChainPath != null)
44+
{
45+
var certificateChainPath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.ChainPath);
46+
47+
intermediates = new X509Certificate2Collection();
48+
intermediates.ImportFromPemFile(certificateChainPath);
49+
}
50+
4251
if (certInfo.KeyPath != null)
4352
{
4453
var certificateKeyPath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.KeyPath);
54+
4555
var certificate = GetCertificate(certificatePath);
4656

4757
if (certificate != null)
@@ -57,10 +67,10 @@ public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpo
5767
{
5868
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
5969
{
60-
return PersistKey(certificate);
70+
return (PersistKey(certificate), intermediates);
6171
}
6272

63-
return certificate;
73+
return (certificate, intermediates);
6474
}
6575
else
6676
{
@@ -70,14 +80,14 @@ public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpo
7080
throw new InvalidOperationException(CoreStrings.InvalidPemKey);
7181
}
7282

73-
return new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path), certInfo.Password);
83+
return (new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path), certInfo.Password), intermediates);
7484
}
7585
else if (certInfo.IsStoreCert)
7686
{
77-
return LoadFromStoreCert(certInfo);
87+
return (LoadFromStoreCert(certInfo), null);
7888
}
7989

80-
return null;
90+
return (null, null);
8191
}
8292

8393
private static X509Certificate2 PersistKey(X509Certificate2 fullCertificate)

src/Servers/Kestrel/Core/src/Internal/Certificates/ICertificateConfigLoader.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates
77
{
88
internal interface ICertificateConfigLoader
99
{
10-
bool IsTestMock { get; }
11-
12-
X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName);
10+
(X509Certificate2, X509Certificate2Collection) LoadCertificate(CertificateConfig certInfo, string endpointName);
1311
}
1412
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ public override int GetHashCode() => HashCode.Combine(
346346

347347
// "CertificateName": {
348348
// "Path": "testCert.pfx",
349+
// "KeyPath": "",
350+
// "ChainPath": "",
349351
// "Password": "testPassword"
350352
// }
351353
internal class CertificateConfig
@@ -368,6 +370,8 @@ internal CertificateConfig()
368370

369371
public string Path { get; set; }
370372

373+
public string ChainPath { get; set; }
374+
371375
public string KeyPath { get; set; }
372376

373377
public string Password { get; set; }

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,13 @@ public SniOptionsSelector(
4949
{
5050
var sslOptions = new SslServerAuthenticationOptions
5151
{
52-
ServerCertificate = certifcateConfigLoader.LoadCertificate(sniConfig.Certificate, $"{endpointName}:Sni:{name}"),
5352
EnabledSslProtocols = sniConfig.SslProtocols ?? fallbackHttpsOptions.SslProtocols,
5453
CertificateRevocationCheckMode = fallbackHttpsOptions.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
5554
};
5655

57-
if (sslOptions.ServerCertificate is null)
56+
var (serverCert, intermediates) = certifcateConfigLoader.LoadCertificate(sniConfig.Certificate, $"{endpointName}:Sni:{name}");
57+
58+
if (serverCert is null)
5859
{
5960
if (fallbackHttpsOptions.ServerCertificate is null && _fallbackServerCertificateSelector is null)
6061
{
@@ -63,21 +64,18 @@ public SniOptionsSelector(
6364

6465
if (_fallbackServerCertificateSelector is null)
6566
{
66-
// Cache the fallback ServerCertificate since there's no fallback ServerCertificateSelector taking precedence.
67-
sslOptions.ServerCertificate = fallbackHttpsOptions.ServerCertificate;
67+
// Cache the fallback ServerCertificate since there's no fallback ServerCertificateSelector taking precedence.
68+
serverCert = fallbackHttpsOptions.ServerCertificate;
69+
intermediates = fallbackHttpsOptions.ServerCertificateIntermediates;
6870
}
6971
}
7072

71-
if (sslOptions.ServerCertificate != null)
73+
if (serverCert != null)
7274
{
7375
// This might be do blocking IO but it'll resolve the certificate chain up front before any connections are
7476
// made to the server
75-
sslOptions.ServerCertificateContext = SslStreamCertificateContext.Create((X509Certificate2)sslOptions.ServerCertificate, additionalCertificates: null);
76-
}
77-
78-
if (!certifcateConfigLoader.IsTestMock && sslOptions.ServerCertificate is X509Certificate2 cert2)
79-
{
80-
HttpsConnectionMiddleware.EnsureCertificateIsAllowedForServerAuth(cert2);
77+
sslOptions.ServerCertificate = serverCert;
78+
sslOptions.ServerCertificateContext = SslStreamCertificateContext.Create(serverCert, intermediates);
8179
}
8280

8381
var clientCertificateMode = sniConfig.ClientCertificateMode ?? fallbackHttpsOptions.ClientCertificateMode;

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,10 @@ public void Load()
342342
}
343343

344344
// A cert specified directly on the endpoint overrides any defaults.
345-
httpsOptions.ServerCertificate = CertificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name)
346-
?? httpsOptions.ServerCertificate;
345+
var (serverCert, intermediates) = CertificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name);
346+
347+
httpsOptions.ServerCertificate = serverCert ?? httpsOptions.ServerCertificate;
348+
httpsOptions.ServerCertificateIntermediates = intermediates ?? httpsOptions.ServerCertificateIntermediates;
347349

348350
if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
349351
{
@@ -404,7 +406,7 @@ private void LoadDefaultCert()
404406
{
405407
if (ConfigurationReader.Certificates.TryGetValue("Default", out var defaultCertConfig))
406408
{
407-
var defaultCert = CertificateConfigLoader.LoadCertificate(defaultCertConfig, "Default");
409+
var (defaultCert, intermediates) = CertificateConfigLoader.LoadCertificate(defaultCertConfig, "Default");
408410
if (defaultCert != null)
409411
{
410412
DefaultCertificateConfig = defaultCertConfig;

src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapter
9393

9494
// This might be do blocking IO but it'll resolve the certificate chain up front before any connections are
9595
// made to the server
96-
_serverCertificateContext = SslStreamCertificateContext.Create(_serverCertificate, additionalCertificates: null);
96+
_serverCertificateContext = SslStreamCertificateContext.Create(_serverCertificate, additionalCertificates: options.ServerCertificateIntermediates);
9797
}
9898

9999
var remoteCertificateValidationCallback = _options.ClientCertificateMode == ClientCertificateMode.NoCertificate ?

src/Servers/Kestrel/Core/test/SniOptionsSelectorTests.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -751,18 +751,16 @@ private class MockCertificateConfigLoader : ICertificateConfigLoader
751751
{
752752
public Dictionary<object, string> CertToPathDictionary { get; } = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
753753

754-
public bool IsTestMock => true;
755-
756-
public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
754+
public (X509Certificate2, X509Certificate2Collection) LoadCertificate(CertificateConfig certInfo, string endpointName)
757755
{
758756
if (certInfo is null)
759757
{
760-
return null;
758+
return (null, null);
761759
}
762760

763761
var cert = TestResources.GetTestCertificate();
764762
CertToPathDictionary.Add(cert, certInfo.Path);
765-
return cert;
763+
return (cert, null);
766764
}
767765
}
768766

0 commit comments

Comments
 (0)