TLS/SSL 最佳做法

TLS(传输层安全性)是一种加密协议,旨在保护两台计算机之间通过 Internet 的通信。 TLS 协议通过 SslStream 类在 .NET 中公开。

本文介绍在客户端和服务器之间设置安全通信的最佳做法,并假定使用 .NET。 有关 .NET Framework 的最佳做法,请参阅使用 .NET Framework 的传输层安全性 (TLS) 最佳做法

选择 TLS 版本

虽然可以通过 EnabledSslProtocols 属性指定要使用的 TLS 协议的版本,但建议使用 None 值(这是默认值)遵从操作系统设置。

将决策推迟到 OS 会自动使用可用的最新版本的 TLS,并允许应用程序在 OS 升级后进行更改。 操作系统也可能会阻止使用不再被视为安全的 TLS 版本。

选择密码套件

SslStream 允许用户通过 CipherSuitesPolicy 类指定 TLS 握手可以协商哪些密码套件。 与 TLS 版本一样,建议让 OS 决定与之协商的最佳密码套件,因此建议避免使用 CipherSuitesPolicy

注意

CipherSuitesPolicy 在 Windows 上不受支持,如果尝试对其进行实例化,将导致引发 NotSupportedException

指定服务器证书

作为服务器进行身份验证时,SslStream 需要一个 X509Certificate2 实例。 建议始终使用同样包含私钥的 X509Certificate2 实例。

可以通过多种方法将服务器证书传递给 SslStream

推荐的方法是使用 SslServerAuthenticationOptions.ServerCertificateContext 属性。 如果通过其他两种方式之一获得证书,SslStreamCertificateContext 实例将由 SslStream 实现在内部创建。 创建 SslStreamCertificateContext 涉及生成 X509Chain,这是一个 CPU 密集型操作。 一次创建一个 SslStreamCertificateContext 并为多个 SslStream 实例重用它会更有效。

重用 SslStreamCertificateContext 实例还可以启用其他功能,例如 Linux 服务器上的 TLS 会话恢复

自定义 X509Certificate 验证

在某些情况下,默认的证书验证过程不够充分,需要一些自定义验证逻辑。 可通过指定 SslClientAuthenticationOptions.CertificateChainPolicySslServerAuthenticationOptions.CertificateChainPolicy 自定义部分验证逻辑。 此外,也可通过 <System.Net.Security.SslClientAuthenticationOptions.RemoteCertificateValidationCallback> 属性提供完全自定义的逻辑。 有关详细信息,请参阅自定义证书信任

自定义证书信任

当遇到计算机不信任的任何证书颁发机构颁发的证书(包括自签名证书)时,默认的证书验证过程将失败。 解决此问题的一种可能方法是,将必要的颁发者证书添加到计算机的受信任存储。 但这可能会影响系统上的其他应用程序,而且并非始终可行。

替代解决方案是通过 X509ChainPolicy 指定自定义受信任的根证书。 要指定将在验证期间使用的代替系统信任列表的自定义信任列表,请考虑以下示例:

SslClientAuthenticationOptions clientOptions = new();

clientOptions.CertificateChainPolicy = new X509ChainPolicy()
{
    TrustMode = X509ChainTrustMode.CustomRootTrust,
    CustomTrustStore =
    {
        customIssuerCert
    }
};

使用上述策略配置的客户端将只接受 customIssuerCert 信任的证书。

忽略特定的验证错误

考虑没有持久时钟的 IoT 设备。 开机后,设备的时钟将从过去多年开始,因此所有证书都将被视为“尚未生效”。 请考虑以下显示忽略有效期冲突的验证回调实现的代码。

static bool CustomCertificateValidationCallback(
    object sender,
    X509Certificate? certificate,
    X509Chain? chain,
    SslPolicyErrors sslPolicyErrors)
{
    // Anything that would have been accepted by default is OK
    if (sslPolicyErrors == SslPolicyErrors.None)
    {
        return true;
    }
    
    // If there is something wrong other than a chain processing error, don't trust it.
    if (sslPolicyErrors != SslPolicyErrors.RemoteCertificateChainErrors)
    {
        return false;
    }
    
    Debug.Assert(chain is not null);

    // If the reason for RemoteCertificateChainError is that the chain built empty, don't trust it.
    if (chain.ChainStatus.Length == 0)
    {
        return false;
    }

    foreach (X509ChainStatus status in chain.ChainStatus)
    {
        // If an error other than `NotTimeValid` (or `NoError`) is present, don't trust it.
        if ((status.Status & ~X509ChainStatusFlags.NotTimeValid) != X509ChainStatusFlags.NoError)
        {
            return false;
        }
    }

    return true;
}

证书固定

另一种需要自定义证书验证的情况是,当客户端希望服务器使用特定证书或来自一小组已知证书的证书时。 这种做法称为证书固定。 以下代码片段显示验证回调,该验证回调检查服务器是否提供了具有特定已知公钥的证书。

static bool CustomCertificateValidationCallback(
    object sender,
    X509Certificate? certificate,
    X509Chain? chain,
    SslPolicyErrors sslPolicyErrors)
{
    // If there is something wrong other than a chain processing error, don't trust it.
    if ((sslPolicyErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0)
    {
        return false;
    }
    
    Debug.Assert(certificate is not null);

    const string ExpectedPublicKey =
        "3082010A0282010100C204ECF88CEE04C2B3D850D57058CC9318EB5C" +
        "A86849B022B5F9959EB12B2C763E6CC04B604C4CEAB2B4C00F80B6B0" +
        "F972C98602F95C415D132B7F71C44BBCE9942E5037A6671C618CF641" +
        "42C546D31687279F74EB0A9D11522621736C844C7955E4D16BE8063D" +
        "481552ADB328DBAAFF6EFF60954A776B39F124D131B6DD4DC0C4FC53" +
        "B96D42ADB57CFEAEF515D23348E72271C7C2147A6C28EA374ADFEA6C" +
        "B572B47E5AA216DC69B15744DB0A12ABDEC30F47745C4122E19AF91B" +
        "93E6AD2206292EB1BA491C0C279EA3FB8BF7407200AC9208D98C5784" +
        "538105CBE6FE6B5498402785C710BB7370EF6918410745557CF9643F" +
        "3D2CC3A97CEB931A4C86D1CA850203010001";

    return certificate.GetPublicKeyString().Equals(ExpectedPublicKey);
}

客户端证书验证的注意事项

服务器应用程序在要求和验证客户端证书时需要小心。 证书可以包含 AIA(授权信息访问) 扩展,该扩展指定可以下载颁发者证书的位置。 因此,在为客户端证书生成 X509Chain 时,服务器可以尝试从外部服务器下载颁发者证书。 同样,服务器可能需要联系外部服务器,以确保客户端证书没有被吊销。

如果外部服务器响应缓慢,则在生成和验证 X509Chain 时需要联系外部服务器,这可能会使应用程序遭受拒绝服务攻击。 因此,服务器应用程序应使用 CertificateChainPolicy 配置 X509Chain 生成行为。