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 需要实例 X509Certificate2 。 建议始终使用 X509Certificate2 包含私钥的实例。
可以通过多种方式将 SslStream服务器证书传递给:
- 可以直接作为参数传递给 SslStream.AuthenticateAsServerAsync,或通过 SslServerAuthenticationOptions.ServerCertificate 属性进行传递。
- 来自 SslServerAuthenticationOptions.ServerCertificateSelectionCallback 属性中的选择回调
- 通过在 SslStreamCertificateContext 属性中传递 SslServerAuthenticationOptions.ServerCertificateContext
建议的方法是使用 SslServerAuthenticationOptions.ServerCertificateContext 属性。 当证书通过另外两种方式之一获取时,SslStreamCertificateContext实现会在内部创建一个SslStream实例。 创建SslStreamCertificateContext涉及构建X509Chain,这是一项CPU密集型操作。 一次创建一个 SslStreamCertificateContext 并为多个 SslStream 实例重用它会更有效。
SslStreamCertificateContext重用实例还允许在 Linux 服务器上恢复 TLS 会话等其他功能。
自定义 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 生成行为。