将自签名证书与 .NET 的 HttpWebRequest/Response 一起使用

Posted

技术标签:

【中文标题】将自签名证书与 .NET 的 HttpWebRequest/Response 一起使用【英文标题】:Using a self-signed certificate with .NET's HttpWebRequest/Response 【发布时间】:2010-10-06 07:37:14 【问题描述】:

我正在尝试连接到使用自签名 SSL 证书的 API。我这样做是使用 .NET 的 HttpWebRequest 和 HttpWebResponse 对象。我遇到了一个例外:

底层连接已关闭:无法为 SSL/TLS 安全通道建立信任关系。

我明白这意味着什么。我理解为什么 .NET 认为它应该警告我并关闭连接。但在这种情况下,我还是想连接到 API,中间人攻击该死。

那么,我该如何为这个自签名证书添加一个例外呢?或者是告诉 HttpWebRequest/Response 根本不验证证书的方法?我该怎么做?

【问题讨论】:

【参考方案1】:

事实证明,如果您只想完全禁用证书验证,您可以更改 ServicePointManager 上的 ServerCertificateValidationCallback,如下所示:

ServicePointManager.ServerCertificateValidationCallback = delegate  return true; ;

这将验证所有证书(包括无效、过期或自签名证书)。

【讨论】:

非常适合对开发机器进行一些快速测试。谢谢。 这会影响什么范围 - appdomain 中的所有内容? apppool 上的所有内容?机器上的所有东西? 但要小心! RL 经验表明,这种开发技巧通常会进入发布产品:The most dangerous code in the world 这是一个在开发中很有用的 hack,所以在它周围加上一个#if DEBUG #endif 语句是你至少应该做的,以使它更安全,并阻止它在生产中结束。 除非这个人删除了这个答案,否则我们会看到一个有趣的事实,即错误答案获得的票数远远多于正确答案。【参考方案2】:

@Domster:可行,但您可能希望通过检查证书哈希是否符合您的预期来加强一点安全性。所以扩展版本看起来有点像这样(基于我们正在使用的一些实时代码):

static readonly byte[] apiCertHash =  0xZZ, 0xYY, ....;

/// <summary>
/// Somewhere in your application's startup/init sequence...
/// </summary>
void InitPhase()

    // Override automatic validation of SSL server certificates.
    ServicePointManager.ServerCertificateValidationCallback =
           ValidateServerCertficate;


/// <summary>
/// Validates the SSL server certificate.
/// </summary>
/// <param name="sender">An object that contains state information for this
/// validation.</param>
/// <param name="cert">The certificate used to authenticate the remote party.</param>
/// <param name="chain">The chain of certificate authorities associated with the
/// remote certificate.</param>
/// <param name="sslPolicyErrors">One or more errors associated with the remote
/// certificate.</param>
/// <returns>Returns a boolean value that determines whether the specified
/// certificate is accepted for authentication; true to accept or false to
/// reject.</returns>
private static bool ValidateServerCertficate(
        object sender,
        X509Certificate cert,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)

    if (sslPolicyErrors == SslPolicyErrors.None)
    
        // Good certificate.
        return true;
    

    log.DebugFormat("SSL certificate error: 0", sslPolicyErrors);

    bool certMatch = false; // Assume failure
    byte[] certHash = cert.GetCertHash();
    if (certHash.Length == apiCertHash.Length)
    
        certMatch = true; // Now assume success.
        for (int idx = 0; idx < certHash.Length; idx++)
        
            if (certHash[idx] != apiCertHash[idx])
            
                certMatch = false; // No match
                break;
            
        
    

    // Return true => allow unauthenticated server,
    //        false => disallow unauthenticated server.
    return certMatch;

【讨论】:

可能有人更喜欢下面的正确方法。无论如何,这个黑客在紧要关头工作,但你可能不应该在......中编码这些类型的异常......要么一起禁用所有检查(通过下面的建议),要么实际指示你的计算机信任证书.. . @BrainSlugs83:禁用当然也是一种选择,但是将证书添加到机器级根权限存储只能由管理员完成。无论哪种方式,我的解决方案都有效。 我完全理解这一点,但你问了,这仍然是我对为什么有人否决你的答案的猜测。不管它是否需要更多工作,恕我直言 wgthom 下面的答案仍然是最正确的答案。 顺便说一句,小心,我认为 ServerCertificateValidationCallback 是静态的,甚至不是线程本地的。如果我没记错的话,一旦设置,它就会保持设置,直到你清除它。如果您只想将它​​用于一个连接而不是所有其他连接,请非常小心并行请求.. 这是最好的方法。如果您取消对 sslPolicyErrors 的检查,您实际上可以确保 API 证书始终是预期的证书。需要注意的一点是,上面代码中的证书指纹是一个 const 字节数组。这不会按书面形式编译。请尝试使用静态只读字节数组。编译器对此感到窒息,因为它需要 new() 运算符。【参考方案3】:

将自签名证书添加到本地计算机受信任的根证书颁发机构

您可以通过以管理员身份运行 MMC 来导入证书。

How to: View Certificates with the MMC Snap-in

【讨论】:

恕我直言,这是最正确的方法;人们只是太懒了,所以他们在特殊的例外情况下编写他们可能不应该做的事情。 该方法是否适用于 Windows Mobile 6.5? 7号怎么样?就我而言,我不想为我计划在其上运行开发版本的每台移动设备添加本地证书。在这种情况下,一个很好的例外使部署变得更加容易。懒惰或效率,你告诉我。 @domster 您使用 SSL 证书是有原因的 - 验证端点。如果您开发专门解决该问题的代码,您就没有正确地对其进行测试,并且有可能将该代码泄漏到实际环境中。如果在客户端上安装证书实在是太麻烦了,为什么不直接从所有设备都信任的颁发者那里购买证书呢? @Basic 如果我记得这个具体案例,我需要几个通配符证书(它连接到的有六个 TLD,都在我们的控制之下)。对于开发环境来说,这是一个难以证明的成本。在这种情况下,唯一被“解决”且未经过测试的代码是不会在本来应该出现的地方抛出异常。无论您是否使用此解决方法,您都应该测试该特定异常路径。最后,如果您不能将开发代码排除在生产环境之外,那么您将面临比 SSL 验证更大的问题。 对于 webapps,请务必回收您的应用程序池或重新启动您的网站。就个人而言,我只是重新编译,然后它工作。对于我们的 wsdl 内容,证书验证似乎发生在初始化和缓存中。【参考方案4】:

Domster's answer 中使用的验证回调的范围可以限制为使用ServerCertificateValidationCallback 委托上的 sender 参数的特定请求。下面的简单作用域类使用这种技术来临时连接一个仅针对给定请求对象执行的验证回调。

public class ServerCertificateValidationScope : IDisposable

    private readonly RemoteCertificateValidationCallback _callback;

    public ServerCertificateValidationScope(object request,
        RemoteCertificateValidationCallback callback)
    
        var previous = ServicePointManager.ServerCertificateValidationCallback;
        _callback = (sender, certificate, chain, errors) =>
            
                if (sender == request)
                
                    return callback(sender, certificate, chain, errors);
                
                if (previous != null)
                
                    return previous(sender, certificate, chain, errors);
                
                return errors == SslPolicyErrors.None;
            ;
        ServicePointManager.ServerCertificateValidationCallback += _callback;
    

    public void Dispose()
    
        ServicePointManager.ServerCertificateValidationCallback -= _callback;
    

上述类可用于忽略特定请求的所有证书错误,如下所示:

var request = WebRequest.Create(uri);
using (new ServerCertificateValidationScope(request, delegate  return true; ))

    request.GetResponse();

【讨论】:

这个答案需要更多的赞成票 :) 使用 HttpWebRequest 对象跳过单个请求的证书验证是最合理的答案。 我添加了这个,但我仍然收到请求被中止:无法创建 SSL/TLS 安全通道。 这并不能真正解决多线程环境中的问题。 maaan !!!,一个 5 岁的帖子拯救了我的一天,我在使用无效证书连接到旧卫星调制解调器设备时遇到问题!谢谢!! 我很困惑/有点担心!在没有先前回调的情况下不返回SslPolicyErrors.None 是否意味着我们最终用“全部接受”策略覆盖了默认策略?参看。这个问题及其各种答案:***.com/q/9058096。很高兴有人告诉我为什么我错了,这段代码很好!【参考方案5】:

基于devstuff 的回答,包括主题和发行人...欢迎 cmets...

public class SelfSignedCertificateValidator

    private class CertificateAttributes
    
        public string Subject  get; private set; 
        public string Issuer  get; private set; 
        public string Thumbprint  get; private set; 

        public CertificateAttributes(string subject, string issuer, string thumbprint)
        
            Subject = subject;
            Issuer = issuer;                
            Thumbprint = thumbprint.Trim(
                new char[]  '\u200e', '\u200f'  // strip any lrt and rlt markers from copy/paste
                ); 
        

        public bool IsMatch(X509Certificate cert)
        
            bool subjectMatches = Subject.Replace(" ", "").Equals(cert.Subject.Replace(" ", ""), StringComparison.InvariantCulture);
            bool issuerMatches = Issuer.Replace(" ", "").Equals(cert.Issuer.Replace(" ", ""), StringComparison.InvariantCulture);
            bool thumbprintMatches = Thumbprint == String.Join(" ", cert.GetCertHash().Select(h => h.ToString("x2")));
            return subjectMatches && issuerMatches && thumbprintMatches; 
        
    

    private readonly List<CertificateAttributes> __knownSelfSignedCertificates = new List<CertificateAttributes> 
        new CertificateAttributes(  // can paste values from "view cert" dialog
            "CN = subject.company.int", 
            "CN = issuer.company.int", 
            "f6 23 16 3d 5a d8 e5 1e 13 58 85 0a 34 9f d6 d3 c8 23 a8 f4") 
    ;       

    private static bool __createdSingleton = false;

    public SelfSignedCertificateValidator()
    
        lock (this)
        
            if (__createdSingleton)
                throw new Exception("Only a single instance can be instanciated.");

            // Hook in validation of SSL server certificates.  
            ServicePointManager.ServerCertificateValidationCallback += ValidateServerCertficate;

            __createdSingleton = true;
        
    

    /// <summary>
    /// Validates the SSL server certificate.
    /// </summary>
    /// <param name="sender">An object that contains state information for this
    /// validation.</param>
    /// <param name="cert">The certificate used to authenticate the remote party.</param>
    /// <param name="chain">The chain of certificate authorities associated with the
    /// remote certificate.</param>
    /// <param name="sslPolicyErrors">One or more errors associated with the remote
    /// certificate.</param>
    /// <returns>Returns a boolean value that determines whether the specified
    /// certificate is accepted for authentication; true to accept or false to
    /// reject.</returns>
    private bool ValidateServerCertficate(
        object sender,
        X509Certificate cert,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
    
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;   // Good certificate.

        Dbg.WriteLine("SSL certificate error: 0", sslPolicyErrors);
        return __knownSelfSignedCertificates.Any(c => c.IsMatch(cert));            
    

【讨论】:

【参考方案6】:

注意,在 .NET 4.5 中,您可以覆盖每个 HttpWebRequest 本身的 SSL 验证(而不是通过影响所有请求的全局委托):

http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.servercertificatevalidationcallback.aspx

HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.ServerCertificateValidationCallback = delegate  return true; ;

【讨论】:

请点赞;这值得升级到 4.5! @FlorianWinter 是的,你必须采用用户 devstuff 的逻辑【参考方案7】:

要记住的一点是,拥有 ServicePointManager.ServerCertificateValidationCallback 似乎并不意味着没有完成 CRL 检查和服务器名称验证,它只是提供了一种覆盖其结果的方法。因此,您的服务可能仍需要一段时间才能获得 CRL,只有事后您才会知道它未通过某些检查。

【讨论】:

【参考方案8】:

为其他人添加可能的帮助...如果您希望它提示用户安装自签名证书,您可以使用此代码(从上面修改)。

不需要管理员权限,安装到本地用户受信任的配置文件:

    private static bool ValidateServerCertficate(
        object sender,
        X509Certificate cert,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
    
        if (sslPolicyErrors == SslPolicyErrors.None)
        
            // Good certificate.
            return true;
        

        Common.Helpers.Logger.Log.Error(string.Format("SSL certificate error: 0", sslPolicyErrors));
        try
        
            using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
            
                store.Open(OpenFlags.ReadWrite);
                store.Add(new X509Certificate2(cert));
                store.Close();
            
            return true;
        
        catch (Exception ex)
        
            Common.Helpers.Logger.Log.Error(string.Format("SSL certificate add Error: 0", ex.Message));
        

        return false;
    

这似乎对我们的应用程序很有效,如果用户按否,通信将无法正常工作。

更新:2015-12-11 - 将 StoreName.Root 更改为 StoreName.My - 我将安装到本地用户存储中,而不是 Root。即使您“以管理员身份运行”,某些系统上的 Root 也无法正常工作

【讨论】:

如果它可以在 Compact Framework winCE 上运行,那就太棒了。 store.Add(..) 不可用。【参考方案9】:

我遇到了与 OP 相同的问题,其中 Web 请求会引发确切的异常。我认为所有设置都正确,证书已安装,我可以很好地在机器存储中找到它并将其附加到 Web 请求中,并且我已禁用请求上下文中的证书验证。

原来我是在我的用户帐户下运行的,并且证书已安装到机器商店。这导致 Web 请求抛出此异常。为了解决这个问题,我必须以管理员身份运行或将证书安装到用户存储并从那里读取。

似乎 C# 能够在机器存储中找到证书,即使它不能用于 Web 请求,这会导致一旦发出 Web 请求就会引发 OP 的异常。

【讨论】:

对于 Windows 服务,您可以为每个服务设置单独的证书配置。如果您编写的不是桌面应用程序而是服务,则可以在 MMC 中专门为服务守护程序导入 CA 证书。用户帐户和机器帐户有什么区别?我认为机器帐户中的所有内容都会自动应用于用户。【参考方案10】:

首先 - 我很抱歉,因为我使用了 @devstuff 描述的解决方案。不过,我已经找到了一些改进方法。

添加自签名证书处理 证书原始数据对比 实际的证书颁发机构验证 一些额外的 cmets 和改进

这是我的修改:

private static X509Certificate2 caCertificate2 = null;

/// <summary>
/// Validates the SSL server certificate.
/// </summary>
/// <param name="sender">An object that contains state information for this validation.</param>
/// <param name="cert">The certificate used to authenticate the remote party.</param>
/// <param name="chain">The chain of certificate authorities associated with the remote certificate.</param>
/// <param name="sslPolicyErrors">One or more errors associated with the remote certificate.</param>
/// <returns>Returns a boolean value that determines whether the specified certificate is accepted for authentication; true to accept or false to reject.</returns>
private static bool ValidateServerCertficate(
        object sender,
        X509Certificate cert,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)

    if (sslPolicyErrors == SslPolicyErrors.None)
    
        // Good certificate.
        return true;
    

    // If the following line is not added, then for the self-signed cert an error will be (not tested with let's encrypt!):
    // "A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider. (UntrustedRoot)"
    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;

    // convert old-style cert to new-style cert
    var returnedServerCert2 = new X509Certificate2(cert);

    // This part is very important. Adding known root here. It doesn't have to be in the computer store at all. Neither do certificates.
    chain.ChainPolicy.ExtraStore.Add(caCertificate2);

    // 1. Checks if ff the certs are OK (not expired/revoked/etc) 
    // 2. X509VerificationFlags.AllowUnknownCertificateAuthority will make sure that untrusted certs are OK
    // 3. IMPORTANT: here, if the chain contains the wrong CA - the validation will fail, as the chain is wrong!
    bool isChainValid = chain.Build(returnedServerCert2);
    if (!isChainValid)
    
        string[] errors = chain.ChainStatus
            .Select(x => String.Format("0 (1)", x.StatusInformation.Trim(), x.Status))
            .ToArray();

        string certificateErrorsString = "Unknown errors.";

        if (errors != null && errors.Length > 0)
        
            certificateErrorsString = String.Join(", ", errors);
        

        Log.Error("Trust chain did not complete to the known authority anchor. Errors: " + certificateErrorsString);
        return false;
    

    // This piece makes sure it actually matches your known root
    bool isValid = chain.ChainElements
        .Cast<X509ChainElement>()
        .Any(x => x.Certificate.RawData.SequenceEqual(caCertificate2.GetRawCertData()));

    if (!isValid)
    
        Log.Error("Trust chain did not complete to the known authority anchor. Thumbprints did not match.");
    

    return isValid;

设置证书:

caCertificate2 = new X509Certificate2("auth/ca.crt", "");
var clientCertificate2 = new X509Certificate2("auth/client.pfx", "");

传递委托方法

ServerCertificateValidationCallback(ValidateServerCertficate)

client.pfx 是使用 KEY 和 CERT 生成的:

openssl pkcs12 -export -in client.crt -inkey client.key -out client.pfx

【讨论】:

以上是关于将自签名证书与 .NET 的 HttpWebRequest/Response 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

仅将自签名 SSL 证书用于 Web 服务

将自签名证书添加到iphone Simulator?

将自签名证书链添加到密钥库

如何将自签名 SSL 证书上传到 Elastic Beanstalk

如何将自签名证书正确导入默认情况下可供所有 Java 应用程序使用的 Java 密钥库?

如何将自签名证书分配给 AWS 弹性 beantalk 应用程序