如何在C# WebClient中验证服务端证书实现证书固定
Great question! You've got the client certificate part sorted, but to block man-in-the-middle attacks (like Charles proxy intercepting/tampering with your traffic), you need to enforce strict server certificate validation that rejects any untrusted certificates—including Charles' self-signed root cert even if it's installed in the system trust store.
Here are two robust approaches to implement this in your SafeClient class:
解决方案1:验证服务端证书的指纹(最简单直接)
If you know the exact thumbprint of the server's certificate (and it doesn't change frequently), you can hardcode it and validate that the server presents a certificate with a matching thumbprint. This bypasses the system trust store entirely, so even if Charles installs its own root cert, it won't pass validation.
Update your SafeClient class like this:
using System.Reflection; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; class SafeClient : WebClient { // Replace this with your server's certificate thumbprint (remove all spaces, case-insensitive) private const string ExpectedServerThumbprint = "A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0"; protected override WebRequest GetWebRequest(Uri address) { var request = base.GetWebRequest(address) as HttpWebRequest; if (request == null) return base.GetWebRequest(address); // Keep your existing client certificate loading logic byte[] embeddedCert; using (Stream certStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Test.Resources.test.pfx")) { embeddedCert = new byte[certStream.Length]; certStream.Read(embeddedCert, 0, (int)certStream.Length); } var cert = new X509Certificate2(embeddedCert, "Pass"); request.ClientCertificates.Add(cert); // Attach server certificate validation callback request.ServerCertificateValidationCallback += ValidateServerByThumbprint; return request; } private bool ValidateServerByThumbprint(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { // Reject any request with SSL errors first if (sslPolicyErrors != SslPolicyErrors.None) return false; // Compare the server's certificate thumbprint to our expected value string serverThumbprint = certificate.GetCertHashString().Replace(" ", "").ToUpperInvariant(); return serverThumbprint.Equals(ExpectedServerThumbprint.ToUpperInvariant()); } }
解决方案2:信任指定的根CA证书(更灵活)
If your server's certificate is issued by a private root CA (and you want to allow certificate rotations without changing code), embed the root CA's certificate into your app and validate that the server's certificate chain traces back to this trusted root. This ignores the system trust store, so Charles' root cert won't be recognized.
Here's how to implement this:
using System.Reflection; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; class SafeClient : WebClient { private readonly X509Certificate2 _trustedRootCa; public SafeClient() { // Load your embedded root CA certificate (usually a .cer file, no password) using (Stream caStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Test.Resources.TrustedRootCA.cer")) { _trustedRootCa = new X509Certificate2(caStream); } } protected override WebRequest GetWebRequest(Uri address) { var request = base.GetWebRequest(address) as HttpWebRequest; if (request == null) return base.GetWebRequest(address); // Keep your existing client certificate loading logic byte[] embeddedCert; using (Stream certStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Test.Resources.test.pfx")) { embeddedCert = new byte[certStream.Length]; certStream.Read(embeddedCert, 0, (int)certStream.Length); } var cert = new X509Certificate2(embeddedCert, "Pass"); request.ClientCertificates.Add(cert); // Attach server certificate validation callback request.ServerCertificateValidationCallback += ValidateServerByTrustedRoot; return request; } private bool ValidateServerByTrustedRoot(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { // Reject any request with SSL errors if (sslPolicyErrors != SslPolicyErrors.None) return false; // Configure the chain to only trust our embedded root CA var chainPolicy = new X509ChainPolicy(); chainPolicy.ExtraStore.Add(_trustedRootCa); // Disable system trust store - only use our custom root chainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; // Optional: Adjust revocation check based on your needs chainPolicy.RevocationMode = X509RevocationMode.NoCheck; chain.ChainPolicy = chainPolicy; // Build and validate the certificate chain bool isChainValid = chain.Build(new X509Certificate2(certificate)); // Verify the chain ends with our trusted root CA if (isChainValid && chain.ChainElements.Count > 0) { var rootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate; return rootCert.Thumbprint.Equals(_trustedRootCa.Thumbprint, StringComparison.OrdinalIgnoreCase); } return false; } }
关键注意事项
- Avoid global validation callbacks: Never use
ServicePointManager.ServerCertificateValidationCallbackglobally, as it affects all HTTP requests in your app. Always attach the callback to individualHttpWebRequestinstances like the examples above. - Secure your embedded certificates: Make sure your embedded PFX and root CA certificates are protected (e.g., via code signing, obfuscation) to prevent tampering.
- Thumbprint accuracy: Double-check the server's certificate thumbprint—you can get it from the certificate's properties (remove all spaces before using it).
- Revocation checks: If you need to validate certificate revocation, change
X509RevocationMode.NoChecktoOnlineorOffline, but be aware of potential network latency or availability issues.
With either approach, Charles (or any other MITM tool) won't be able to intercept your traffic, since their certificates won't pass your custom validation logic.
内容的提问来源于stack exchange,提问作者DerRuedi

