How to properly validate X.509 certificates in C# with .NET Core 3.1 & .NET 5+

If you landed here from Google, you're probably most interested in my reference repo stewartadam/dotnet-x509-certificate-verification on performing self-signed X.509 validation in .NET correctly. If you want to understand the nuances involved, read on below.

I have been recently working on a C# project that required issuing certificates from a self-signed root CA and discovered some unintuitive (and at the time, undocumented) behavior from the X509Chain.Build() method: its AllowUnknownCertificateAuthority X509VerificationFlag flag used to perform validation on self-signed certificates not only ignores only UntrustedRoot status, but also the PartialChain status as well!

I detail the consequences in dotnet/runtime#49615, but imn short any certificate can be considered a partial chain with the chain consisting of itself; so when PartialChain is ignored the return value of X509Chain.Build() (which is supposed to indicate if validation succeeded) can be deceiving. It is entirely possible that the certificate under validation was not issued by any of the trusted root CAs or those specified in the ExtraStore. Instead X509Chain.Build() considered a new chain, consisting only the certificate under validation, and determined that to be a partial chain which was then ignored - so it still returns true.

The X509VerificationFlag docs have since been corrected to include PartialChain as one of the ignored statuses when using the AllowUnknownCertificateAuthority flag, but contributions of further examples and implications of this to the docs were rejected and I do not believe that the docs provide sufficient clarity around these issues.

Unless you use .NET 5's X509Chain.CustomTrustStore property, the return value of X509Chain.Build() should not be trusted on its own; proper certificate verification requires one manually and separately perform verification of correct chain termination after running X509Chain.Build() (i.e. checking the last item in the chain is indeed the signing root CA we expect).

I have created a new GitHub repository stewartadam/dotnet-x509-certificate-verification that describes these issues in detail and provides code samples for securely validating X.509 certificates on both .NET Core and .NET 5 including with self-signed root CAs.