Skip to content

Commit 339bf3a

Browse files
davidfowldougbu
authored andcommitted
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 46d58a5 commit 339bf3a

File tree

8 files changed

+47
-32
lines changed

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
@@ -24,13 +24,11 @@ public CertificateConfigLoader(IHostEnvironment hostEnvironment, ILogger<Kestrel
2424
public IHostEnvironment HostEnvironment { get; }
2525
public ILogger<KestrelServer> Logger { get; }
2626

27-
public bool IsTestMock => false;
28-
29-
public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
27+
public (X509Certificate2, X509Certificate2Collection) LoadCertificate(CertificateConfig certInfo, string endpointName)
3028
{
3129
if (certInfo is null)
3230
{
33-
return null;
31+
return (null, null);
3432
}
3533

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

4858
if (certificate != null)
@@ -58,10 +68,10 @@ public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpo
5868
{
5969
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
6070
{
61-
return PersistKey(certificate);
71+
return (PersistKey(certificate), intermediates);
6272
}
6373

64-
return certificate;
74+
return (certificate, intermediates);
6575
}
6676
else
6777
{
@@ -71,14 +81,14 @@ public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpo
7181
throw new InvalidOperationException(CoreStrings.InvalidPemKey);
7282
}
7383

74-
return new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path), certInfo.Password);
84+
return (new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path), certInfo.Password), intermediates);
7585
}
7686
else if (certInfo.IsStoreCert)
7787
{
78-
return LoadFromStoreCert(certInfo);
88+
return (LoadFromStoreCert(certInfo), null);
7989
}
8090

81-
return null;
91+
return (null, null);
8292
}
8393

8494
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
@@ -380,6 +382,8 @@ internal CertificateConfig()
380382

381383
public string Path { get; set; }
382384

385+
public string ChainPath { get; set; }
386+
383387
public string KeyPath { get; set; }
384388

385389
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
@@ -355,8 +355,10 @@ public void Load()
355355
}
356356

357357
// A cert specified directly on the endpoint overrides any defaults.
358-
httpsOptions.ServerCertificate = CertificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name)
359-
?? httpsOptions.ServerCertificate;
358+
var (serverCert, intermediates) = CertificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name);
359+
360+
httpsOptions.ServerCertificate = serverCert ?? httpsOptions.ServerCertificate;
361+
httpsOptions.ServerCertificateIntermediates = intermediates ?? httpsOptions.ServerCertificateIntermediates;
360362

361363
if (httpsOptions.ServerCertificate == null && httpsOptions.ServerCertificateSelector == null)
362364
{
@@ -417,7 +419,7 @@ private void LoadDefaultCert()
417419
{
418420
if (ConfigurationReader.Certificates.TryGetValue("Default", out var defaultCertConfig))
419421
{
420-
var defaultCert = CertificateConfigLoader.LoadCertificate(defaultCertConfig, "Default");
422+
var (defaultCert, intermediates) = CertificateConfigLoader.LoadCertificate(defaultCertConfig, "Default");
421423
if (defaultCert != null)
422424
{
423425
DefaultCertificateConfig = defaultCertConfig;

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

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

106106
// This might be do blocking IO but it'll resolve the certificate chain up front before any connections are
107107
// made to the server
108-
_serverCertificateContext = SslStreamCertificateContext.Create(certificate, additionalCertificates: null);
108+
_serverCertificateContext = SslStreamCertificateContext.Create(certificate, additionalCertificates: options.ServerCertificateIntermediates);
109109
}
110110

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