Skip to content
ecdsa.com

Verify a certificate chain · .NET

How to verify a certificate chain against your own trust store in C# (.NET)

To verify a certificate chain against your own trust store in C#, use X509Chain with ChainPolicy.TrustMode = CustomRootTrust, your root in CustomTrustStore and the intermediates in ExtraStore; Build(leaf) returns true for a trusted chain and ChainStatus explains a failure. This recipe checks a leaf → intermediate → root chain from a test CA and three chains that must fail.

Tested with .NET 10.0.13 (SDK 10.0.402) (Linux, X509Chain with CustomRootTrust).

verify.cs — X509Chain with a custom root
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 found
static 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

  1. TrustMode = X509ChainTrustMode.CustomRootTrust with the root in CustomTrustStore (.NET 5+) ignores the machine's root store entirely.
  2. ExtraStore holds intermediates the chain may use; they are candidates, not trust anchors.
  3. DisableCertificateDownloads = true stops .NET from fetching missing issuers from AIA URLs, and RevocationMode = NoCheck from fetching CRLs — the test CA publishes neither.
  4. Build returns true and ChainElements holds 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. VerificationTime moves 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.
  • DisableCertificateDownloads matters 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