C# 在 .NET 的 HttpWebRequest/Response 中使用自签名证书

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/526711/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 06:47:53  来源:igfitidea点击:

Using a self-signed certificate with .NET's HttpWebRequest/Response

c#sslhttpwebrequestssl-certificate

提问by Dominic Scheirlinck

I'm trying to connect to an API that uses a self-signed SSL certificate. I'm doing so using .NET's HttpWebRequest and HttpWebResponse objects. And I'm getting an exception that:

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

The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.

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

I understand what this means. And I understand why.NET feels it should warn me and close the connection. But in this case, I'd like to just connect to the API anyway, man-in-the-middle attacks be damned.

我明白这意味着什么。我明白为什么.NET 觉得它应该警告我并关闭连接。但在这种情况下,无论如何我只想连接到 API,中间人攻击该死。

So, how do I go about adding an exception for this self-signed certificate? Or is the approach to tell HttpWebRequest/Response not to validate the certificate at all? How would I do that?

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

采纳答案by devstuff

@Domster: that works, but you might want to enforce a bit of security by checking if the certificate hash matches what you expect. So an expanded version looks a bit like this (based on some live code we're using):

@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;
}

回答by Dominic Scheirlinck

Turns out, if you just want to disable certificate validation altogether, you can change the ServerCertificateValidationCallback on the ServicePointManager, like so:

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

ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };

This will validate all certificates (including invalid, expired or self-signed ones).

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

回答by wgthom

Add the self signed cert to the Local Computer Trusted Root Certification Authorities

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

You can import the cert by running the MMC as Administrator.

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

How to: View Certificates with the MMC Snap-in

如何:使用 MMC 管理单元查看证书

回答by Nathan Baulch

The scope of the validation callback used in Domster's answercan be limited to a specific request using the sender parameter on the ServerCertificateValidationCallbackdelegate. The following simple scope class uses this technique to temporarily wire up a validation callback that only executes for a given request object.

Domster 的回答中使用的验证回调的范围可以限制为使用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;
    }
}

The above class can be used to ignore all certificate errors for a specific request as follows:

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

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

回答by crokusek

Just building on answer from devstuffto include subject and issuer...comments welcome...

只是基于devstuff 的回答,包括主题和发行人...欢迎评论...

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));            
    }
}

回答by user2117074

Note, that in .NET 4.5 you can override SSL validation per HttpWebRequest itself (and not via global delegate which affects all requests):

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

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

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

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

回答by nicki

One thing to keep in mind is that having the ServicePointManager.ServerCertificateValidationCallback does not seem to mean that the CRL check and servername validation are not done, it only provides a means to override their result. So your service might still take a while to get a CRL, you'll only know afterwards that it failed some checks.

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

回答by TravisWhidden

To add as a possible help to someone else... If you want it to prompt the user to install the self-signed cert, you can use this code (modified from above).

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

Does not require admin rights, installs to the local users trusted profiles:

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

    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;
    }

This seems to work well for our application, and if the user presses no, the communication will not work.

这对我们的应用程序来说似乎很有效,如果用户按“否”,则通信将无法进行。

Update: 2015-12-11- Changed StoreName.Root to StoreName.My - My will install into the local users store, instead of Root. Root on some systems will not work, even if you "run as administrator"

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

回答by Simon Ejsing

I was running into the same problem as the OP where the web request would throw that exact exception. I had everything setup correctly I thought, the certificate was installed, I could locate it in the machine store just fine and attach it to the web request, and I had disabled the verification of certificates on the request context.

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

It turned out that I was running under my user account, and that the certificate was installed to the machine store. This caused the web request to throw this exception. To solve the problem I had to either be running as administrator or install the certificate to the user store and read it from there.

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

It would seem that C# is able to find the certificate in the machine store even though it can't be used with a web request, and that this results in the OP's exception being thrown once the web request is issued.

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

回答by Alex

First of all - I apologize, because I have used the solution that was described by @devstuff. However, I have found some ways to improve it.

首先 - 我很抱歉,因为我使用了@devstuff 描述的解决方案。但是,我找到了一些方法来改进它。

  • adding self-signed certificates handling
  • comparison by the Raw data of certificates
  • actual certificate authority validation
  • some additional comments and improvements
  • 添加自签名证书处理
  • 证书原始数据对比
  • 实际的证书颁发机构验证
  • 一些额外的评论和改进

Here is my modification:

这是我的修改:

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;
}

setting certificates:

设置证书:

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

passing delegate method

传递委托方法

ServerCertificateValidationCallback(ValidateServerCertficate)

client.pfxis generated with KEY and CERT as such:

client.pfx使用 KEY 和 CERT 生成,如下所示:

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