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

87
我想连接一个使用自签名SSL证书的API,我正在使用.NET的HttpWebRequest和HttpWebResponse对象进行连接。但是我遇到了一个异常:

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

我知道这是什么意思。我也明白为什么.NET认为应该警告我并关闭连接。但是在这种情况下,我只想连接到API,不管中间人攻击是否存在。

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

10个回答

100

结果表明,如果您只想完全禁用证书验证,可以像下面这样更改ServicePointManager上的ServerCertificateValidationCallback:

ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };

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


2
非常适合对开发机进行快速测试。谢谢。 - Nate
3
这会影响到什么范围 - 应用程序域中的所有内容?应用程序池中的所有内容?机器上的所有内容? - codeulike
31
但要小心!强化学习的经验表明,这种开发技巧通常会被悄悄地加入到发布的产品中:世界上最危险的代码 - Doomjunky
5
这是一个在开发中有用的技巧,因此在其周围添加 #if DEBUG #endif 语句是你至少应该做的,以使其更安全,避免其出现在生产环境中。 - AndyD
4
除非这个人删除这个回答,否则我们将看到一个有趣的事实,即错误的答案获得的投票数比正确答案多得多。 - Lex Li
显示剩余3条评论

82

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

可能有人更喜欢下面的正确方法。无论如何,这种hack在紧急情况下是有效的,但您可能不应该编写这些类型的异常... 要么完全禁用检查(通过直接下面的建议),要么实际上指示计算机信任证书... - BrainSlugs83
4
禁用当然也是一种选择,但将证书添加到机器级根机构存储只能由管理员完成。我的解决方案无论哪种方式都有效。 - devstuff
我完全理解,但你问了,这仍然是我猜测为什么有人对你的答案进行了负面评价。而且,尽管更费力,但在我看来,wgthom在下面的回答仍然是最正确的。 - BrainSlugs83
顺便提一下,小心点,我认为ServerCertificateValidationCallback是静态的,甚至不是线程本地的。如果我没记错的话,那么一旦设置,它就会保持设置状态,直到你清除它。如果你只想在一个连接中使用它而不是所有其他连接中使用它,请非常小心处理并行请求。 - quetzalcoatl
3
这是最佳的做法。如果删除对sslPolicyErrors的检查,您实际上可以确保API证书始终是预期的证书。需要注意的一点是代码中的证书指纹是一个常量字节数组。按照原样编写将无法编译。请尝试使用静态只读字节数组。编译器会遇到困难,因为它需要new()运算符。 - Centijo
感谢 @Centijo 发现了这个错别字,已经修复。 - devstuff

52

1
请点赞此内容;这值得升级到4.5! - Lynn Crumbling
1
@FlorianWinter 是的,你必须采用用户devstuff的逻辑。 - Summer-Time

47
将自签名证书添加到本地计算机受信任的根证书颁发机构
您可以通过以管理员身份运行MMC来导入证书。
使用MMC快捷方式查看证书:如何:使用MMC快捷方式查看证书

5
我认为这是最正确的方式;只是人们太懒了,所以他们编写特殊异常来处理本应该避免的情况。 - BrainSlugs83
4
这种方法适用于Windows Mobile 6.5吗?第7版呢?在我的情况下,我不想在计划运行开发版本的每个移动设备上添加本地证书。在这种情况下,一个好的例外会让部署变得更加容易。懒惰还是高效,你说了算。 - Dominic Scheirlinck
3
你使用SSL证书是为了验证端点。如果你编写的代码绕过此功能,则无法进行适当的测试,可能会将该代码泄露到实际环境中。如果在客户端安装证书实在太麻烦,为什么不直接购买一个所有设备都信任的发行商颁发的证书呢? - Basic
1
@Basic 如果我记得这个特定案例,我需要几个通配符证书(有半打TLD连接到我们控制下的所有内容)。对于开发环境来说,这是一个难以证明的成本。在这种情况下,“被解决”的唯一代码而不是测试的是,在否则会抛出异常的地方没有抛出异常。无论您是否使用此解决方法,都应该测试特定的异常路径。最后,如果您无法将开发代码保留在生产环境之外,则SSL验证问题比您更大的问题。 - Dominic Scheirlinck
对于 Web 应用程序,请确保回收您的应用程序池或重新启动您的网站。个人而言,我只是重新编译了一下,然后它就可以工作了。对于我们的 WSDL 问题,证书验证似乎发生在初始化并被缓存。 - sonjz
我已经使用MMC将CA文件添加到受信任的CA中,但.NET应用程序不接受它。该应用程序不是我的:( CA证书没问题,因为如果我将其添加到Firefox或Cygwin中并进行测试,它可以正常工作。 有什么想法为什么会这样吗? - ArticIceJuice

36

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

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

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

6
这个回答需要更多赞 :) 它是使用HttpWebRequest对象跳过单个请求的证书验证最合理的解决方案。 - MikeJansen
我已经添加了这个,但仍然出现“请求被中止:无法创建 SSL/TLS 安全通道”的错误。 - vikingben
8
在多线程环境下,这并不能真正解决问题。 - Hans
1
太棒了!一篇五年前的帖子解决了我的问题。我在连接一个带有无效证书的旧卫星调制解调器设备时遇到了麻烦。谢谢! - WindyHen
我有点困惑/略感担忧!如果在没有先前回调的情况下返回SslPolicyErrors.None,这是否意味着我们最终会用“接受所有”策略覆盖默认策略?请参考此问题及其各种答案:https://dev59.com/hWox5IYBdhLWcg3wql9t。如果有人告诉我我错了,这段代码没问题,我会非常高兴! - MikeBeaton
SslPolicyErrors.None 不是返回值,而是与 errors 回调参数进行比较。 - Nathan Baulch

3

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

3

为了对他人提供帮助... 如果您想提示用户安装自签名证书,可以使用以下代码(修改自上面的代码)。

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

    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 - My将安装到本地用户存储中,而不是Root。在某些系统上,即使您以管理员身份运行,Root也无法工作。

如果在紧凑框架winCE上运行,那将是非常棒的。但store.Add(..)不可用。 - Dawit

3

首先,我要道歉,因为我使用了@devstuff描述的解决方案。然而,我已经找到一些改进它的方法。

  • 添加处理自签名证书的功能
  • 通过证书原始数据进行比较
  • 实际进行证书授权验证
  • 一些额外的注释和改进

这是我的修改:

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

1
我遇到了与OP相同的问题,即网络请求会抛出该异常。我认为所有设置都正确,证书已安装,我可以在机器存储中找到它并将其附加到网络请求中,并且我已经在请求上下文中禁用了证书验证。
事实证明,我是在我的用户帐户下运行的,而证书是安装在机器存储中的。这导致网络请求抛出此异常。要解决问题,我必须以管理员身份运行或将证书安装到用户存储中并从那里读取它。
似乎C#能够在机器存储中找到证书,即使无法与网络请求一起使用,这导致发出网络请求后出现OP的异常。

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

1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接