Skip to content

Commit 8bc28a9

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.
1 parent 51b719a commit 8bc28a9

8 files changed

+50
-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: 22 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,24 @@ 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+
if (File.Exists(certificateChainPath))
48+
{
49+
intermediates = new X509Certificate2Collection();
50+
intermediates.ImportFromPemFile(certificateChainPath);
51+
}
52+
}
53+
4254
if (certInfo.KeyPath != null)
4355
{
4456
var certificateKeyPath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.KeyPath);
57+
4558
var certificate = GetCertificate(certificatePath);
4659

4760
if (certificate != null)
@@ -57,10 +70,10 @@ public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpo
5770
{
5871
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
5972
{
60-
return PersistKey(certificate);
73+
return (PersistKey(certificate), intermediates);
6174
}
6275

63-
return certificate;
76+
return (certificate, null);
6477
}
6578
else
6679
{
@@ -70,14 +83,14 @@ public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpo
7083
throw new InvalidOperationException(CoreStrings.InvalidPemKey);
7184
}
7285

73-
return new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path), certInfo.Password);
86+
return (new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path), certInfo.Password), intermediates);
7487
}
7588
else if (certInfo.IsStoreCert)
7689
{
77-
return LoadFromStoreCert(certInfo);
90+
return (LoadFromStoreCert(certInfo), null);
7891
}
7992

80-
return null;
93+
return (null, null);
8194
}
8295

8396
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)