TLS/SSL 最佳做法

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

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

选择 TLS 版本

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

将决定交给操作系统,会自动使用可用的最新版本 TLS,并允许应用程序在操作系统升级后获取更改。 作系统还可能阻止使用不再被视为安全的 TLS 版本。

选择密码套件

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

注释

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

指定本地证书

作为服务器进行身份验证时, SslStream 始终需要证书。 以客户端身份进行身份验证时,如果服务器请求提供用于双向 TLS(mTLS)的证书,你还需要提供该证书。 在这两个 X509Certificate2 角色中,证书必须是包含私钥的实例。

最近的.NET版本以对称方式处理服务器和客户端,因此以下指南适用于应用程序是作为服务器、客户端还是同时进行身份验证。

您可以通过几种方式将该证书提供给 SslStream。

以服务器身份进行身份验证时:

以客户端身份进行身份验证时:

注释

该ClientCertificateContext属性从 .NET 8 开始可用。

为了获得更好的性能,请使用证书上下文属性(ServerCertificateContext 或 ClientCertificateContext)。 以其他方式提供证书时, SslStream 在内部创建证书 SslStreamCertificateContext 。 创建上下文会生成一个 X509ChainCPU 密集型操作,因此,创建上下文一次,并在多个 SslStream 实例之间重复使用该上下文会更高效。

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

将中间证书发送到对等方

当中间证书颁发机构颁发本地证书时,如果握手中不包含中间证书,对等方可能无法构建完整的证书链。 若要发送这些中间证书,请使用 Create 方法创建一个 SslStreamCertificateContext,并通过 additionalCertificates 参数传递中间证书:

X509Certificate2 leafCertificate = GetLeafCertificate();
X509Certificate2Collection intermediates = GetIntermediateCertificates();

SslStreamCertificateContext certificateContext =
    SslStreamCertificateContext.Create(leafCertificate, intermediates);

// When you authenticate as a server.
serverOptions.ServerCertificateContext = certificateContext;

// When you authenticate as a client for mutual TLS.
clientOptions.ClientCertificateContext = certificateContext;

上下文对这两种角色的作用方式相同。 在客户端上,建议使用证书上下文发送中间项,因为替代方法(将中间项添加到计算机或用户证书存储)会影响系统上的每个应用程序。

自定义 X509Certificate 验证

在某些情况下,默认证书验证过程不够充分,并且需要一些自定义验证逻辑。 可以通过指定 SslClientAuthenticationOptions.CertificateChainPolicy 或 SslServerAuthenticationOptions.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 时需要联系外部服务器,如果外部服务器响应缓慢,可能会导致应用程序面临拒绝服务攻击。 因此,服务器应用程序应使用 X509Chain 配置 CertificateChainPolicy 生成行为。

注释

从 .NET 11 开始,SslStream在将客户端证书验证为服务器时默认禁用 AIA 证书下载。 如果未提供任何自定义 CertificateChainPolicy ,则服务器不会尝试通过 AIA 提取缺少的中间证书。 有关详细信息,请参阅 默认情况下禁用的 SslStream 服务器端 AIA 证书下载。