using System.Security.Cryptography.X509Certificates; static X509Certificate2 Load(string path) => X509CertificateLoader.LoadCertificateFromFile(path); // Builds the chain from leaf to the given root; prints the path or every problem foundstatic void Verify(string label, string leafPath, string[] intermediatePaths, string rootPath){ using var chain = new X509Chain(); chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; // not the system store chain.ChainPolicy.CustomTrustStore.Add(Load(rootPath)); foreach (string path in intermediatePaths) chain.ChainPolicy.ExtraStore.Add(Load(path)); chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // a test CA has no CRL or OCSP chain.ChainPolicy.DisableCertificateDownloads = true; // no fetching of missing issuers (AIA) bool valid = chain.Build(Load(leafPath)); if (valid) { Console.WriteLine($"{label}: valid, " + string.Join(" -> ", chain.ChainElements.Select(e => e.Certificate.GetNameInfo(X509NameType.SimpleName, false)))); } else { Console.WriteLine($"{label}: " + string.Join(", ", chain.ChainStatus.Select(s => s.Status))); }} Verify("good chain", "leaf.pem", ["intermediate.pem"], "root.pem");Verify("impostor root", "leaf.pem", ["intermediate.pem"], "impostor-root.pem");Verify("expired leaf", "expired.pem", ["intermediate.pem"], "root.pem");Verify("missing intermediate", "leaf.pem", [], "root.pem"); How it works
TrustMode = X509ChainTrustMode.CustomRootTrustwith the root inCustomTrustStore(.NET 5+) ignores the machine's root store entirely.ExtraStoreholds intermediates the chain may use; they are candidates, not trust anchors.DisableCertificateDownloads = truestops .NET from fetching missing issuers from AIA URLs, andRevocationMode = NoCheckfrom fetching CRLs — the test CA publishes neither.BuildreturnstrueandChainElementsholds the path:api.example.com → ECDSA.com Howto Issuing CA 1 → ECDSA.com Howto Root CA.
Gotchas
- The look-alike root and the missing intermediate both report
PartialChain: .NET could not complete a path to the custom root. The status does not say which link is missing. - The expired leaf reports
NotTimeValid.VerificationTimemoves the check to another moment — with 2025-02-01 the same expired leaf builds a trusted chain — which is right for checking old signatures and wrong as a way around an expired certificate. DisableCertificateDownloadsmatters for certificates with AIA links: without it .NET may fetch a missing intermediate, so the same chain can pass on a machine with network access and fail on one without.
Related recipes
- How to verify a certificate chain against your own trust store in Java
- How to verify a certificate chain against your own trust store in Python
- How to verify a certificate chain against your own trust store with OpenSSL
- How to check SSL certificate expiration date in C# (.NET)
- How to decode an X.509 certificate in C# (.NET)