65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Core 中如何使用证书

starIconstarIconstarIconstarIconstarIcon

5.00/5 (33投票s)

2021年10月14日

CPOL

22分钟阅读

viewsIcon

74701

什么是 ASP.NET 中的证书,我们为什么需要它们,如何创建用于测试的自签名证书,以及如何在 ASP.NET Core 中使用证书

引言

最近,为您的 Web 资源使用 HTTPS 协议已成为所有相对大型 Web 项目的强制性要求。这项技术基于所谓的证书。以前,您需要付费才能为您的 Web 服务器获取证书。但现在,我们有了 Let's Encrypt 等服务,您可以免费获取证书。这就是为什么价格不再是不使用 HTTPS 的理由。

最简单的情况下,证书允许您在客户端和服务器之间建立受保护的连接。但这并非它的全部功能。例如,我在 Pluralsight 上看到一个名为 Microservices Security 的在线课程。其中提到了一项称为“双向传输层安全”的技术。它不仅允许客户端确保它正在与正确的服务器进行交互,还允许服务器对客户端进行身份验证。

因此,开发人员必须了解如何处理证书。出于这个原因,我决定写这篇文章。我希望它能成为人们找到有关证书基本知识的地方。我不认为专家会在这里找到有趣的东西,但我希望它对初学者和那些想更新知识的人有用。

本文将包含以下章节

我们为什么需要证书?

在我们开始处理证书之前,我们需要了解为什么需要它们。让我们看看两个人。传统上,我们称他们为 Alice 和 Bob。他们需要彼此交流。但唯一的方法是通过公开通信渠道交换消息

Alice and Bob

所有图标均由 Vitaly Gorbachev 在 Flaticon 创建

不幸的是,由于渠道是公开的,任何人都可以阅读甚至更改 Alice 和 Bob 之间发送的消息

Man in the middle

这种情况被称为“中间人攻击”。

Alice 和 Bob 如何保护自己免受这种危险?加密系统应运而生。最古老、最广泛使用的加密系统是具有对称密钥的系统。在这种情况下,Alice 和 Bob 必须拥有完全相同的密钥(因此称为对称密钥),并且除了他们之外,没有人知道这些密钥。然后,使用任何对称加密系统,他们都可以通过公开通信渠道安全地交换消息,而不必担心黑客能够读取或更改消息。

但是,黑客仍然可以重放他之前看到的其中一条或多条消息。在某些情况下,这可能构成严重危险(想象一下黑客可以重放从一个账户转账到另一个账户的请求)。但这个问题在所有现代通信系统中都得到了有效解决。(例如,您可以在每条消息中添加一个序列号。如果接收方消息中的数字与预期数字不符,则会丢弃该消息)。

Symmetric encryption

但让我们回到我们的 Alice 和 Bob。看起来他们的问题已经解决了。但事实并非如此。问题是如何让他们获得相同的加密密钥,而不让任何人获得它们。毕竟,他们只能通过公共渠道进行通信。通过此渠道传递密钥也将很简单地传递给黑客。在这种情况下,他将能够解密和更改 Alice 和 Bob 的消息。

我们该怎么办?这就是非对称加密或公钥加密系统应运而生的地方。它的主要思想如下。假设 Alice 想给 Bob 发送一条消息。现在 Bob 生成的不是一个,而是两个密钥——公钥和私钥。公钥不是秘密。Bob 可以将其交给任何想与他交谈的人。但他会保密私钥,并且不向任何人展示,甚至不向 Alice 展示。诀窍在于,如果一条消息是用公钥加密的,那么它只能用私钥解密。反之,用私钥加密的消息只能用公钥解密。

现在很清楚 Alice 和 Bob 应该如何行事。他们各自生成自己的公钥和私钥。然后他们通过通信渠道交换他们的公钥。由于公钥不是秘密,它们可以通过公共渠道传输。但是 Alice 和 Bob 保留他们的私钥。假设 Bob 想将消息发送给 Alice。他用她的公钥加密该消息,并通过渠道发送加密的消息。只有拥有私钥的人才能解密这条消息(这意味着只有 Alice 可以这样做)。黑客无法解密它。

Encryption with public key

实际上,事情比这要复杂一些。您看,公钥加密比对称加密慢得多。因此,不方便用这种方式加密大量数据。这就是为什么当 Bob 想和 Alice 交谈时,他会这样做。他为对称加密系统生成一个新的密钥(通常称为会话密钥)。然后他用 Alice 的公钥加密这个会话密钥并将它发送给她。现在 Alice 和 Bob 有了一个只有他们知道的对称密钥。从现在开始,他们可以使用快速的对称加密算法。

看起来我们解决了这个问题。但事情并非如此简单。控制通信渠道的黑客有一些话要说。问题再次出现在密钥分发机制上,但现在是公钥。让我们看看会发生什么。

假设 Alice 生成了一对公钥和私钥。现在她想把她的公钥给 Bob。她通过通信渠道发送这个密钥。这时,黑客截获了这个密钥,并没有让 Bob 收到。相反,黑客生成了自己的公钥和私钥对。然后他将他的公钥发送给 Bob,说这是 Alice 的公钥。黑客把 Alice 的真实公钥留给自己

Attack on public key distribution

是的,现在我们有很多不同的密钥。让我们看看这一切是如何运作的。假设 Bob 想给 Alice 发送一条消息。他用他认为属于 Alice 的公钥加密该消息。但实际上,这是黑客的密钥。黑客截获了这条消息,并没有让 Alice 收到。由于这条消息是用黑客的公钥加密的,他可以用他的私钥解密它,阅读并根据需要更改它。之后,他用 Alice 的真实公钥(记住黑客把她的公钥留给了他)加密它并发送给她。Alice 用她的私钥解密它,没有任何问题。因此,Alice 收到 Bob 的消息,并不知道这条消息已被黑客读取并可能被修改。

我们能做些什么来避免这种情况?于是,我们接近了证书。想象一下 Alice 通过公共渠道分发的不仅仅是她的公钥,而是一个带有标签的密钥,上面写着密钥属于 Alice。这个标签还包含一个 Alice 和 Bob 都信任的受尊敬人物的签名

Signed public key

假定密钥和标签是一体的。标签不能从一个密钥上取下并放到另一个密钥上。在这种情况下,如果黑客无法伪造签名,他也无法伪造密钥。如果 Bob 收到一个带有标签的密钥,上面写着这是 Alice 的密钥,并且上面有受信任人物的签名,他就可以确信这是 Alice 的密钥,而不是其他人的。

您可以认为证书就是带有这样标签的密钥。但在数字世界中,它是如何运作的呢?

在数字世界中,一切都可以表示为一串比特(零和一)。密钥也是如此。我们该如何为这样的比特串创建数字签名?这个签名必须具有以下属性

  • 它应该很短。想象一下您想为电影文件创建数字签名。这样的文件可能会占用磁盘几十 GB 的空间。如果我们的签名大小相同,那么将其与文件一起传输将会很困难。
  • 伪造它应该是(或者在实践中非常困难)不可能的。否则,黑客仍然可以强迫 Bob 用他自己的密钥代替 Alice 的密钥。

我们如何创建这样的签名?我们可以这样做。首先,我们将计算所谓的哈希值,用于我们的比特串。您将比特串发送到某个函数(称为哈希函数)的输入,该函数会返回另一个比特串,但已经很短了。这个输出串称为哈希值。所有现代哈希函数都具有以下属性

  • 对于任意长度的输入序列,它们都会生成相同长度的哈希值。通常,这个长度不超过几十个字节。记住,我们的签名必须很短。哈希值的这个属性使其易于在签名中使用。
  • 如果您只知道哈希值,您将无法获得创建该哈希值的输入序列。这意味着您无法从哈希值恢复输入序列。
  • 如果您有一个比特串的哈希值,您就无法指定另一个具有相同哈希值的比特串。确实,有许多不同长度为 1GB 的文件。但对于其中任何一个,您都可以计算出一个例如 32 字节的哈希值。长度为 32 字节的不同序列比长度为 1GB 的不同文件要少得多。这意味着必须有两个长度为 1GB 的不同文件具有相同的哈希值。然而,如果您知道其中一个文件及其哈希值,您将无法指定另一个产生相同哈希值的文件。

但关于哈希值就够了。不幸的是,哈希值本身不适合作为签名的角色。是的,它很短。但任何人都可以计算出来。黑客可以为他的公钥计算哈希值,没有什么能阻止他这样做。我们如何才能使哈希值难以伪造?而在这里,公钥加密又派上用场了。

请记住,我说过 Alice 和 Bob 应该信任密钥标签上的签名。假设 Alice 和 Bob 信任“非常重要人物”的签名。非常重要人物如何签名密钥?为此,他会生成自己的公钥和私钥对。他将他的公钥交给 Alice 和 Bob,并保密私钥。当他需要签署 Alice 的公钥时,他会这样做。首先,他计算 Alice 密钥的哈希值,然后用他的私钥对其进行加密。用“非常重要人物”(通常称为证书颁发机构)的私钥加密的哈希值就是签名。由于没有人知道“非常重要人物”的私钥,所以没有人能够伪造他的签名。

现在我们明白了如何创建签名。但我们也需要知道如何验证它,如何确保签名未被伪造。假设 Bob 有一个密钥。标签上写着这是 Alice 的公钥。此外,还有一个“非常重要人物”的签名。但如何检查呢?首先,Bob 计算接收到的公钥的哈希值。记住,每个人都可以做到。然后 Bob 用“非常重要人物”的公钥解密签名。正如我之前所说,签名只是一个加密的哈希值。之后,Bob 比较两个哈希值:他计算出的哈希值,以及他从解密的签名中获得的哈希值。如果它们相等,那么一切都很好,Bob 可以确信这是 Alice 的密钥。但如果哈希值不同,则密钥不可信。由于黑客无法创建正确的签名,他无法强迫 Bob 信任错误的密钥。

因此,证书只是一个密钥及其标签。然而,在实践中,证书中添加了许多额外信息

  • 密钥的所有者。在我们的例子中,这是 Alice。
  • 密钥的有效期从哪个日期到哪个日期。
  • 谁签署了密钥。在我们的例子中,这是“非常重要人物”。此信息是必需的,因为实际上,不同的证书颁发机构可以签署密钥。
  • 用于计算哈希值和创建签名的算法。
  • ...以及任何附加信息。

对所有这些数据都创建了哈希值和签名,因此黑客无法伪造其中的任何内容。

但我们的严格方案中仍然存在一个漏洞。我希望您已经明白了我的意思。Alice 和 Bob 如何获得“非常重要人物”的公钥?如果黑客可以将此密钥替换为他自己的密钥,我们的整个系统将崩溃。

当然,“非常重要人物”的公钥是通过证书分发的,但现在由“非常非常重要人物”签名。嗯……但是“非常非常重要人物”的公钥是如何分发的?当然是带有证书。好吧,您知道……一直都是证书

但说笑归说笑。确实,Alice 的证书可以由“非常重要人物”的证书签名。而他的证书可以由“非常非常重要人物”的证书签名。这称为信任链。但这条链并不是无限的。它通常以根证书结束。这个证书没有被任何人签名,更确切地说,它是自签名的(自签名证书)。通常,根证书属于非常可靠的公司,他们的工作是用他们的根证书签名其他证书。

以前,公司收取证书签名费用。但现在我们有了 Let's Encrypt 等服务,它们免费提供服务。我认为许多大公司已经意识到,免费提供证书并使互联网成为一个更安全的地方,比拥有大量保护不力的网站(每个网站都可以被用作攻击这些大公司的平台)要好。这有点像杀毒软件。二十年前,我们必须为此付费。现在,一个人可以轻松找到免费的高质量杀毒软件安装在个人电脑上。

但让我们回到我们的证书。我们还有最后一个问题。为什么我们信任根证书?什么能阻止黑客替换它们?原因是它们如何到达 Alice 和 Bob 的计算机。您看,它们不是通过开放的通信渠道传输的,而是与操作系统一起提供的。最近,一些浏览器开始附带自己的受信任证书集。

就是这样。这就是我想说的关于证书的所有内容。它们有很多有趣的事情,例如证书的弃用和吊销机制,但在这里我们不谈论这些。让我们继续实际操作。

证书创建

我希望我已经说服您,证书是一件重要且必要的事情。而您,作为一个开发人员,已经决定是时候学习如何使用它们了。如果您从 Visual Studio 创建 ASP.NET Core 项目,您可以简单地选中配置 HTTPS 复选框,所有必需的基础设施都会为您准备好

Configure for HTTPS

但我想向您展示如何创建自己的证书来测试您的应用程序。首先,我将创建一个自签名证书,一个由其自身签名的证书。接下来,我将向您展示如何将此证书安装到您的系统中,以便它开始信任该证书。

让我们开始吧。我们所需的一切都已包含在 .NET Core 中。让我们创建一个控制台应用程序并使用一些有用的命名空间

using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

然后我们需要创建一个公钥和私钥对。公钥的安全分发是证书的工作

// Generate private-public key pair
var rsaKey = RSA.Create(2048);

然后我们需要创建一个证书请求

// Describe certificate
string subject = "CN=localhost";

// Create certificate request
var certificateRequest = new CertificateRequest(
    subject,
    rsaKey,
    HashAlgorithmName.SHA256,
    RSASignaturePadding.Pkcs1
);

证书请求包含有关此证书颁发给谁的信息(subject 变量)。如果我们希望该证书由 www.example.com 处的 Web 服务器使用,则 subject 变量应等于 CN=www.example.com。在我们的例子中,我们想在 localhost 上测试我们的 Web 服务器。这就是为什么 subject 变量的值等于 CN=localhost

接下来,我们将密钥对传递给证书请求,并指定应使用哪些算法来计算哈希值和签名。

现在我们需要提供一些关于我们需要哪种证书的附加信息。我们表明我们不想用它来签署其他证书

certificateRequest.CertificateExtensions.Add(
    new X509BasicConstraintsExtension(
        certificateAuthority: false,
        hasPathLengthConstraint: false,
        pathLengthConstraint: 0,
        critical: true
    )
);

然后有一些有趣的东西。您看,证书只是一个加密密钥存储。这些密钥可用于各种目的。我们已经看到它们可用于数字签名和会话密钥加密。但还有其他用途。现在我们必须指定我们的证书如何使用

certificateRequest.CertificateExtensions.Add(
    new X509KeyUsageExtension(
        keyUsages:
            X509KeyUsageFlags.DigitalSignature
            | X509KeyUsageFlags.KeyEncipherment,
        critical: false
    )
);

您可以自己查看 X509KeyUsageFlags 枚举,其中列出了证书的各种使用领域。

接下来,我们提供一个公钥用于标识

certificateRequest.CertificateExtensions.Add(
    new X509SubjectKeyIdentifierExtension(
        key: certificateRequest.PublicKey,
        critical: false
    )
);

这里有些黑魔法。正如我之前告诉您的,如果您想使用该证书保护 www.example.com 网站,其 subject 字段必须包含 CN=www.example.com。但这不足以让 Chrome 浏览器信任。它们要求 Subject Alternative Name 字段必须包含 DNS Name=www.example.com。在我们的例子中,它必须包含 DNS Name=localhost。否则 Chrome 将不信任此类证书。不幸的是,我没有找到一种方便的方法来设置证书的 Subject Alternative Name 字段。但以下代码片段将其设置为 DNS Name=localhost

certificateRequest.CertificateExtensions.Add(
    new X509Extension(
        new AsnEncodedData(
            "Subject Alternative Name",
            new byte[] { 48, 11, 130, 9, 108, 111, 99, 97, 108, 104, 111, 115, 116 }
        ),
        false
    )
);

就是这样。我们的证书请求已准备就绪。现在我们可以创建证书本身了

var expireAt = DateTimeOffset.Now.AddYears(5);

var certificate = certificateRequest.CreateSelfSigned(DateTimeOffset.Now, expireAt);

在这里,我们表明证书将在当前时间起有效五年。

现在我们有了一个证书。但它目前只存在于计算机内存中。要能够将其安装到我们的系统中,我们需要将其以 PFX 格式写入文件。但这里有一个障碍。我们想要的文件必须同时包含公钥和私钥,因为服务器必须执行加密和解密。但出于安全原因,我们的证书不能用于导出私钥。我们可以创建一个准备好导出的证书,如下所示

// Export certificate with private key
var exportableCertificate = new X509Certificate2(
    certificate.Export(X509ContentType.Cert),
    (string)null,
    X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet
).CopyWithPrivateKey(rsaKey);

为了方便起见,我们可以添加描述

exportableCertificate.FriendlyName = 
    "Ivan Yakimov Test-only Certificate For Client Authorization";

现在我们可以将证书导出到文件。由于该文件还包含私钥,因此对其进行密码保护是合理的。在这种情况下,即使文件被盗,罪犯也无法使用它

// Create password for certificate protection
var passwordForCertificateProtection = new SecureString();
foreach (var @char in "p@ssw0rd")
{
    passwordForCertificateProtection.AppendChar(@char);
}

// Export certificate to a file.
File.WriteAllBytes(
    "certificateForServerAuthorization.pfx",
    exportableCertificate.Export(
        X509ContentType.Pfx,
        passwordForCertificateProtection
    )
);

因此,我们有一个可以用来保护 Web 服务器的证书文件。但您也可以创建一个证书来验证此服务器的客户端。创建过程与服务器证书几乎相同,但 subject 字段可以包含任何内容,并且我们不再需要 Subject Alternative Name 字段

// Generate private-public key pair
var rsaKey = RSA.Create(2048);

// Describe certificate
string subject = "CN=Ivan Yakimov";

// Create certificate request
var certificateRequest = new CertificateRequest(
    subject,
    rsaKey,
    HashAlgorithmName.SHA256,
    RSASignaturePadding.Pkcs1
);

certificateRequest.CertificateExtensions.Add(
    new X509BasicConstraintsExtension(
        certificateAuthority: false,
        hasPathLengthConstraint: false,
        pathLengthConstraint: 0,
        critical: true
    )
);

certificateRequest.CertificateExtensions.Add(
    new X509KeyUsageExtension(
        keyUsages:
            X509KeyUsageFlags.DigitalSignature
            | X509KeyUsageFlags.KeyEncipherment,
        critical: false
    )
);

certificateRequest.CertificateExtensions.Add(
    new X509SubjectKeyIdentifierExtension(
        key: certificateRequest.PublicKey,
        critical: false
    )
);

var expireAt = DateTimeOffset.Now.AddYears(5);

var certificate = certificateRequest.CreateSelfSigned(DateTimeOffset.Now, expireAt);

// Export certificate with private key
var exportableCertificate = new X509Certificate2(
    certificate.Export(X509ContentType.Cert),
    (string)null,
    X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet
).CopyWithPrivateKey(rsaKey);

exportableCertificate.FriendlyName = 
    "Ivan Yakimov Test-only Certificate For Client Authorization";

// Create password for certificate protection
var passwordForCertificateProtection = new SecureString();
foreach (var @char in "p@ssw0rd")
{
    passwordForCertificateProtection.AppendChar(@char);
}

// Export certificate to a file.
File.WriteAllBytes(
    "certificateForClientAuthorization.pfx",
    exportableCertificate.Export(
        X509ContentType.Pfx,
        passwordForCertificateProtection
    )
);

现在我们可以将创建的证书安装到系统中。在 Windows 中执行此操作,双击 PFX 证书文件。向导窗口将打开。指定您只想为当前用户安装证书,而不是为整个计算机安装

Install a certificate for the current user

在下一个屏幕上,您可以指定证书文件的路径。保留原样

Selecting the certificate file

在下一个屏幕上,输入您用于保护证书文件的密码

Entering a password

然后指定您想将证书安装到受信任的根证书颁发机构

Selecting a storage

还记得我们之前讨论过的证书信任链吗?此受信任根证书颁发机构存储库存储这些最终(根)证书,系统会在不进行额外检查的情况下信任它们。

证书导入配置到此结束。然后您只需点击“下一步”、“完成”和“确定”。

现在我们的证书已存在于受信任的根证书颁发机构存储中。您可以通过在控制面板中点击“管理用户证书”链接来打开它

Management of user certificates

这是我们的证书的样子

Our certificate

用于客户端身份验证的证书可以以相同的方式安装。

在将这些证书用于 .NET 代码之前,我想向您展示另一种创建自签名证书的方法。如果您不想编写证书创建程序,但您有 PowerShell,您可以使用它来创建证书。

这是生成用于保护服务器的证书的代码

$certificate = New-SelfSignedCertificate `
    -Subject localhost `
    -DnsName localhost `
    -KeyAlgorithm RSA `
    -KeyLength 2048 `
    -NotBefore (Get-Date) `
    -NotAfter (Get-Date).AddYears(5) `
    -FriendlyName "Ivan Yakimov Test-only Certificate For Server Authorization" `
    -HashAlgorithm SHA256 `
    -KeyUsage DigitalSignature, KeyEncipherment, DataEncipherment `
    -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.1")

$pfxPassword = ConvertTo-SecureString `
    -String "p@ssw0rd" `
    -Force `
    -AsPlainText

Export-PfxCertificate `
    -Cert $certificate `
    -FilePath "certificateForServerAuthorization.pfx" `
    -Password $pfxPassword

New-SelfSignedCertificateExport-PfxCertificate 命令来自 pki 模块。我希望您现在已经能够理解此处各种参数的含义。

这是用于创建客户端身份验证证书的代码

$certificate = New-SelfSignedCertificate `
      -Type Custom `
      -Subject "Ivan Yakimov" `
      -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.2") `
      -FriendlyName "Ivan Yakimov Test-only Certificate For Client Authorization" `
      -KeyUsage DigitalSignature `
      -KeyAlgorithm RSA `
      -KeyLength 2048

$pfxPassword = ConvertTo-SecureString `
    -String "p@ssw0rd" `
    -Force `
    -AsPlainText

Export-PfxCertificate `
    -Cert $certificate `
    -FilePath "certificateForClientAuthorization.pfx" `
    -Password $pfxPassword

现在让我们看看如何使用这些证书。

如何在 .NET 代码中使用证书

因此,我们有一个用 ASP.NET Core 编写的 Web 服务器。我们想用我们的证书来保护它。首先,我们需要在服务器代码中获取此证书。有两种方法可以做到这一点。

第一种选择是从 PFX 文件获取证书。如果您有一个已安装在受信任证书存储中的证书文件,则可以使用此选项。在这种情况下,您可以如下获取证书

var certificate = new X509Certificate2(
    "certificateForServerAuthorization.pfx",
    "p@ssw0rd"
);

这里 certificateForServerAuthorization.pfx 是证书文件的路径,p@ssw0rd 是您用于保护它的密码。

但是,您可能并不总是有权访问证书文件。在这种情况下,您可以直接从存储中获取证书

var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var certificate = store.Certificates.OfType<X509Certificate2>()
    .First(c => c.FriendlyName == "Ivan Yakimov Test-only Certificate For Server Authorization");

StoreLocation.CurrentUser 值表示我们要使用当前用户的证书存储,而不是整个计算机的。StoreName.Root 值表示我们必须在受信任的根证书颁发机构存储中查找证书。在这里,为了简单起见,我按名称查找证书,但您可以指定任何合适的标准。

现在我们有了一个证书。让我们让我们的服务器使用它。为此,我们需要更改 Program.cs 文件的代码

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args)
    {
        var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
        store.Open(OpenFlags.ReadOnly);
        var certificate = store.Certificates.OfType<X509Certificate2>()
            .First(c => c.FriendlyName == 
            "Ivan Yakimov Test-only Certificate For Server Authorization");

        return Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder
                    .UseKestrel(options =>
                    {
                        options.Listen(System.Net.IPAddress.Loopback, 44321, listenOptions =>
                        {
                            var connectionOptions = new HttpsConnectionAdapterOptions();
                            connectionOptions.ServerCertificate = certificate;

                            listenOptions.UseHttps(connectionOptions);
                        });
                    })
                    .UseStartup<Startup>();
            });
    }
}

正如您所看到的,所有的魔术都发生在 UseKestrel 方法中。在这里,我们指定了要使用的端口以及要应用的证书。

现在浏览器认为我们的站点受到保护

Protected site

但我们并不总是通过浏览器与 Web 服务器交互。有时,我们需要从代码中联系它。这时 HttpClient 就派上用场了

var client = new HttpClient()
{
    BaseAddress = new Uri("https://:44321")
};

var result = await client.GetAsync("data");

var content = await result.Content.ReadAsStringAsync();

Console.WriteLine(content);

事实上,标准的 HttpClient 会验证服务器证书,如果无法验证其真实性,将不会建立连接。但是,如果我们想做一些额外的检查呢?例如,您可能想检查是谁签署了服务器证书。或者您想检查该证书的某个非标准字段。这可以做到。我们只需要定义一个系统在执行标准证书验证后调用的方法

var handler = new HttpClientHandler()
{
    ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => {
        if (errors != SslPolicyErrors.None) return false;

        return true;
    }
};

var client = new HttpClient(handler)
{
    BaseAddress = new Uri("https://:44321")
};

您将此方法分配给 HttpClientHandler 实例的 ServerCertificateCustomValidationCallback 属性。该实例必须传递给 HttpClient 的构造函数。

让我们仔细看看这个验证方法。正如我之前所说,它是被调用在标准检查之后,而不是代替标准检查。这个检查的结果可以从该方法的最后一个参数(errors)中获得。如果此值不等于 SslPolicyErrors.No,则标准验证失败,您不能信任此类证书。此方法还允许您获取有关以下信息

  • 请求(request)。
  • 服务器证书(certificate)。
  • 此证书的信任链(chain)。如果您对这些信息感兴趣,可以在此处找到标准检查失败的详细原因。

所以,现在我们知道如何用证书保护我们的服务器。但证书也可以用于客户端身份验证。在这种情况下,服务器将只处理那些提供“正确”证书的客户端的请求。如果一个证书通过了标准验证,并且还满足服务器要求的任何附加条件,则该证书被认为是正确的。

让我们看看如何让服务器要求客户端提供证书。为此,您只需要一个小小的代码更改

return Host.CreateDefaultBuilder(args)
    .UseSerilog()
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder
            .UseKestrel(options =>
            {
                options.Listen(System.Net.IPAddress.Loopback, 44321, listenOptions =>
                {
                    var connectionOptions = new HttpsConnectionAdapterOptions();
                    connectionOptions.ServerCertificate = certificate;

                    connectionOptions.ClientCertificateMode = 
                                      ClientCertificateMode.RequireCertificate;
                    connectionOptions.ClientCertificateValidation = 
                                           (certificate, chain, errors) =>
                    {
                        if (errors != SslPolicyErrors.None) return false;

                        // Here is your code...

                        return true;
                    };

                    listenOptions.UseHttps(connectionOptions);
                });
            })
            .UseStartup<Startup>();
    });

正如您所看到的,我们额外设置了 HttpsConnectionAdapterOptions 对象的两个属性。使用 ClientCertificateMode 属性,我们确定客户端证书是必需的,并使用 ClientCertificateValidation 属性,我们为附加的证书验证设置了自定义函数。

如果在浏览器中打开这样的站点,它会询问您要使用哪个客户端证书

Choosing a client certificate in browser

剩下要做的就是为 HttpClient 提供客户端证书。您获取证书的方式与为服务器获取证书的方式相同。其他更改也很小

var handler = new HttpClientHandler()
{
    ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) => {
        if (errors != SslPolicyErrors.None) return false;

        // Here is your code...

        return true;
    }
};

handler.ClientCertificates.Add(certificate);

var client = new HttpClient(handler)
{
    BaseAddress = new Uri("https://:44321")
};

您只需将证书添加到 HttpClientHandler 对象的 ClientCertificates 集合中。

结论

因此,我们的文章到此结束。它相当长。我把它构思成一个可以让我将来刷新关于证书及其使用的知识的地方。我希望这对您也有用。

附录

在我的工作中,我使用了以下材料

本文的源代码可以在 GitHub 上找到。

历史

  • 2021 年 10 月 14 日:初始版本
© . All rights reserved.