第十章 在TLS中使用X.509证书

在【第8章X.509证书和PKI】中,我们学习了X.509证书X.509 certificates),而在【第9章建立TLS连接并通过它们发送数据】中,我们了解了传输层安全Transport Layer SecurityTLS)协议以及为什么证书对TLS很重要。我们还回顾了TLS中证书的一些简单但非常流行的用法,例如OpenSSL执行的默认服务器证书验证、主机名验证,以及使用X.509证书接受TLS服务器连接。

第十章 在TLS中使用X.509证书技术要求C程序中对等证书的自定义验证注册验证回调实现验证回调运行程序在C程序中使用证书吊销列表注册CRL查找回调实现CRL查找回调实现从分发点下载CRL的功能实现从HTTP URL下载CRL的功能运行程序使用在线证书状态协议了解在线证书状态协议在命令行上使用OCSP在C程序中使用OCSP注册OCSP回调实现OCSP回调运行程序使用TLS客户端证书生成TLS客户端证书将客户端证书打包到PKCS#12容器文件中在服务器端以编程方式请求和验证TLS客户端证书验证TLS客户端证书实现响应生成功能运行程序以编程方式使用客户端证书建立TLS客户端连接更改从tls客户端程序继承的代码加载TLS客户端证书运行程序总结

技术要求

本章将包含可以在命令行上运行的命令以及可以构建和运行的C源代码。对于命令行命令,您将需要 openssl 命令行工具以及OpenSSL动态库。要构建C代码,您需要OpenSSL动态或静态库、库头、C编译器和链接器。

我们将在本章中实现一些示例程序来练习手头的主题。这些程序的完整源代码可以在以下地址找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/tree/main/Chapter10

C程序中对等证书的自定义验证

每个TLS连接都是在两个对等体之间建立的:客户端和服务器。每个对等体都可以请求并验证另一个对等体的证书。在现实生活中,服务器证书几乎总是在TLS握手期间进行验证。在TLS 1.3之前,TLS协议支持匿名密码,这允许服务器在没有证书的情况下运行。在实践中,这些匿名密码很少使用,默认情况下是被禁止的。因此,在实践中,始终需要TLS证书。相反,TLS客户端证书很少使用。然而,使用OpenSSL在应用程序中验证客户端证书与验证服务器证书非常相似。因此,讨论对等证书验证而不是仅限于服务器证书验证是有意义的。我们将在大多数代码示例中验证服务器证书。稍后将在【“在服务器端以编程方式请求和验证TLS客户端证书”】一节中提供有关验证客户端证书的更多信息。

您可以通过多种方式使用OpenSSL验证TLS对等证书:

在本节中,我们将编写一个小型 verify-callback 程序,以演示如何使用“小型”验证回调,该回调可以通过 SSL_CTX_set_verify()SSL_set_verify() 函数设置。

以下是我们将要使用的函数的一些相关手册页:

正如我们在 SSL_CTX_set_verify() 手册页上看到的,验证回调函数必须具有以下概要:

此函数接受以下参数:

对于对等证书链中的每个证书,将至少调用一次验证回调。将对链中的最后一个证书进行第一次调用,如果证书链可以完成,则该证书将是根CA证书。然后,OpenSSL将朝着链的开始工作,对所有中间证书调用验证回调,最后对对等证书调用。

OpenSSL库有一个称为证书深度certificate depth)的概念。证书的深度是一个数字,表示该证书与证书验证链中的对等证书相距多远。对等证书的深度始终为0。其颁发者证书的深度为1,颁发者的颁发者深度为2,以此类推。还有一个概念称为验证深度verify depth),它指定了整个证书验证链的深度或长度。可以使用 SSL_CTX_set_verify_depth()SSL_set_verify_depth() 函数限制最大验证深度。

当调用证书验证回调(callback)时, x509_store_ctx 参数将包含当前正在检查的验证链中的证书信息。可以使用 X509_STORE_CTX_get_current_cert() 函数获取当前证书,并使用 X509-STORE_CTY_get_error_depth() 函数获得其深度。

验证回调的 preverify_ok 参数告诉我们OpenSSL是否发现了当前证书的问题。 preverify_ok 参数的值可以是 01

此外,OpenSSL会将错误添加到错误队列中。如前所述,处理和清除错误队列很重要——别忘了这样做。

请注意,如果证书有多个错误,OpenSSL可以对一个证书多次调用 preverify_ok=0 的验证回调。很高兴知道,如果验证回调在 preverify_ok=1 的情况下调用,那么 X509_STORE_CTX_get_error() 函数不一定返回 X509_V_ok ,这表示成功。如果之前以 preverify_ok=0 调用了验证回调,则 X509_STORE_CTX_get_error() 将返回最后一个已知错误。只有当前证书和以前的证书没有验证错误,函数才会返回 X509_V_OK

验证回调应返回 1 表示成功,或返回 0 表示失败。如果返回 0 ,则验证立即失败,向对等端发送带有错误的TLS警报消息,TLS握手失败。如果返回 1 ,则验证继续。如果验证回调的所有调用都返回 1 ,则验证成功。

值得一提的是,如果通过 SSL_CTX_set_verify()SSL_set_verify() 函数提供 NULL 作为验证回调,则使用默认的验证回调。默认的验证回调仅返回 preverify_ok

请注意,验证回调函数不会像许多其他回调函数那样接收指向用户数据的 void* 参数。因此,如果我们想避免使用全局变量——避免使用它们是一种很好的做法——在验证回调中使用应用程序其余部分的数据并非易事。然而,这是可能的。幸运的是,我们可以通过 x509_store_ctx 参数(即验证上下文)获取用户数据。可以从 x509_store_ctx 获取 SSL* 指针,然后从SSL对象获取用户数据。下面的代码示例将演示如何做到这一点。

现在,让我们编写一个小的 verify-callback 程序,演示验证回调函数的使用。verify-callback 程序将连接到TLS服务器,启动TLS握手,在验证回调的帮助下打印有关对等证书链验证的信息,并断开连接。我们将把我们的程序建立在【第9章“建立tls连接并通过它们发送数据”】中的 tls-client 程序的基础上。我们将采用 tls-client 程序代码并对其进行进一步开发。

verify-callback 程序将采用与tls客户端程序相同的三个命令行参数:

  1. 服务器主机名
  2. 服务器端口
  3. 可选参数:包含一个或多个用于服务器证书验证的受信任CA证书的文件的名称

注册验证回调

在我们新的验证回调程序的代码中,我们将引入一个具有以下签名的函数:

此函数将是我们的验证回调函数。

tls-client 程序中,我们有以下代码行,它启用了对等证书验证过程:

我们将用以下行替换该代码行,这将在 SSL_CTX 对象中设置验证回调:

我们还将为验证回调设置一些用户数据。我们不会在 SSL_CTX 对象中设置用户数据;相反,我们将在SSL对象中设置它。通过这样做,以后更容易在 verify_callback 函数代码中获取用户数据。我们可以将任何指针设置为用户数据。在这个例子中,我们的用户数据将是 FILE* error_stream 指针,它是打印错误和诊断信息的文件句柄。以下代码行显示了如何设置用户数据:

SSL_set_app_data() 是一个宏,用于调用 SSL_set_ex_data() ,它是 *_set_ex_data() 函数家族的成员。通过使用 *_set_ex_data() 函数,可以设置一个或多个所谓的 额外数据extra data)指针,指向 SSLBIOX509 等类型的对象。您可以在 SSL_set_app_dataCRYPTO_set_ex_data 手册页上阅读有关这些函数的更多信息。我发现 *_ex_data API既不方便又令人困惑,所以我建议尽可能少地使用它。如果你需要为一个SSL对象设置多个用户数据元素,我建议你创建一个结构,将这些数据元素放入结构中,并使用 SSL_set_app_data() 函数将指向该结构的指针设置为SSL对象,就像我们在这个例子中所做的那样。

在这个例子中,我们对证书验证感兴趣,所以我们不会与服务器交换请求和响应数据。相反,我们将在握手成功或失败后关闭连接。因此,我们将从 tls-client 程序继承的代码中删除HTTP请求-响应代码。

实现验证回调

现在,让我们实现验证回调:

  1. 我们的验证回调将只打印呼叫参数和当前证书的诊断信息。我们将把该信息打印到我们设置为 SSL 对象用户数据的 FILE* 流中。

    我们将首先从函数headline实现验证回调:

  2. 我们要做的第一件事是获取用户数据,这是我们的错误和诊断流:

    为了获取用户数据,在本例中是 FILE* 流,我们必须获取 SSL 对象。SSL 对象在 x509_store_ctx 参数中作为具有特定索引的额外数据可用。因此,首先,我们必须获取索引,然后获取 SSL* 指针,然后获取 FILE* error_stream 用户数据指针。

  3. 接下来,我们将获得当前的证书验证深度和证书的当前错误(如果有的话):

  4. 接下来,我们将获取当前证书及其主题:

    在这里,Subject是一个 X509_NAME 对象。

  5. 现在,让我们通过将获得的 X509_NAME 对象打印到内存BIO来获取其文本表示:

  6. 接下来,我们将得到一个指向打印到内存BIO的文本和文本长度的指针:

    请注意,打印的文本不是以null结尾的,可能包含null字符,这就是为什么知道文本长度很重要。

  7. 现在我们有足够的数据来打印我们想要的诊断信息,让我们将其打印到 error_stream

    我们本可以使用 X509_NAME_print_ex_fp() 函数将包含证书Subject的 X509_NAME 对象直接打印到 error_stream 。我们在这里没有这样做,因为我想演示如何使用内存BIO并获取 X509_NAME 对象的文本表示。在现实世界的程序中,我们经常需要将文本放入内存并对其进行进一步处理,而不仅仅是将其打印到 FILE* 流中。

  8. 我们的 verify_callback 函数即将完成。现在,我们必须释放一些我们使用的内存结构:

    请注意,我们尚未释放包含证书主题的 X509_NAME 对象。这是因为 X509_NAME 对象归包含证书的 X509 对象所有;当 X509 对象被释放时, X509_NAME 对象将被释放。一般来说,很难预测哪些OpenSSL对象必须释放,哪些不能释放。通常,您可以在OpenSSL文档中找到该信息,但并非总是如此。如果您在文档中找不到它,则必须通过其他方式找到答案,例如检查OpenSSL源代码、进行实验或询问您当地的OpenSSL专家。

  9. 释放使用过的数据结构后,我们必须从验证回调中返回退出代码。最简单的选择就是返回 preverify_ok

至此,您已经学习了如何制作和使用证书验证回调。

我们的verify回调程序的完整源代码可以在本书的GitHub存储库中的verify callback.c文件中找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter10/verify-callback.c

运行程序

让我们运行我们的程序,看看它是如何工作的。我们将在之前使用的 www.example.org 服务器上运行它。在运行它之前,请确保您当前安装的OpenSSL能够找到受信任的CA证书。有关如何向OpenSSL提供受信任的CA证书的说明,请参阅【第9章“建立TLS连接并通过它们发送数据”】。

以下是我们如何运行该程序:

正如我们所看到的,OpenSSL已经为对等证书构建了一个证书验证链并对其进行了验证,从根CA证书开始,到服务器证书结束。在这里,我们的 verify-callback 程序打印了我们对成功案例的预期。但一个失败的案例呢?

对于失败的情况,我们将对特殊 badssl.com 域下的服务器运行 verify-callback 程序。此域包含已配置为测试TLS握手失败的服务器。您可以在以下网址找到不同测试服务器和故障原因的列表 https://badssl.com/ 。在下面的示例中,我们将使用 incomplete-chain.badssl.com 服务器。我们预计OpenSSL将无法为对等证书构建验证链,因为它无法在链中找到证书的颁发者证书。让我们检查一下我们的期望是否正确:

正如我们所看到的,我们的期望是正确的。我们在验证回调中得到了以下数据:

服务器证书(depth=0)存在问题(preverify_ok=0)。这里的问题是OpenSSL找不到颁发者证书(error_code=20error_string=unable to get local issuer certificate)。

由此,我们可以得出结论,我们的 verify-callback 程序在成功和失败的情况下都能工作,并且可以在证书验证回调的帮助下诊断TLS握手问题。

证书链中的错误并不是TLS握手失败的唯一原因。有时,证书的私钥会被泄露,因此证书会被吊销。可以通过 证书吊销列表Certificate Revocation ListsCRLs)或 在线证书状态协议Online Certificate Status ProtocolOCSP)检查证书吊销状态。在下一节中,我们将了解CRLs。

在C程序中使用证书吊销列表

CRL是一种列出已吊销证书的数据结构。证书可能因多种原因被吊销,例如私钥泄露、私钥丢失、证书错误、证书所有者停止操作、证书被另一个证书取代等。

CRL通常可以从 证书颁发机构Certificate AuthorityCA)web服务器下载。这些可下载的CRL通常以 可分辨编码规则Distinguished Encoding RulesDER)格式表示。例如,www.example.org 网站证书的CRL可以从以下网址下载http://crl3.digicert.com/DigiCertTLSRSASHA2562020CA1-4.crl

下载到文件后,可以使用 openssl 命令行工具使用 openssl crl 子命令查看CRL:

我们将得到以下输出:

正如我们所看到的,CRL包含以下信息:

CRL必须由发行者的私钥签名。CRL的颁发者与证书的颁发者是同一实体,CRL可以检查证书的吊销情况。只有证书颁发者有权通过放入CRL来吊销证书。

吊销的证书由证书序列号标识。CRL中的吊销证书条目可能包含吊销原因,但这不是强制性的。

当涉及到以编程方式根据CRL检查证书时,可以使用两种方法:

现在,让我们编写一个小的 crl-check 程序,演示如何使用CRL查找回调函数。

CRL查找回调函数必须具有以下概要:

此函数接受以下参数:

该函数返回一组 X509_CRL 对象,这些对象表示CRL。OpenSSL通常将堆栈用作列表。正如我们所看到的,如果需要,回调函数可以返回几个CRL。

我们将把 crl-check 程序建立在 verify-callback 程序的基础上,如 C程序中对等证书的自定义验证 部分所述。我们将采用 verify-callback 程序代码并对其进行进一步开发。

我们的 crl-check 程序将采用与 verify-callback 程序相同的三个命令行参数:

  1. 服务器名称
  2. 服务器端口
  3. 可选参数:包含一个或多个用于服务器证书验证的受信任CA证书的文件的名称

我们将使用一些以前没有使用过的OpenSSL函数。以下是相关的手册页及其文档:

让我们开始crl检查程序的实现。首先,我们将注册CRL查找回调。

注册CRL查找回调

以下是我们如何注册CRL查找回调:

  1. 在我们新的crl检查程序的代码中,我们将引入一个具有以下签名的函数:

    此函数将是我们的CRL查找回调函数。

  2. 为了确保我们的回调函数被调用,我们需要使用 X509_STORE_set_lookup_crls() 函数将其设置为 X509_STORE 对象,并通过将 X509_V_FLAG_CRL_CHECK 标志设置为相同的 X509_STORE 对象来启用CRL检查。 X509_STORE 对象是 SSL_CTX 对象的证书存储区。 X509_STORE 对象可以存储可信证书、不可信证书、CRL、验证参数和额外的特定于应用程序的数据。我们可以从 SSL_CTX 对象中获取一个指向 X509_STORE 对象的指针。

    要设置回调函数并启用CRL检查,我们需要将以下代码添加到 run_tls_client() 函数中:

下一步是实现CRL查找回调。

实现CRL查找回调

让我们实现CRL查找回调函数 lookup_crls()

  1. 正如我们从函数签名中看到的,回调函数获得一个 X509_NAME 对象,证书Subject作为第二个参数。如果我们有一些CRL缓存,可以帮助我们根据证书主题获取所需的CRL,那么该参数将对我们有所帮助。但在这个例子中,我们没有。相反,我们将从 X509_STORE_CTX 对象获取当前证书,该对象作为第一个回调函数参数提供,从证书中获取CRL分发点,并从其中一个分发点下载相关CRL。

    首先,让我们以与 verify_callback() 函数中相同的方式获取错误流:

  2. 现在,让我们获取需要检查吊销的当前证书:

  3. 接下来,让我们获取并打印一些关于证书的有用信息:

    请注意,这次我们使用了 X509_NAME_print_ex_fp() 函数,而不是像在 verify_callback() 函数中那样,根据BIO打印证书Subject。

  4. 下一步是从证书中获取CRL分发点。并非每个证书都定义了CRL分发点。幸运的是,我们将要测试的来自 www.example.org 的证书定义了它们,因此我们可以从它们那里下载CRL。以下是我们获取CRL分发点的方法:

    CRL_DIST_POINTSSTACK_OF(DIST_POINT)的类型定义,其中 DIST_POINT 是定义CRL分发点的结构。

  5. 接下来,我们必须遍历分发点,尝试从其中一个分发点下载CRL,并返回下载的CRL:

    lookup_crls() 函数到此结束。正如我们所看到的,如果无法下载CRL, lookup_crls() 函数将返回 NULL

    您可能已经注意到,我们调用 download_crl_from_dist_point() 函数来下载CRL。我们也必须实现该函数。

实现从分发点下载CRL的功能

让我们来看看下载CRL的功能代码:

  1. download_crl_from_dist_point() 函数具有以下签名:

  2. 分发点结构可以包含多个字符串,称为通用名称。让我们得到他们:

  3. 然后,我们必须遍历通用名称:

  4. 以下代码表示循环内的代码。首先,我们将在通用名称中查找URL:

  5. 如果我们找到一个带有URL的通用名称,我们将得到它的文本表示:

  6. 在这个例子中,我们只支持HTTP URL,所以我们将跳过非HTTP URL。这很好,因为CRL分发点通常是HTTP URL:

  7. 此时,我们发现了一个指向CRL的HTTP URL。让我们尝试下载它:

  8. 如果下载尝试失败,我们可以尝试另一个通用名称:

  9. 如果我们已成功下载CRL,我们将返回它:

for 循环中的代码到此结束。

如果 for 循环完成,这意味着我们无法从当前的CRL分发点下载CRL。在这种情况下,我们将返回 NULL

如果 download_crl_from_dist_point() 函数返回 NULL ,则调用 lookup_crls() 函数将尝试下一个分发点。

download_crl_from_dist_point() 函数调用 download_cr1_from_http_url() 函数从找到的url下载crl。让我们看看它的代码。

实现从HTTP URL下载CRL的功能

download_crl_from_http_url() 函数相对较短:

download_crl_from_http_url() 函数中,我们使用了两个以前从未使用过的函数: OSSL_http_get()d2i_X509_crl_bio()OSSL_HTTP_get() 是OpenSSL HTTP客户端的函数之一,这是OpenSSL 3.0中引入的一项新功能。d2i_X509_CRL_bio() 是一个函数,它读取从BIO编码的DER编码的CRL,并将其转换为 X509_CRL 对象。OpenSSL有许多类似的转换函数,如 i2d_X509()d2i_RSAPublicKey_fp() 等。这些函数的名称由以下组件组成:

如前所述,并非所有证书都定义了CRL分发点。即使定义了分发点,CRL下载也可能失败。在这种情况下会发生什么?如果设置了 X509_V_FLAG_CRL_CHECK 标志,但OpenSSL找不到CRL,则证书验证将失败,并出现 X509_V_ERR_UNABLE_TO_GET_CRL 错误。通常,这样的失败是不希望的,因为所颁发的证书中只有一小部分被撤销。如果吊销检查失败,则证书未被吊销的可能性要高得多。如果OpenSSL无法获取CRL,我们如何避免证书验证失败?如果设置了 X509_V_flag_CRL_CHECK 标志,则没有标志指示OpenSSL忽略 X509_V_ERR_UNABLE_to_GET_CRL 错误。但是,所有验证错误都由验证回调处理,因此可以通过从验证回调返回 1 来指示OpenSSL忽略该错误。为此,我们只需将以下行添加到 verify_callback() 函数中:

这是我们必须添加到 crl-check 程序中以使其完整的最后几行。

我们的 crl-check 程序的完整源代码可以在本书的GitHub存储库中的 crl-check.c 文件中找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter10/crl-check.c

运行程序

让我们运行我们的程序,看看它是如何工作的。我们将在 www.example.org 服务器上运行它,我们在前面的示例中也使用了该服务器:

正如我们所看到的,我们的 crl-check 程序成功地找到并下载了CRL,以便OpenSSL可以根据下载的CRL检查证书吊销。

让我们还检查一个负面情况——即证书被吊销。我们可以通过转到 revoked.badssl.com 服务器来查找已吊销的证书:

正如我们所看到的,服务器证书验证失败,因为证书已被吊销。我们可以在 error_code=23error_string=certificate revoked 错误消息中看到这一点。

我们进行的测试证实,我们的crl检查程序按预期工作,可以检测吊销的证书。

至此,我们学习了如何使用CRL检查证书吊销。但检查CRL并不是检查证书吊销的唯一方法。还有另一种吊销检查方法,称为OCSP。我们将在下一节中对其进行回顾。

使用在线证书状态协议

在本节中,我们将学习OCSP。首先,我们将了解它是什么以及它是如何工作的。然后,我们将学习如何在命令行和C程序中使用OCSP。

OCSP——Online Certificate Status Protocol,在线证书状态协议。

了解在线证书状态协议

OCSP是一种更现代的证书吊销检查方法,它使用的网络流量比CRL少得多。使用OCSP时,您不需要下载大型CRL文件。相反,可以向OCSP服务器(也称为 OCSP响应器OCSP responder)查询特定证书的状态。与特定证书的颁发者发布CRL的方式类似,OCSP服务器也由证书颁发者维护。

当查询OCSP响应者时,OCSP客户端发送ASN.1编码的OCSP请求,其中包含要检查吊销的证书列表。OCSP服务器以ASN.1编码的OCSP响应进行响应,其中包含查询的证书状态、响应的有效期、一些其他信息以及使用证书颁发者的私钥生成的响应签名。

由于OCSP响应是签名的,OCSP响应者通常通过未加密的HTTP进行操作。在验证OCSP响应者的HTTPS服务器证书时,选择HTTP而不是HTTPS进行OCSP检查也可以避免循环OCSP请求。

OCSP有时因隐私问题而受到批评。如果TLS客户端(如web浏览器)通过OCSP检查每个TLS服务器证书,那么OCSP响应者就会知道浏览器用户访问了哪些网站。每个OCSP响应者只知道那些使用由拥有OCSP响应器的同一颁发者颁发的证书的网站。但是,如果有人窃听TLS客户端的互联网线路,他们可以读取通过未加密的HTTP发送的所有OCSP请求和响应。另一方面,窃听者已经知道用户联系的服务器的IP地址。因此,窃听者只需知道服务器IP地址,就可以知道流行的网站,如谷歌或脸书。窃听者获得的额外信息包括托管在共享网络主机上的小型网站,其中相同的IP可供多个网站使用,以及一些博客平台,其中每个博客都有一个子域名。

无论隐私问题是大是小,都可以通过OCSP装订来缓解。OCSP装订OCSP stapling)意味着在TLS握手期间,TLS服务器将关于服务器证书的OCSP响应发送给TLS客户端,这样TLS客户端就不需要联系OCSP响应者。在这种情况下,OCSP响应被绑定到TLS握手。因此,这种机制被称为OCSP装订。OCSP装订是使用证书状态请求Certificate Status Request)TLS扩展实现的。TLS客户端必须在握手期间请求证书状态,才能从TLS服务器获取证书状态。另一方面,服务器必须定期向OCSP响应者轮询其证书的状态,以始终获得新的OCSP响应并将其提供给客户端。

OpenSSL支持通过命令行和编程方式进行OCSP查询。首先,让我们学习如何在命令行上检查证书的吊销。

在命令行上使用OCSP

可以使用 openssl 工具的 openssl ocsp 子命令在命令行上执行OCSP查询。让我们试试这个功能:

  1. 首先,我们需要服务器证书及其颁发者证书。我们可以使用 openssl s_client 子命令来获取它们:

    注意 -showcerts 开关。这指示 openssl 工具打印服务器发送到终端的证书。该命令将生成一个包含证书的长输出:

  2. 现在,我们可以将第一个PEM编码的证书从终端复制并粘贴到一些文件中。让我们将第一个打印的证书( CN = www.example.org)保存到名为 www.example.org.cert.pem 的文件中,并将第二个证书( CN = DigiCert TLS RSA SHA256 2020 CA1 )保存到一个名为 DigiCert_Intermediate_CA.pem 的文件中。

  3. 我们已经获得了证书,但我们如何找到OCSP响应者URL?幸运的是,证书通常包含有关 权威信息访问Authority Information Access)X509v3扩展中的OCSP响应者URL的信息。 www.example.org 网站的证书还包含该扩展名和OCSP响应者URL。我们可以使用 openssl x509 子命令的- ocsp_uri 开关提取此URL:

  4. 然后,我们可以使用以下命令通过OCSP执行吊销检查:

    正如我们所看到的,证书仍然有效,这意味着它没有被撤销。Response verify OK 消息表示OCSP响应已成功验证:它已由预期的颁发者正确签名,并且没有过时。

  5. 如果我们在命令行中添加 -text-resp_tex t开关,我们将能够看到OCSP请求和响应的文本表示。运行以下命令:

    运行上述命令将得到以下输出。输出的第一部分显示了OCSP请求:

    输出的第二部分显示了OCSP响应以及响应和证书的验证状态:

    正如我们所看到的,OCSP请求和OCSP响应都相当简单。两者都包含一个复合证书ID和一个可选的随机数。响应包含重要的附加数据,如证书状态、有效性时间戳和签名。

现在我们已经通过OCSP检查了好证书的吊销状态,让我们来看看吊销的证书:

  1. 我们已经知道,我们可以在 revokeed.badssl.com 上找到一个已撤销的证书。与前面的示例一样,让我们连接到服务器并获取没有根CA证书的服务器证书链:

    让我们将命令输出中的证书保存到终端,将服务器证书保存到 revoked.badssl.com.cert.pem 文件,并将其颁发者证书保存到 RapidSSL_Intermediate_CA1.pem 文件。

  2. 接下来,让我们获取OCSP响应程序URL:

  3. 最后,让我们检查服务器证书吊销状态:

    正如我们所看到的,这一次,证书状态被报告为已吊销revoked)。我们的观察证实, openssl ocsp 子命令可以获取良好证书和已吊销证书的吊销状态。

至此,我们已经学习了如何在命令行上使用OCSP。现在,让我们学习如何以编程方式使用OCSP。

在C程序中使用OCSP

我们可以通过以下方式以编程方式检查证书吊销状态:

现在,让我们编写一个小的 ocsp-check 程序,演示如何使用ocsp装订回调函数。

OCSP装订回调函数必须具有以下概要:

此函数接受以下参数:

回调函数应返回以下值之一:

要使用OCSP装订回调,必须使用 SSL_CTX_set_tlsext_status_type() 函数激活证书状态请求TLS扩展。使用 SSL_CTX_set_tlsext_status_cb() 函数将回调设置为 SSL_CTX 对象。可以使用 SSL_CTX_set_tlsext_status_arg() 函数设置将传递给回调函数的用户数据指针。

我们将把 ocsp-check 程序建立在 verify-callback 程序的基础上,如 【C程序中对等证书的自定义验证】部分所述。我们将采用验证回调程序代码并对其进行进一步开发。

我们的 ocsp-check 程序将采用与 verify-callback 程序相同的三个命令行参数:

  1. 服务器主机名
  2. 服务器端口
  3. 可选参数:包含一个或多个用于服务器证书验证的受信任CA证书的文件的名称

我们将使用一些以前没有使用过的OpenSSL函数。以下是相关的手册页及其文档:

让我们从使用OCSP回调注册实现 ocsp-check 程序开始。

注册OCSP回调

以下是我们如何注册OCSP回调:

  1. 首先,我们将声明以下函数。这将是我们的OCSP装订回调:

  2. run_tls_client() 函数中,我们必须激活证书状态请求TLS扩展并提供OCSP装订回调:

    在这里,我们还可以使用 SSL_CTX_set_tlsext_status_arg() 函数提供用户数据空指针,但在这个例子中我们不需要它。我们已经使用 SSL_set_app_data() 函数提供了用户数据。

这些是我们提供和激活OCSP装订回调所需的唯一代码更改。现在,让我们实现回调本身。

实现OCSP回调

以下是我们如何实现OCSP回调:

  1. 我们将通过定义函数的默认退出代码来开始回调的实现:

    在这个例子中,我想对服务器证书给予最大的怀疑。这意味着,只有当OCSP响应能够证明该证书时,我们才会考虑撤销该证书。如果OCSP响应有问题——例如,它不可解析或已过时——我们将忽略OCSP响应,并认为服务器证书仍然有效。我们这样做是因为TLS服务器从OCSP响应器获取OCSP响应时发生错误的可能性比证书被吊销的可能性大得多。

  2. 我们在OCSP装订回调中收到的第一个参数是指向SSL对象的指针,该对象存储有关当前TLS连接的信息。这意味着我们不必像 verify_callback() 函数那样以复杂的方式获取SSL对象。SSL对象对我们来说是即时可用的,我们可以很容易地从中获取用户数据:

  3. 下一步非常重要,即获取OCSP响应:

    前面的代码将获得作为DER编码数据的OCSP响应。

  4. 下一步是将该数据解码为 OCSP_RESPONSE 对象:

  5. 接下来我们可以做的是打印解码的OCSP响应。从OpenSSL 3.0开始,OpenSSL库没有可以将OCSP响应打印到 FILE 流的函数。只有 OCSP_RESPONSE_print() 函数,它打印对BIO的响应。因此,我们必须使用 FILE 指针BIO:

    请注意,我们在创建BIO时使用了 BIO_NOCLOSE 标志。这非常重要。由于该标志,当我们释放BIO时,稍后对 BIO_free() 的调用将不会尝试关闭底层 FILE 流。

  6. 下一步是检查整个OCSP响应的总体状态:

    OCSP响应状态告诉我们OCSP响应程序是否可以成功处理OCSP请求并形成OCSP响应。

  7. 下一步是验证OCSP响应签名:

    注意 SSL_get0_verified_chain() 函数调用。只有在构建了服务器证书的验证链后,才能调用此函数;否则,函数可能会返回部分或不正确的链。幸运的是,OCSP回调是在服务器证书验证后调用的,因此我们可以安全地调用 SSL_get0_verified_chain()

  8. 我们接下来要做的是在OCSP响应中查找TLS服务器证书的状态。OCSP响应可以包含有关多个证书的吊销信息,因此我们必须在响应中搜索所需证书的状态:

    我们刚才进行的 OCSP_resp_find_status() 调用为我们提供了有关证书吊销状态、生成OCSP响应的时间以及必须获取同一证书的下一个OCSP响应时间的信息。

  9. 让我们检查一下响应是否仍然有效:

  10. 现在我们已经验证了OCSP响应,让我们检查证书吊销状态,如果证书已吊销,则更改 exit_code 变量:

  11. 我们将通过释放不再需要的对象并返回退出代码来完成OCSP装订回调函数:

我们的 ocsp-check 程序的完整源代码可以在本书的GitHub存储库中的 ocsp-check.c 文件中找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter10/ocsp-check.c

现在我们已经完成了 ocsp-check 程序的实现,让我们运行它。

运行程序

我们可以这样运行 ocsp-check 程序:

正如我们所看到的,我们的 ocsp-check 程序可以通过OCSP成功检查服务器证书吊销状态。

在前面的部分中,我们学习了如何使用自定义验证回调、CRL和OCSP来广泛验证TLS服务器证书。这些是在TLS协议中使用X.509证书的重要方面。在下一节中,我们将了解在TLS中使用证书的另一个重要方面:TLS客户端证书。

使用TLS客户端证书

在本节中,我们将学习如何使用TLS客户端证书:生成它们,将它们打包到公钥加密标准#12Public Key Cryptography Standards #12PKCS#12)容器中,从TLS服务器请求它们,并将它们提供给TLS客户端。

生成TLS客户端证书

除了TLS服务器,TLS客户端还可以在TLS连接上提供证书。但是,根据TLS协议,默认情况下,TLS客户端不会发送其证书,即使客户端有证书。TLS客户端仅在TLS服务器请求时发送其证书。

TLS客户端证书的另一个特点是,客户端证书通常以PKCS#12格式与其证书私钥(或更确切地说是密钥对)一起存储。PKCS#12是一种文件格式,可以存储多个加密对象,如X.509证书和密钥对。这些存储的对象可以使用对称密码进行加密,并使用基于哈希的消息身份验证码Hash-based Message Authentication CodeHMAC)进行身份验证。具有TLS客户端证书的典型PKCS#12文件受密码保护,包含客户端证书、其密钥对和形成客户端证书验证链的CA证书。即使一个典型的PKCS#12文件包含多个客户端证书,它通常也被称为客户端证书文件。

PKCS#12文件的文件扩展名为 .p12.pfx 。如果你想在流行的网络浏览器(如Mozilla Firefox或Google Chrome)中使用TLS客户端证书,你需要在PKCS#12容器文件中向浏览器提供客户端证书,最好带有 .p12.pfx 扩展名。在接下来的代码示例中,我们将从PKCS#12文件中读取客户端证书。

让我们学习如何生成客户端证书并将其打包到PKCS#12容器中。

我们将生成一个客户端证书,其方式与【第9章“建立TLS连接并通过它们发送数据”】中生成服务器证书的方式非常相似。我们还将重用上一章中的根CA证书及其密钥对:

运行指定的命令后,我们将把客户端证书及其密钥对保存到PEM文件中。在下一节中,我们将学习如何将它们打包到PKCS#12容器中。

将客户端证书打包到PKCS#12容器文件中

要将生成的客户端证书、其密钥对和CA证书打包到PKCS#12容器中,我们可以使用 openssl 命令行工具的 openssl pkcs12 子命令。此子命令的文档可在 openssl pkcs12 手册页上找到:

以下是我们如何将密钥对和证书打包到PKCS#12容器中:

请注意,我们为PKCS#12容器提供了一个密码,即 SuperPa$$w0rd 。在命令行上提供密码不是很安全,因为命令可能会保存在命令的历史记录中,并可能在进程列表中可见。在这个例子中,我们这样做只是为了简单。请注意, openssl 工具支持更安全的密码提供方式,例如从文件或stdin读取密码。您可以在以下手册页上阅读有关向 openssl 工具提供密码的更多信息:

至此,我们成功创建了PKCS#12容器文件。现在,我们可以检查里面有什么:

正如我们所看到的,生成的PKCS#12容器文件包含客户端证书、CA证书和客户端证书密钥对。容器的内容使用AES-256-CBC加密,并使用HMAC-SHA256进行身份验证。

现在我们已经准备好了客户端证书,让我们实现一个小型TLS服务器程序来请求和验证它。

在服务器端以编程方式请求和验证TLS客户端证书

在本节中,我们将编写一个小型TLS服务器程序,用于请求和验证TLS客户端证书。我们将基于【第9章“建立tls连接并通过它们发送数据”】中的 tls-server 程序。我们将采用 tls-server 程序的代码并对其进行进一步开发。我们的新程序将被称为 tls-server2

我们的 tls-server2 程序将采用以下命令行参数:

  1. 服务器端口
  2. 包含TLS服务器密钥对的文件的名称
  3. 包含TLS服务器证书链的文件的名称
  4. 包含可验证客户端证书的受信任CA证书的文件的名称

为了能够验证客户端证书,我们必须加载相应的可信CA证书并启用对等证书验证。

验证TLS客户端证书

以下是我们如何验证客户端证书:

  1. 首先,我们必须加载所需的可信CA证书。我们必须捕获第四个命令行参数并将其发送到 run_tls_server() 函数。让我们在 main() 函数中添加以下行:

  2. 我们还必须将 trusted_cert_fname 参数传递给 run_tls_server() 函数,并在加载和检查私钥的代码之后在 run_tls_server() 功能中添加以下代码:

  3. 我们还必须更改以下代码以请求客户端证书并启用其验证:

    SSL_VERIFY_PEER 标志指示OpenSSL请求客户端证书。请注意,如果客户端没有提供证书,则证书的缺失不会被视为服务器端的错误。通过将 SSL_VERIFY_FAIL_IF_NO_PEER_CERT 标志与 SSL_VERIFY_PEER 标志一起设置,可以更改此行为,如下所示:

    如果 SSL_VERIFY_PEERSSL_VERIIF_FAIL_If_NO_PEER_CERT 标志都已设置,并且客户端没有提供证书,则TLS握手将失败。

此代码足以请求客户端证书并对其进行验证。但是,打印客户端证书主题也很好,以确保TLS客户端已发送其证书,TLS服务器已对其进行了验证。为此,我们将引入一个 construct_response() 函数,该函数将构造一个包含客户端证书信息的服务器响应。在 tls-server2 中,我们将使用该函数创建的响应,而不是我们在原始 tls-server 程序中使用的静态响应。

实现响应生成功能

该函数将具有以下签名:

获取客户端证书需要ssl参数。该函数将返回一个包含服务器响应的内存BIO。

让我们实现 construct_response() 函数:

  1. 我们将首先创建一个内存BIO并在那里打印服务器响应头:

  2. 下一步是获取客户端证书:

    请注意,在我们的示例中,我们没有使用 SSL_VERIFY_FAIL_IF_NO_PEER_CERT 标志,这就是为什么TLS客户端可以选择不提供证书的原因。在这种情况下, peer_cert 指针将为 NULL 。我们稍后必须考虑到这一点。

  3. 下一步是将有关客户端证书或其缺失的信息打印到内存BIO:

  4. 我们通过返回内存BIO来完成 construct_response() 函数:

    非常好——我们已经实现了 construct_response() 函数!我们将从 handle_accepted_connection() 函数调用该函数,而不是从构造静态响应并计算其长度的代码调用。

  5. 以下代码来自原始 tls-server 程序:

    我们必须用以下代码替换它:

  6. 别忘了释放我们在新代码中引入的内存BIO!为了释放它,我们必须在 handle_accepted_connect() 函数的末尾添加以下代码:

这些都是我们需要对TLS服务器代码进行的更改。

tls-server2 程序的完整源代码可以在本书的GitHub存储库 tls-server2.c 文件中找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter10/tls-server2.c

运行程序

让我们运行 tls-server2 程序,看看它是如何工作的:

  1. 我们还没有编写提供客户端证书的TLS客户端程序。但是,我们可以使用 openssl s_client 子命令或 curl 实用程序进行测试。我们需要两个终端窗口——一个用于TLS服务器,另一个用于SSL客户端。让我们在第一个终端窗口中启动 tls-server2 程序:

  2. 让我们在第二个终端窗口中使用客户端证书作为TLS客户端启动 openssl 工具:

  3. 当启动 openssl 时,它将连接到 tls-server2 并输出大量信息。之后只需按 Enter 键。 openssl 工具将输出以下内容:

  4. 让我们检查运行 tls-server2 的第一个终端窗口。我们将看到以下输出:

    正如我们所看到的,我们的 tls-server2 程序已经请求、接收并成功验证了客户端证书,并报告了客户端证书的主题。

  5. 我们可以使用 curl 实用程序执行相同的测试。让我们在第二个终端窗口中运行它:

    请注意,我们提供了PKCS#12格式的 curl 客户端证书。

    正如我们所看到的,如果tls客户端提供了客户端证书,我们的 tls-server2 程序可以提供关于客户端证书的良好报告。但是如果TLS客户端不提供证书,会发生什么?

  6. 让我们使用 openssl 工具检查一下。让我们在没有证书的情况下启动 openssl s_client

  7. 连接并按 Enter 键后,我们将得到以下报告:

  8. 我们将使用 curl 得到类似的报告:

由此,我们可以得出结论,我们的 tls-server2 程序按预期工作。它可以请求、验证和报告有关TLS客户端证书的信息,以及优雅地处理客户端证书的缺失。

tls-server2 程序将运行并接受连接,直到中止。您可以在运行它的终端窗口中按 Ctrl+C 中止它。

至此,我们已经学习了如何从服务器端请求客户端证书并对其进行验证。在下一节中,我们将学习如何将TLS客户端证书加载到客户端并将其用于TLS连接。

以编程方式使用客户端证书建立TLS客户端连接

在本节中,我们将编写一个使用TLS客户端证书的小型TLS客户端程序。我们将基于【第9章“建立TLS连接并通过它们发送数据”】中的 tls-client 程序。在这里,我们将采用 tls-client 程序的代码并对其进行进一步开发。我们的新程序将被称为 tls-client2

我们的 tls-client2 程序将采用以下命令行参数:

  1. 服务器主机名
  2. 服务器端口
  3. 包含用于服务器证书验证的受信任CA证书的文件的名称
  4. 包含客户端证书及其密钥对的PKCS#12文件的名称
  5. PKCS#12文件的密码以及客户端证书

与原始 tls-client 程序相比, tls-client2 多了两个参数。所有五个参数现在都是强制性的。

我们将从PKCS#12容器文件加载客户端证书。也可以使用 SSL_CTX_use_certificate_chain_file()SSL_CTX_use_PrivateKey_file() 函数从PEM文件加载客户端证书及其密钥对,就像我们在 tls-servertls-server2 程序中所做的那样。但是,我想演示从PKCS#12文件加载,因为客户端证书通常分布在PKCS#12文档中。

我们将使用一些以前没有使用过的OpenSSL函数。以下是相关的手册页及其文档:

现在,让我们开始实现 tls-client2 程序,并对从 tls-client 程序继承的代码进行更改。

更改从tls客户端程序继承的代码

以下是我们需要改变的:

  1. 由于我们还有两个命令行参数,我们应该在 main() 函数的命名变量中捕获它们:

  2. 我们还必须将这两个新参数传递给 run_tls_client() 函数。因此, run_tls_client() 函数看起来像这样:

    main() 函数中的 run_tls_client() 调用如下:

  3. run_tls_client() 函数中,加载受信任的证书后,我们必须向 load_client_certificate() 函数添加一个调用,将客户端证书加载到 SSL_CTX 对象中:

现在,我们必须实现 load_client_certificate 函数。

加载TLS客户端证书

load_client_certificate 函数将具有以下签名:

函数成功时返回 0 ,失败时返回非零退出代码。

让我们继续函数的实现:

  1. 首先,我们必须定义函数退出代码,如果发生故障,可以更改该代码:

  2. 然后,我们必须创建一个文件BIO并打开客户端证书文件进行读取:

  3. 下一步是将客户端证书文件加载到 PKCS12 对象中:

    请注意,我们可以使用 fopen() 打开客户端证书文件,并使用 d2i_PKCS12_fp() 加载它。然而,我想演示另一种BIO类型的用法—— file BIO。

  4. d2i_PKCS12_bio() 函数已加载PKCS#12容器,但未对其进行解密或解析。下一步是验证密码,以便我们可以解密PKCS#12集装箱。此步骤是可选的,但对于故障排除可能很有用:

  5. 在我们验证了PKCS#12容器密码后,我们必须尝试解密它,然后解析并获取验证链的客户端证书、密钥对和CA证书:

  6. 如果PKCS#12容器已成功解析,我们可以将客户端证书、其密钥对和CA证书设置为 SSL_CTX 对象:

    请注意, SSL_CTX_use_cert_and_key() 函数将增加 certpkeycert_chain 对象的引用计数。因此,我们必须在函数结束时释放它们。由于 SSL_CTX 对象仍将保存它们的引用,因此在函数结束时释放时,上述对象将不会被释放。相反,当 SSL_CTX 对象本身被释放时,它们将被释放。

  7. 当涉及到潜在的故障排除时,最好检查加载的客户端证书密钥对是否与客户端证书匹配:

    此时,我们已经成功加载了客户端证书、其密钥对和CA证书,并将其设置为 SSL_CTX 对象。

  8. 现在,让我们通过释放已使用的对象并返回退出代码来完成 load_client_certificate() 函数代码:

    请注意,BIO_freeclient_cert_bio)调用将在释放文件BIO之前关闭底层文件。

至此,我们已经完成了 load_client_certificate() 函数和整个 tls-client2 程序的实现。我们 tls-client2 程序的完整源代码可以在本书的GitHub存储库 tls-client2.c 文件中找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter10/tls-client2.c

运行程序

现在,让我们试着运行 tls-client2 程序并检查它是如何工作的:

  1. 与前面的示例一样,我们需要两个终端窗口。在第一个窗口中,我们将运行 tls-server2 程序:

  2. 在第二个终端窗口中,我们将运行 tls-client2 程序:

    正如我们所看到的, tls-client2 可以加载客户端证书并在tls连接中使用它。 tls-server2 程序报告它已在服务器端获得客户端证书。由此,我们可以得出结论, tls-client2 程序按预期工作。

至此,我们已经学习了如何使用TLS客户端证书。现在,让我们总结一下这一章。

总结

本章与前几章不同。在前面的章节中,我们每章都讨论了一个大主题。然而,在本章中,我们讨论了几个小主题,并编写了更多的示例程序。

在本章中,我们学习了如何在TLS握手期间验证对等证书。我们还学习了CRL和OCSP方法。然后,我们学习了TLS客户端证书和PKCS#12容器。之后,我们学习了如何在服务器端请求和验证TLS客户端证书,以及如何在客户端加载和使用客户端证书。我们通过将支持TLS客户端证书的TLS服务器和TLS客户端程序相互连接来完成本章。

您从本章中获得的知识将帮助您在启用TLS的程序中对TLS连接执行高级客户端和服务器证书处理。

在下一章中,我们将了解TLS的更高级和特殊用途。