From 6af8d1996696a66af3c0ab54199250b2583f69a5 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Sun, 30 Aug 2026 21:43:50 -0700 Subject: [PATCH 1/7] Add a test --- .../X509Certificates/CertificateAuthority.cs | 126 +++++++++- .../RevocationTests/DynamicRevocationTests.cs | 237 +++++++++++++++++- 2 files changed, 352 insertions(+), 11 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs index ceae2dcb1ae251..258e79517dac2a 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -157,6 +157,17 @@ internal X509Certificate2 CreateSubordinateCA( string subject, PublicKey publicKey, int? depthLimit = null) + { + return CreateSubordinateCA( + new X500DistinguishedName(subject), + publicKey, + depthLimit); + } + + internal X509Certificate2 CreateSubordinateCA( + X500DistinguishedName subject, + PublicKey publicKey, + int? depthLimit = null) { return CreateCertificate( subject, @@ -171,7 +182,22 @@ internal X509Certificate2 CreateSubordinateCA( s_caKeyUsage }); } - internal X509Certificate2 CreateEndEntity(string subject, PublicKey publicKey, X509ExtensionCollection extensions) + internal X509Certificate2 CreateEndEntity( + string subject, + PublicKey publicKey, + X509ExtensionCollection extensions) + { + return CreateCertificate( + new X500DistinguishedName(subject), + publicKey, + TimeSpan.FromSeconds(2), + extensions); + } + + internal X509Certificate2 CreateEndEntity( + X500DistinguishedName subject, + PublicKey publicKey, + X509ExtensionCollection extensions) { return CreateCertificate( subject, @@ -254,6 +280,18 @@ private X509Certificate2 CreateCertificate( TimeSpan nestingBuffer, X509ExtensionCollection extensions, bool ocspResponder = false) + { + X500DistinguishedName name = new(subject); + + return CreateCertificate(name, publicKey, nestingBuffer, extensions, ocspResponder); + } + + private X509Certificate2 CreateCertificate( + X500DistinguishedName subject, + PublicKey publicKey, + TimeSpan nestingBuffer, + X509ExtensionCollection extensions, + bool ocspResponder = false) { if (_cdpExtension == null && CdpUri != null) { @@ -271,7 +309,7 @@ private X509Certificate2 CreateCertificate( } CertificateRequest request = new CertificateRequest( - new X500DistinguishedName(subject), + subject, publicKey, HashAlgorithmIfNeeded(_cert.GetKeyAlgorithm()), RSASignaturePadding.Pkcs1); @@ -807,6 +845,74 @@ internal static void BuildPrivatePki( KeyFactory keyFactory = null, bool forTls = false, X509ExtensionCollection extensions = null) + { + BuildPrivatePkiCore( + pkiOptions, + out responder, + out rootAuthority, + out intermediateAuthorities, + out endEntityCert, + BuildSubject("A Revocation Test Root", testName, pkiOptions, pkiOptionsInSubject), + index => BuildSubject($"A Revocation Test CA {index}", testName, pkiOptions, pkiOptionsInSubject), + intermediateAuthorityCount, + BuildSubject(subjectName ?? "A Revocation Test Cert", testName, pkiOptions, pkiOptionsInSubject), + keyFactoryHashSubjectName: subjectName, + testName, + registerAuthorities, + keyFactory, + forTls, + extensions); + } + + internal static void BuildPrivatePki( + PkiOptions pkiOptions, + out RevocationResponder responder, + X500DistinguishedName rootName, + out CertificateAuthority rootAuthority, + X500DistinguishedName[] intermediateNames, + out CertificateAuthority[] intermediateAuthorities, + X500DistinguishedName endEntityName, + out X509Certificate2 endEntityCert, + string testName = null, + bool registerAuthorities = true, + KeyFactory keyFactory = null, + bool forTls = false, + X509ExtensionCollection extensions = null) + { + BuildPrivatePkiCore( + pkiOptions, + out responder, + out rootAuthority, + out intermediateAuthorities, + out endEntityCert, + rootName, + index => intermediateNames[index], + intermediateNames.Length, + endEntityName: endEntityName, + keyFactoryHashSubjectName: null, + testName, + registerAuthorities, + keyFactory, + forTls, + extensions); + } + + private static void BuildPrivatePkiCore( + PkiOptions pkiOptions, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority[] intermediateAuthorities, + out X509Certificate2 endEntityCert, + X500DistinguishedName rootName, + Func intermediateAuthorityNameFactory, + int intermediateAuthorityCount, + X500DistinguishedName endEntityName, + string keyFactoryHashSubjectName, + string testName = null, + bool registerAuthorities = true, + KeyFactory keyFactory = null, + bool forTls = false, + X509ExtensionCollection extensions = null) { bool rootDistributionViaHttp = !pkiOptions.HasFlag(PkiOptions.NoRootCertDistributionUri); bool issuerRevocationViaCrl = pkiOptions.HasFlag(PkiOptions.IssuerRevocationViaCrl); @@ -838,7 +944,7 @@ internal static void BuildPrivatePki( hasher.AppendData(MemoryMarshal.AsBytes(new ReadOnlySpan(ref pkiOptions))); hasher.AppendData(MemoryMarshal.AsBytes(new ReadOnlySpan(ref intermediateAuthorityCount))); hasher.AppendData(MemoryMarshal.AsBytes(testName.AsSpan())); - hasher.AppendData(MemoryMarshal.AsBytes(subjectName.AsSpan())); + hasher.AppendData(MemoryMarshal.AsBytes(keyFactoryHashSubjectName.AsSpan())); Span hash = stackalloc byte[256 / 8]; int written = hasher.GetCurrentHash(hash); @@ -854,8 +960,7 @@ internal static void BuildPrivatePki( using (KeyHolder rootKey = KeyHolder.CreateKey(keyFactory)) using (KeyHolder eeKey = KeyHolder.CreateKey(keyFactory)) { - CertificateRequest rootReq = rootKey.CreateRequest( - BuildSubject("A Revocation Test Root", testName, pkiOptions, pkiOptionsInSubject)); + CertificateRequest rootReq = rootKey.CreateRequest(rootName); X509BasicConstraintsExtension caConstraints = new X509BasicConstraintsExtension(true, false, 0, true); @@ -894,7 +999,7 @@ internal static void BuildPrivatePki( { X509Certificate2 intermedPub = issuingAuthority.CreateSubordinateCA( - BuildSubject($"A Revocation Test CA {intermediateIndex}", testName, pkiOptions, pkiOptionsInSubject), + intermediateAuthorityNameFactory(intermediateIndex), intermediateKey.ToPublicKey()); intermedCert = intermediateKey.OntoCertificate(intermedPub); intermedPub.Dispose(); @@ -918,7 +1023,7 @@ internal static void BuildPrivatePki( } endEntityCert = issuingAuthority.CreateEndEntity( - BuildSubject(subjectName ?? "A Revocation Test Cert", testName, pkiOptions, pkiOptionsInSubject), + endEntityName, eeKey.ToPublicKey(), extensions); @@ -970,7 +1075,7 @@ internal static void BuildPrivatePki( intermediateAuthority = intermediateAuthorities.Single(); } - private static string BuildSubject( + private static X500DistinguishedName BuildSubject( string cn, string testName, PkiOptions pkiOptions, @@ -979,7 +1084,8 @@ private static string BuildSubject( string testNamePart = !string.IsNullOrWhiteSpace(testName) ? $", O=\"{testName}\"" : ""; string pkiOptionsPart = includePkiOptions ? $", OU=\"{pkiOptions}\"" : ""; - return $"CN=\"{cn}\"" + testNamePart + pkiOptionsPart; + string subject = $"CN=\"{cn}\"" + testNamePart + pkiOptionsPart; + return new X500DistinguishedName(subject); } private static HashAlgorithmName HashAlgorithmIfNeeded(string publicKeyOid) @@ -1114,7 +1220,7 @@ internal static KeyHolder CreateKey(KeyFactory factory) return new KeyHolder(factory.CreateKey()); } - internal CertificateRequest CreateRequest(string subject) + internal CertificateRequest CreateRequest(X500DistinguishedName subject) { return _key switch { diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs index 015166f8f776c4..ee6025063ce847 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs @@ -1227,6 +1227,166 @@ public static void TestRevocation_Offline_Revoked() }); } + [Theory] + [MemberData(nameof(AllViableRevocation))] + public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreRootUnknown(PkiOptions pkiOptions) + { + SelfIssuedButNotSelfSignedRevocationUnknown( + pkiOptions, + (endEntity, chainHolder) => + { + X509Chain chain = chainHolder.Chain; + + chain.ChainPolicy.VerificationFlags |= + X509VerificationFlags.IgnoreRootRevocationUnknown; + + bool success = chain.Build(endEntity); + + AssertChainStatus( + chain, + rootStatus: X509ChainStatusFlags.NoError, + iss1Status: ThisOsRevocationStatusUnknown, + iss2Status: ThisOsNoErrorWithPreviousRevocationError, + leafStatus: ThisOsNoErrorWithPreviousRevocationError); + + AssertExtensions.FalseExpression(success, "chain.Build(endEntity)"); + }); + } + + [Theory] + [MemberData(nameof(AllViableRevocation))] + public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreIntermediateUnknown(PkiOptions pkiOptions) + { + SelfIssuedButNotSelfSignedRevocationUnknown( + pkiOptions, + (endEntity, chainHolder) => + { + X509Chain chain = chainHolder.Chain; + + chain.ChainPolicy.VerificationFlags |= + X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown; + + bool success = chain.Build(endEntity); + + AssertChainStatus( + chain, + rootStatus: X509ChainStatusFlags.NoError, + iss1Status: ThisOsRevocationStatusUnknown, + iss2Status: ThisOsNoErrorWithPreviousRevocationError, + leafStatus: ThisOsNoErrorWithPreviousRevocationError); + + AssertExtensions.TrueExpression(success, "chain.Build(endEntity)"); + }); + } + + private static void SelfIssuedButNotSelfSignedRevocationUnknown( + PkiOptions pkiOptions, + Action callback, + [CallerMemberName] string callerName = "") + { + string rootNameStr = BuildSubject( + "Some Root CA", + callerName, + pkiOptions, + true); + + string intermediateNameStr = BuildSubject( + "Some Self-Issued CA", + callerName, + pkiOptions, + true); + + string endEntityNameStr = BuildSubject( + "Some End-Entity Certificate", + callerName, + pkiOptions, + true); + + X500DistinguishedName rootName = new(rootNameStr); + X500DistinguishedName intermediateName = new(intermediateNameStr); + X500DistinguishedName endEntityName = new(endEntityNameStr); + + BuildPrivatePki( + pkiOptions, + out RevocationResponder responder, + out CertificateAuthority root, + out CertificateAuthority[] intermediates, + out X509Certificate2 endEntity, + rootName: rootName, + intermediateNames: [intermediateName, intermediateName], + endEntityName: endEntityName, + callerName, + registerAuthorities: false); + + using (responder) + using (root) + using (intermediates[0]) + using (intermediates[1]) + using (endEntity) + using (X509Certificate2 rootCert = root.CloneIssuerCert()) + using (X509Certificate2 intermediate1Cert = intermediates[0].CloneIssuerCert()) + using (X509Certificate2 intermediate2Cert = intermediates[1].CloneIssuerCert()) + { + Assert.Equal(intermediate1Cert.SubjectName.RawData, intermediate2Cert.SubjectName.RawData); + Assert.Equal(intermediate1Cert.SubjectName.RawData, intermediate2Cert.IssuerName.RawData); + + if (pkiOptions.HasFlag(PkiOptions.RootAuthorityHasDesignatedOcspResponder)) + { + using (RSA tmpKey = RSA.Create()) + using (X509Certificate2 tmp = root.CreateOcspSigner( + BuildSubject("Some Root OCSP Responder", callerName, pkiOptions, true), + tmpKey)) + { + root.DesignateOcspResponder(tmp.CopyWithPrivateKey(tmpKey)); + } + } + + if (pkiOptions.HasFlag(PkiOptions.IssuerAuthorityHasDesignatedOcspResponder)) + { + using (RSA tmpKey = RSA.Create()) + using (X509Certificate2 tmp = intermediates[0].CreateOcspSigner( + BuildSubject("Some Self-Issued CA OCSP Responder", callerName, pkiOptions, true), + tmpKey)) + { + intermediates[0].DesignateOcspResponder(tmp.CopyWithPrivateKey(tmpKey)); + } + + using (RSA tmpKey = RSA.Create()) + using (X509Certificate2 tmp = intermediates[1].CreateOcspSigner( + BuildSubject("Some Self-Issued CA OCSP Responder", callerName, pkiOptions, true), + tmpKey)) + { + intermediates[1].DesignateOcspResponder(tmp.CopyWithPrivateKey(tmpKey)); + } + } + + // Leave the root unregistered so the original issuing CA has + // unknown revocation status. All certificates below it have + // conclusive revocation status. + responder.AddCertificateAuthority(intermediates[0]); + responder.AddCertificateAuthority(intermediates[1]); + + RetryHelper.Execute( + () => + { + using (ChainHolder holder = new ChainHolder()) + { + X509Chain chain = holder.Chain; + chain.ChainPolicy.CustomTrustStore.Add(rootCert); + chain.ChainPolicy.ExtraStore.Add(intermediate1Cert); + chain.ChainPolicy.ExtraStore.Add(intermediate2Cert); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.VerificationTime = endEntity.NotBefore.AddMinutes(1); + chain.ChainPolicy.UrlRetrievalTimeout = s_urlRetrievalLimit; + chain.ChainPolicy.RevocationFlag = X509RevocationFlag.EntireChain; + + callback(endEntity, holder); + } + }, + maxAttempts: 3); + } + } + private static void RevokeEndEntityWithInvalidRevocation( ChainHolder holder, CertificateAuthority intermediate, @@ -1572,6 +1732,38 @@ private static void AssertChainStatus( } } + private static void AssertChainStatus( + X509Chain chain, + X509ChainStatusFlags rootStatus, + X509ChainStatusFlags iss1Status, + X509ChainStatusFlags iss2Status, + X509ChainStatusFlags leafStatus) + { + Assert.Equal(4, chain.ChainElements.Count); + + X509ChainStatusFlags allFlags = rootStatus | iss1Status | iss2Status | leafStatus; + X509ChainStatusFlags chainActual = chain.AllStatusFlags(); + + X509ChainStatusFlags rootActual = chain.ChainElements[3].AllStatusFlags(); + X509ChainStatusFlags iss1Actual = chain.ChainElements[2].AllStatusFlags(); + X509ChainStatusFlags iss2Actual = chain.ChainElements[1].AllStatusFlags(); + X509ChainStatusFlags leafActual = chain.ChainElements[0].AllStatusFlags(); + + // If things don't match, build arrays so the errors pretty print the full chain. + if (rootActual != rootStatus || + iss1Actual != iss1Status || + iss2Actual != iss2Status || + leafActual != leafStatus || + chainActual != allFlags) + { + X509ChainStatusFlags[] expected = { rootStatus, iss1Status, iss2Status, leafStatus }; + X509ChainStatusFlags[] actual = { rootActual, iss1Actual, iss2Actual, leafActual }; + + Assert.Equal(expected, actual); + Assert.Equal(allFlags, chainActual); + } + } + internal static void BuildPrivatePki( PkiOptions pkiOptions, out RevocationResponder responder, @@ -1592,7 +1784,50 @@ internal static void BuildPrivatePki( endEntityRevocationViaCrl || endEntityRevocationViaOcsp, "At least one revocation mode is enabled"); - CertificateAuthority.BuildPrivatePki(pkiOptions, out responder, out rootAuthority, out intermediateAuthority, out endEntityCert, testName, registerAuthorities, pkiOptionsInSubject); + CertificateAuthority.BuildPrivatePki( + pkiOptions, + out responder, + out rootAuthority, + out intermediateAuthority, + out endEntityCert, + testName, + registerAuthorities, + pkiOptionsInSubject); + } + + internal static void BuildPrivatePki( + PkiOptions pkiOptions, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority[] intermediateAuthorities, + out X509Certificate2 endEntityCert, + X500DistinguishedName rootName, + X500DistinguishedName[] intermediateNames, + X500DistinguishedName endEntityName, + [CallerMemberName] string testName = null, + bool registerAuthorities = true) + { + bool issuerRevocationViaCrl = pkiOptions.HasFlag(PkiOptions.IssuerRevocationViaCrl); + bool issuerRevocationViaOcsp = pkiOptions.HasFlag(PkiOptions.IssuerRevocationViaOcsp); + bool endEntityRevocationViaCrl = pkiOptions.HasFlag(PkiOptions.EndEntityRevocationViaCrl); + bool endEntityRevocationViaOcsp = pkiOptions.HasFlag(PkiOptions.EndEntityRevocationViaOcsp); + + Assert.True( + issuerRevocationViaCrl || issuerRevocationViaOcsp || + endEntityRevocationViaCrl || endEntityRevocationViaOcsp, + "At least one revocation mode is enabled"); + + CertificateAuthority.BuildPrivatePki( + pkiOptions, + out responder, + rootName, + out rootAuthority, + intermediateNames, + out intermediateAuthorities, + endEntityName, + out endEntityCert, + testName, + registerAuthorities); } private static string BuildSubject( From c216b47120dd7b118e7e9e822e84b28f3f48d6d9 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 11 Sep 2026 15:57:06 -0700 Subject: [PATCH 2/7] Fix the tests to do what was intended --- .../RevocationTests/DynamicRevocationTests.cs | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs index ee6025063ce847..6cea56d1793cbb 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs @@ -1227,13 +1227,12 @@ public static void TestRevocation_Offline_Revoked() }); } - [Theory] - [MemberData(nameof(AllViableRevocation))] - public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreRootUnknown(PkiOptions pkiOptions) + [Fact] + public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreRootUnknown() { SelfIssuedButNotSelfSignedRevocationUnknown( - pkiOptions, - (endEntity, chainHolder) => + PkiOptions.AllRevocation, + static (endEntity, chainHolder) => { X509Chain chain = chainHolder.Chain; @@ -1245,21 +1244,20 @@ public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreRootUnknown AssertChainStatus( chain, rootStatus: X509ChainStatusFlags.NoError, - iss1Status: ThisOsRevocationStatusUnknown, - iss2Status: ThisOsNoErrorWithPreviousRevocationError, + iss1Status: X509ChainStatusFlags.NoError, + iss2Status: ThisOsRevocationStatusUnknown, leafStatus: ThisOsNoErrorWithPreviousRevocationError); AssertExtensions.FalseExpression(success, "chain.Build(endEntity)"); }); } - [Theory] - [MemberData(nameof(AllViableRevocation))] - public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreIntermediateUnknown(PkiOptions pkiOptions) + [Fact] + public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreIntermediateUnknown() { SelfIssuedButNotSelfSignedRevocationUnknown( - pkiOptions, - (endEntity, chainHolder) => + PkiOptions.AllRevocation, + static (endEntity, chainHolder) => { X509Chain chain = chainHolder.Chain; @@ -1271,8 +1269,8 @@ public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreIntermediat AssertChainStatus( chain, rootStatus: X509ChainStatusFlags.NoError, - iss1Status: ThisOsRevocationStatusUnknown, - iss2Status: ThisOsNoErrorWithPreviousRevocationError, + iss1Status: X509ChainStatusFlags.NoError, + iss2Status: ThisOsRevocationStatusUnknown, leafStatus: ThisOsNoErrorWithPreviousRevocationError); AssertExtensions.TrueExpression(success, "chain.Build(endEntity)"); @@ -1313,7 +1311,7 @@ private static void SelfIssuedButNotSelfSignedRevocationUnknown( out CertificateAuthority[] intermediates, out X509Certificate2 endEntity, rootName: rootName, - intermediateNames: [intermediateName, intermediateName], + intermediateNames: [intermediateName, new X500DistinguishedName(BuildSubject("Some other CA", callerName, pkiOptions, true))], endEntityName: endEntityName, callerName, registerAuthorities: false); @@ -1327,9 +1325,6 @@ private static void SelfIssuedButNotSelfSignedRevocationUnknown( using (X509Certificate2 intermediate1Cert = intermediates[0].CloneIssuerCert()) using (X509Certificate2 intermediate2Cert = intermediates[1].CloneIssuerCert()) { - Assert.Equal(intermediate1Cert.SubjectName.RawData, intermediate2Cert.SubjectName.RawData); - Assert.Equal(intermediate1Cert.SubjectName.RawData, intermediate2Cert.IssuerName.RawData); - if (pkiOptions.HasFlag(PkiOptions.RootAuthorityHasDesignatedOcspResponder)) { using (RSA tmpKey = RSA.Create()) @@ -1360,10 +1355,10 @@ private static void SelfIssuedButNotSelfSignedRevocationUnknown( } } - // Leave the root unregistered so the original issuing CA has - // unknown revocation status. All certificates below it have - // conclusive revocation status. - responder.AddCertificateAuthority(intermediates[0]); + // Register the root, which covers itself and the original issuing CA. + // Do not register intermediates[0], thus leaving intermediates[1] (the re-key cert) as revocation unknown. + // Register intermediates[1] to cover the end-entity cer. + responder.AddCertificateAuthority(root); responder.AddCertificateAuthority(intermediates[1]); RetryHelper.Execute( From 1005e7d707713940794fc34423a093cbed07a878 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 11 Sep 2026 15:57:59 -0700 Subject: [PATCH 3/7] Don't use IgnoreRootRevocationUnknown for intermediate re-keys. --- .../X509Certificates/UnixChainVerifier.cs | 67 ++++++++++++------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/UnixChainVerifier.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/UnixChainVerifier.cs index 73e195d7d1b840..e3790bb3329f77 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/UnixChainVerifier.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/UnixChainVerifier.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; -using Internal.Cryptography; namespace System.Security.Cryptography.X509Certificates { @@ -11,22 +10,58 @@ internal static class UnixChainVerifier { public static bool Verify(X509ChainElement[] chainElements, X509VerificationFlags flags) { - bool isEndEntity = true; + int rootIndex = HasPartialChain(chainElements) ? -1 : chainElements.Length - 1; - foreach (X509ChainElement element in chainElements) + for (int i = 0; i < chainElements.Length; i++) { - if (HasUnsuppressedError(flags, element, isEndEntity)) + // Element 0 is the end-entity. + // If the chain is complete, then match the last element under "root". + // Otherwise, the element must be part of the middle of a chain. + // + // Two fuzzy pieces in this logic: + // 1. Non-self-issued trust anchors. We generally don't support them, + // but they're possible on macOS because of system trust rules. + // This logic will treat them as roots, which is more correct than not. + // (As the trust anchor, you're not expecting someone to say it was revoked, + // but instead you remove it from anchor status.) + // + // 2. Black-box testing suggests Windows has some special considerations for a + // CA cert that was self-issued by the root (but with a different key and not + // self-signed). This sort of re-keying is exceptionally rare, so it's not a + // high-priority research effort. Until then, calling the signed re-key an + // intermediate is more accurate than calling it a root, as it will be checked + // for revocation under the ExcludeRoot policy. + X509VerificationFlags suppressionFlag = + i == 0 ? X509VerificationFlags.IgnoreEndRevocationUnknown : + i == rootIndex ? X509VerificationFlags.IgnoreRootRevocationUnknown : + X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown; + + if (HasUnsuppressedError(flags, chainElements[i], suppressionFlag)) { return false; } - - isEndEntity = false; } return true; + + static bool HasPartialChain(X509ChainElement[] chainElements) + { + foreach (X509ChainElement element in chainElements) + { + foreach (X509ChainStatus status in element.ChainElementStatus) + { + if (status.Status == X509ChainStatusFlags.PartialChain) + { + return true; + } + } + } + + return false; + } } - private static bool HasUnsuppressedError(X509VerificationFlags flags, X509ChainElement element, bool isEndEntity) + private static bool HasUnsuppressedError(X509VerificationFlags flags, X509ChainElement element, X509VerificationFlags revocationSuppressionFlag) { foreach (X509ChainStatus status in element.ChainElementStatus) { @@ -47,18 +82,7 @@ private static bool HasUnsuppressedError(X509VerificationFlags flags, X509ChainE if (status.Status == X509ChainStatusFlags.RevocationStatusUnknown) { - if (isEndEntity) - { - suppressionFlag = X509VerificationFlags.IgnoreEndRevocationUnknown; - } - else if (IsSelfSigned(element.Certificate)) - { - suppressionFlag = X509VerificationFlags.IgnoreRootRevocationUnknown; - } - else - { - suppressionFlag = X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown; - } + suppressionFlag = revocationSuppressionFlag; } else if (status.Status == X509ChainStatusFlags.OfflineRevocation) { @@ -83,11 +107,6 @@ private static bool HasUnsuppressedError(X509VerificationFlags flags, X509ChainE return false; } - private static bool IsSelfSigned(X509Certificate2 cert) - { - return cert.SubjectName.RawData.ContentsEqual(cert.IssuerName.RawData); - } - private static X509VerificationFlags? GetSuppressionFlag(X509ChainStatusFlags status) { switch (status) From adda54a485863b620b3035a2cb76fcec96f41a77 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 11 Sep 2026 17:33:59 -0700 Subject: [PATCH 4/7] Apply feedback --- .../RevocationTests/DynamicRevocationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs index 6cea56d1793cbb..49ab99a8438e19 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs @@ -1311,7 +1311,7 @@ private static void SelfIssuedButNotSelfSignedRevocationUnknown( out CertificateAuthority[] intermediates, out X509Certificate2 endEntity, rootName: rootName, - intermediateNames: [intermediateName, new X500DistinguishedName(BuildSubject("Some other CA", callerName, pkiOptions, true))], + intermediateNames: [intermediateName, intermediateName], endEntityName: endEntityName, callerName, registerAuthorities: false); @@ -1357,7 +1357,7 @@ private static void SelfIssuedButNotSelfSignedRevocationUnknown( // Register the root, which covers itself and the original issuing CA. // Do not register intermediates[0], thus leaving intermediates[1] (the re-key cert) as revocation unknown. - // Register intermediates[1] to cover the end-entity cer. + // Register intermediates[1] to cover the end-entity cert. responder.AddCertificateAuthority(root); responder.AddCertificateAuthority(intermediates[1]); From 59871937f103d7273134108b1ff2be9b1fcd8db5 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 11 Sep 2026 17:34:24 -0700 Subject: [PATCH 5/7] Disable the new tests on Windows, because they're misbehaving --- .../X509Certificates/RevocationTests/DynamicRevocationTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs index 49ab99a8438e19..9023cd6a011df5 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs @@ -1228,6 +1228,7 @@ public static void TestRevocation_Offline_Revoked() } [Fact] + [PlatformSpecific(~TestPlatforms.Windows)] public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreRootUnknown() { SelfIssuedButNotSelfSignedRevocationUnknown( @@ -1253,6 +1254,7 @@ public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreRootUnknown } [Fact] + [PlatformSpecific(~TestPlatforms.Windows)] public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreIntermediateUnknown() { SelfIssuedButNotSelfSignedRevocationUnknown( From bf79ae613fc7b72dbea4c43944168b1d1c24cf74 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 14 Sep 2026 15:55:34 -0700 Subject: [PATCH 6/7] Make new test pass on Android --- .../RevocationTests/DynamicRevocationTests.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs index 9023cd6a011df5..51b2687c5c8131 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs @@ -1275,7 +1275,17 @@ public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreIntermediat iss2Status: ThisOsRevocationStatusUnknown, leafStatus: ThisOsNoErrorWithPreviousRevocationError); - AssertExtensions.TrueExpression(success, "chain.Build(endEntity)"); + if (ThisOsNoErrorWithPreviousRevocationError == X509ChainStatusFlags.NoError) + { + AssertExtensions.TrueExpression(success, "chain.Build(endEntity)"); + } + else + { + // On Android, the intermediate yielding a RevocationStatusUnknown + // causes the leaf to also yield a RevocationStatusUnknown, + // which we didn't suppress, so we expect false. + AssertExtensions.FalseExpression(success, "chain.Build(endEntity)"); + } }); } From f04fea0961d1c7455c2993ac2f1c9f46b7559039 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Mon, 14 Sep 2026 15:57:28 -0700 Subject: [PATCH 7/7] Restrict the new tests to Linux It looks like a number of OSes special case a self-issued child for revocation, and so long as the parent was known don't complain about the child. Rather than play whack-a-mole, just limit it to Linux for now. --- .../RevocationTests/DynamicRevocationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs index 51b2687c5c8131..031c8716804785 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/RevocationTests/DynamicRevocationTests.cs @@ -1228,7 +1228,7 @@ public static void TestRevocation_Offline_Revoked() } [Fact] - [PlatformSpecific(~TestPlatforms.Windows)] + [PlatformSpecific(TestPlatforms.Linux)] public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreRootUnknown() { SelfIssuedButNotSelfSignedRevocationUnknown( @@ -1254,7 +1254,7 @@ public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreRootUnknown } [Fact] - [PlatformSpecific(~TestPlatforms.Windows)] + [PlatformSpecific(TestPlatforms.Linux)] public static void SelfIssuedButNotSelfSignedRevocationUnknown_IgnoreIntermediateUnknown() { SelfIssuedButNotSelfSignedRevocationUnknown(