如何使用C#检测证书链不完整的证书 避免.NET Core误判为有效
问题背景
我正尝试编写代码验证网站证书的有效性,例如测试站点incomplete-chain.badssl.com。目前遇到的问题是:Windows(或.NET框架)在证书校验过程中会自动补全证书链,当证书链传递到我自定义的校验回调函数时,其内容已经和服务端返回的原始链不同,被判定为有效。
我尝试过HttpClient和SslStream的证书校验回调,二者均将不完整链证书判定为有效,不符合预期。curl请求可以按照预期对不完整证书链返回错误,示例如下:
curl https://incomplete-chain.badssl.com % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--: --:--: --:--: 0 curl: (60) SSL certificate problem: unable to get local issuer certificate
openssl也可正确识别此类不完整证书链为无效,示例如下:
$ openssl s_client -showcerts -connect incomplete-chain.badssl.com:443 CONNECTED(00000188) --- Certificate chain 0 s:C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com i:C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA -----BEGIN CERTIFICATE----- the certificate -----END CERTIFICATE----- --- Server certificate subject=C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com issuer=C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA --- SSL handshake has read 2414 bytes and written 455 bytes Verification error: unable to verify the first certificate ---
已尝试的实现代码
使用WebRequest的测试代码如下:
using System; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; public class Program { public static void Main(string[] args) { var request = (HttpWebRequest)WebRequest.Create("https://incomplete-chain.badssl.com"); request.AllowAutoRedirect = false; request.ServerCertificateValidationCallback = ServerCertificateValidationCallback; var response = (HttpWebResponse)request.GetResponse(); response.Close(); } private static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (sslPolicyErrors != SslPolicyErrors.None) { return false; } var newChain = new X509Chain(); newChain.ChainPolicy.DisableCertificateDownloads = true; // Skip the leaf cert and stop short of the root cert. X509ChainElementCollection chainElements = chain.ChainElements; for (int i = 1; i < chainElements.Count - 1; i++) { newChain.ChainPolicy.ExtraStore.Add(chainElements[i].Certificate); } var result = newChain.Build(chainElements[0].Certificate); // This is True and I want it to be False Console.WriteLine($"This is {result} and I want it to be False"); return result; } }
运行上述代码时newChain.Build()返回了True,不符合预期的False结果。
需求
是否有方法绕过cryptoapi的自动补全证书链特性,直接使用服务端返回的原始证书完成链校验?
你现有代码的核心问题是使用的chain.ChainElements已经是Windows CryptoAPI自动补全后的链条,并非服务端返回的原始链,所以即便你新建了X509Chain,添加的中间证书也是已经补全的内容,自然会校验通过。可以通过以下两种方案实现需求:
方案1:修改X509Chain校验策略(.NET 5+适用)
通过显式配置X509Chain的信任模式,禁用系统中间证书存储的自动读取,仅使用服务端返回的原始证书完成校验:
// 推荐替换为SslStream实现,可以直接获取服务端返回的原始证书链 private static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { var leafCert = new X509Certificate2(certificate.Export(X509ContentType.Cert)); var newChain = new X509Chain(); // 核心配置:使用自定义信任根,禁止从系统证书存储自动读取中间证书 newChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; newChain.ChainPolicy.CustomTrustStore.Clear(); // 导入系统根证书,和openssl/curl的默认校验逻辑对齐 using var rootStore = new X509Store(StoreName.Root, StoreLocation.CurrentUser); rootStore.Open(OpenFlags.ReadOnly); foreach (var rootCert in rootStore.Certificates) { newChain.ChainPolicy.CustomTrustStore.Add(rootCert); } newChain.ChainPolicy.DisableCertificateDownloads = true; // 注意:此处需要从TLS握手原始数据中获取服务端返回的中间证书,不可使用已经补全的chain.ChainElements // 若使用SslStream实现,可通过SslStream.RemoteCertificates获取服务端返回的所有原始证书,再将中间证书加入ExtraStore var result = newChain.Build(leafCert); Console.WriteLine($"校验结果:{result}"); return result; }
方案2:使用第三方密码库(全版本.NET适用)
如果需要兼容.NET Framework版本,或者想要完全对齐openssl的校验行为,可以使用BouncyCastle等第三方密码库实现证书链校验,这类库默认不会读取系统证书存储补全链条,完全基于传入的证书完成校验。
补充说明
如果仅使用HttpClient/WebRequest的回调无法拿到原始证书链,建议直接基于SslStream编写底层TLS握手逻辑,可以完全控制证书的获取和校验流程。
内容的提问来源于stack exchange,提问作者Stephen

