diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs index 0acb8b70c49c..8d5281847077 100644 --- a/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs +++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs @@ -212,6 +212,8 @@ private X509ChainPolicy BuildChainPolicy(X509Certificate2 certificate) chainPolicy.TrustMode = Options.ChainTrustValidationMode; } + chainPolicy.ExtraStore.AddRange(Options.AdditionalChainCertificates); + if (!Options.ValidateValidityPeriod) { chainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreNotTimeValid; diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs index d533e5a978f0..417ae86e669a 100644 --- a/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs +++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs @@ -22,6 +22,11 @@ public class CertificateAuthenticationOptions : AuthenticationSchemeOptions /// Collection of X509 certificates which are trusted components of the certificate chain. /// public X509Certificate2Collection CustomTrustStore { get; set; } = new X509Certificate2Collection(); + + /// + /// Collection of X509 certificates which are added to the X509Chain.ChainPolicy.ExtraStore of the certificate chain. + /// + public X509Certificate2Collection AdditionalChainCertificates { get; set; } = new X509Certificate2Collection(); /// /// Method used to validate certificate chains against . diff --git a/src/Security/Authentication/Certificate/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/Certificate/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..ee3ca4344c56 100644 --- a/src/Security/Authentication/Certificate/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/Certificate/src/PublicAPI.Unshipped.txt @@ -1 +1,3 @@ #nullable enable +Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions.AdditionalChainCertificates.get -> System.Security.Cryptography.X509Certificates.X509Certificate2Collection! +Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions.AdditionalChainCertificates.set -> void diff --git a/src/Security/Authentication/test/CertificateTests.cs b/src/Security/Authentication/test/CertificateTests.cs index 7ece7586b8d4..fc1dd46e0b1c 100644 --- a/src/Security/Authentication/test/CertificateTests.cs +++ b/src/Security/Authentication/test/CertificateTests.cs @@ -319,6 +319,41 @@ public async Task VerifyValidClientCertWithTrustedChainAuthenticates() Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + [Fact] + public async Task VerifyValidClientCertWithAdditionalCertificatesAuthenticates() + { + using var host = await CreateHost( + new CertificateAuthenticationOptions + { + Events = successfulValidationEvents, + ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust, + CustomTrustStore = new X509Certificate2Collection() { Certificates.SelfSignedPrimaryRoot, }, + AdditionalChainCertificates = new X509Certificate2Collection() { Certificates.SignedSecondaryRoot }, + RevocationMode = X509RevocationMode.NoCheck + }, Certificates.SignedClient); + + using var server = host.GetTestServer(); + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task VerifyValidClientCertFailsWithoutAdditionalCertificatesAuthenticates() + { + using var host = await CreateHost( + new CertificateAuthenticationOptions + { + Events = successfulValidationEvents, + ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust, + CustomTrustStore = new X509Certificate2Collection() { Certificates.SelfSignedPrimaryRoot, }, + RevocationMode = X509RevocationMode.NoCheck + }, Certificates.SignedClient); + + using var server = host.GetTestServer(); + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + [Fact] public async Task VerifyHeaderIsUsedIfCertIsNotPresent() { @@ -570,7 +605,7 @@ public async Task VerifyValidationResultCanBeCached(bool cache) Assert.Equal(Expected, name.First().Value); count = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "ValidationCount"); Assert.Single(count); - var expected = cache ? "1" : "2"; + var expected = cache ? "1" : "2"; Assert.Equal(expected, count.First().Value); } @@ -693,6 +728,7 @@ private static async Task CreateHost( options.RevocationFlag = configureOptions.RevocationFlag; options.RevocationMode = configureOptions.RevocationMode; options.ValidateValidityPeriod = configureOptions.ValidateValidityPeriod; + options.AdditionalChainCertificates = configureOptions.AdditionalChainCertificates; }); } else