在【第8章X.509证书和PKI】中,我们学习了X.509证书(X.509 certificates),而在【第9章建立TLS连接并通过它们发送数据】中,我们了解了传输层安全(Transport Layer Security,TLS)协议以及为什么证书对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
每个TLS连接都是在两个对等体之间建立的:客户端和服务器。每个对等体都可以请求并验证另一个对等体的证书。在现实生活中,服务器证书几乎总是在TLS握手期间进行验证。在TLS 1.3之前,TLS协议支持匿名密码,这允许服务器在没有证书的情况下运行。在实践中,这些匿名密码很少使用,默认情况下是被禁止的。因此,在实践中,始终需要TLS证书。相反,TLS客户端证书很少使用。然而,使用OpenSSL在应用程序中验证客户端证书与验证服务器证书非常相似。因此,讨论对等证书验证而不是仅限于服务器证书验证是有意义的。我们将在大多数代码示例中验证服务器证书。稍后将在【“在服务器端以编程方式请求和验证TLS客户端证书”】一节中提供有关验证客户端证书的更多信息。
您可以通过多种方式使用OpenSSL验证TLS对等证书:
SSL_CTX_set_verify
( ctx
,SSL_VERIFY_PEER
, NULL
)或 SSL_set_verify
( ctx
, SSL_VEIFY_PEER
,NULL
)来使用OpenSSL内置的证书验证代码,就像我们在【第9章“建立tls连接并通过它们发送数据”】中的 tls-client
程序中所做的那样。OpenSSL内置验证可以通过启用可选检查(如主机名验证)、使用 SSL_set1_host()
函数或使用 SSL_CTX_set_purpose()
或 SSL_set_purpose()
函数进行目的验证来进行调整。SSL_CTX_set_verify
( ctx
, SSL_VERIFY_PEER
,verify_callback
)或 SSL_set_verify
(ctx
, SSL_VERIFY_PEER
, verify_callback
),将OpenSSL内置验证代码与您自己的验证回调函数相结合。请注意,最后一个函数参数不是NULL,而是指向验证回调函数的指针。SSL_CTX_set_cert_verify_callback
( ctx
,cert_veriff_callback
,arg
),使用“大”证书验证回调函数而不是OpenSSL内置的验证代码。SSL_get_peer_certificate()
、SSL_get_peer_cert_chain()
和 SSL_get0_verified_chain()
等函数来实现。可以通过调用带有 SSL_verify_NONE
标志的 SSL_CTX_set_verify()
或 SSL_set_verife()
,或者通过始终返回验证回调的成功返回码来强制完成握手。在本节中,我们将编写一个小型 verify-callback
程序,以演示如何使用“小型”验证回调,该回调可以通过 SSL_CTX_set_verify()
或 SSL_set_verify()
函数设置。
以下是我们将要使用的函数的一些相关手册页:
xxxxxxxxxx
$ man SSL_CTX_set_verify
$ man SSL_get_app_data
$ man X509_STORE_CTX_get_ex_data
$ man X509_STORE_CTX_get_error
$ man X509_STORE_CTX_get_current_cert
$ man X509_get_subject_name
$ man X509_NAME_print_ex
$ man BIO_s_mem
正如我们在 SSL_CTX_set_verify()
手册页上看到的,验证回调函数必须具有以下概要:
xxxxxxxxxx
int SSL_verify_cb(
int preverify_ok, X509_STORE_CTX* x509_store_ctx);
此函数接受以下参数:
preverify_ok
:错误指示器x509_store_ctx
:X.509证书验证上下文对于对等证书链中的每个证书,将至少调用一次验证回调。将对链中的最后一个证书进行第一次调用,如果证书链可以完成,则该证书将是根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
参数的值可以是 0
或 1
。
preverify_ok
为 1
,则OpenSSL未发现当前证书有任何问题,并将前进到下一个证书。preverify_ok
的值为0,则表示OpenSSL检测到当前证书存在问题。在这种情况下,可以使用 X509_STORE_CTX_get_error()
函数获取错误代码,并使用 X509_verify_cert_error_string()
函数获得错误字符串表示。此外,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客户端程序相同的三个命令行参数:
在我们新的验证回调程序的代码中,我们将引入一个具有以下签名的函数:
xxxxxxxxxx
int verify_callback(
int preverify_ok, X509_STORE_CTX* x509_store_ctx);
此函数将是我们的验证回调函数。
在 tls-client
程序中,我们有以下代码行,它启用了对等证书验证过程:
xxxxxxxxxx
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
我们将用以下行替换该代码行,这将在 SSL_CTX
对象中设置验证回调:
xxxxxxxxxx
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_callback);
我们还将为验证回调设置一些用户数据。我们不会在 SSL_CTX
对象中设置用户数据;相反,我们将在SSL对象中设置它。通过这样做,以后更容易在 verify_callback
函数代码中获取用户数据。我们可以将任何指针设置为用户数据。在这个例子中,我们的用户数据将是 FILE* error_stream
指针,它是打印错误和诊断信息的文件句柄。以下代码行显示了如何设置用户数据:
xxxxxxxxxx
SSL_set_app_data(ssl, error_stream);
SSL_set_app_data()
是一个宏,用于调用 SSL_set_ex_data()
,它是 *_set_ex_data()
函数家族的成员。通过使用 *_set_ex_data()
函数,可以设置一个或多个所谓的 额外数据(extra data)指针,指向 SSL
、 BIO
和 X509
等类型的对象。您可以在 SSL_set_app_data
和 CRYPTO_set_ex_data
手册页上阅读有关这些函数的更多信息。我发现 *_ex_data
API既不方便又令人困惑,所以我建议尽可能少地使用它。如果你需要为一个SSL对象设置多个用户数据元素,我建议你创建一个结构,将这些数据元素放入结构中,并使用 SSL_set_app_data()
函数将指向该结构的指针设置为SSL对象,就像我们在这个例子中所做的那样。
在这个例子中,我们对证书验证感兴趣,所以我们不会与服务器交换请求和响应数据。相反,我们将在握手成功或失败后关闭连接。因此,我们将从 tls-client
程序继承的代码中删除HTTP请求-响应代码。
现在,让我们实现验证回调:
我们的验证回调将只打印呼叫参数和当前证书的诊断信息。我们将把该信息打印到我们设置为 SSL
对象用户数据的 FILE*
流中。
我们将首先从函数headline实现验证回调:
xxxxxxxxxx
int verify_callback(
int preverify_ok, X509_STORE_CTX* x509_store_ctx) {
我们要做的第一件事是获取用户数据,这是我们的错误和诊断流:
xxxxxxxxxx
int ssl_ex_data_idx = SSL_get_ex_data_X509_STORE_CTX_idx();
SSL* ssl = X509_STORE_CTX_get_ex_data(
x509_store_ctx, ssl_ex_data_idx);
FILE* error_stream = SSL_get_app_data(ssl);
为了获取用户数据,在本例中是 FILE*
流,我们必须获取 SSL
对象。SSL
对象在 x509_store_ctx
参数中作为具有特定索引的额外数据可用。因此,首先,我们必须获取索引,然后获取 SSL*
指针,然后获取 FILE* error_stream
用户数据指针。
接下来,我们将获得当前的证书验证深度和证书的当前错误(如果有的话):
xxxxxxxxxx
int depth = X509_STORE_CTX_get_error_depth(x509_store_ctx);
int error_code = preverify_ok ?
X509_V_OK :
X509_STORE_CTX_get_error(x509_store_ctx);
const char* error_string =
X509_verify_cert_error_string(error_code);
接下来,我们将获取当前证书及其主题:
xxxxxxxxxx
X509* current_cert =
X509_STORE_CTX_get_current_cert(x509_store_ctx);
X509_NAME* current_cert_subject =
X509_get_subject_name(current_cert);
在这里,Subject是一个 X509_NAME
对象。
现在,让我们通过将获得的 X509_NAME
对象打印到内存BIO来获取其文本表示:
xxxxxxxxxx
BIO* mem_bio = BIO_new(BIO_s_mem());
X509_NAME_print_ex(
mem_bio,
current_cert_subject,
0,
XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
接下来,我们将得到一个指向打印到内存BIO的文本和文本长度的指针:
xxxxxxxxxx
char* bio_data = NULL;
long bio_data_len = BIO_get_mem_data(mem_bio, &bio_data);
请注意,打印的文本不是以null结尾的,可能包含null字符,这就是为什么知道文本长度很重要。
现在我们有足够的数据来打印我们想要的诊断信息,让我们将其打印到 error_stream
:
xxxxxxxxxx
fprintf(
error_stream,
"verify_callback() called with depth=%i, "
"preverify_ok=%i, error_code=%i, error_string=%s\n",
depth,
preverify_ok,
error_code,
error_string);
fprintf(error_stream, "Certificate Subject: ");
fwrite(bio_data, 1, bio_data_len, error_stream);
fprintf(error_stream, "\n");
我们本可以使用 X509_NAME_print_ex_fp()
函数将包含证书Subject的 X509_NAME
对象直接打印到 error_stream
。我们在这里没有这样做,因为我想演示如何使用内存BIO并获取 X509_NAME
对象的文本表示。在现实世界的程序中,我们经常需要将文本放入内存并对其进行进一步处理,而不仅仅是将其打印到 FILE*
流中。
我们的 verify_callback
函数即将完成。现在,我们必须释放一些我们使用的内存结构:
xxxxxxxxxx
BIO_free(mem_bio);
X509_free(current_cert);
请注意,我们尚未释放包含证书主题的 X509_NAME
对象。这是因为 X509_NAME
对象归包含证书的 X509
对象所有;当 X509
对象被释放时, X509_NAME
对象将被释放。一般来说,很难预测哪些OpenSSL对象必须释放,哪些不能释放。通常,您可以在OpenSSL文档中找到该信息,但并非总是如此。如果您在文档中找不到它,则必须通过其他方式找到答案,例如检查OpenSSL源代码、进行实验或询问您当地的OpenSSL专家。
释放使用过的数据结构后,我们必须从验证回调中返回退出代码。最简单的选择就是返回 preverify_ok
:
xxxxxxxxxx
return preverify_ok;
}
至此,您已经学习了如何制作和使用证书验证回调。
我们的verify回调程序的完整源代码可以在本书的GitHub存储库中的verify callback.c文件中找到:
让我们运行我们的程序,看看它是如何工作的。我们将在之前使用的 www.example.org
服务器上运行它。在运行它之前,请确保您当前安装的OpenSSL能够找到受信任的CA证书。有关如何向OpenSSL提供受信任的CA证书的说明,请参阅【第9章“建立TLS连接并通过它们发送数据”】。
以下是我们如何运行该程序:
xxxxxxxxxx
$ ./verify-callback www.example.org 443
verify_callback() called with depth=2, preverify_ok=1, error_code=0, error_string=ok
Certificate Subject: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify_callback() called with depth=1, preverify_ok=1, error_code=0, error_string=ok
Certificate Subject: C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
verify_callback() called with depth=0, preverify_ok=1, error_code=0, error_string=ok
Certificate Subject: C = US, ST = California, L = Los Angeles, O = Internet Corporation for Assigned Names and Numbers, CN = www.example.org
TLS communication succeeded
正如我们所看到的,OpenSSL已经为对等证书构建了一个证书验证链并对其进行了验证,从根CA证书开始,到服务器证书结束。在这里,我们的 verify-callback
程序打印了我们对成功案例的预期。但一个失败的案例呢?
对于失败的情况,我们将对特殊 badssl.com
域下的服务器运行 verify-callback
程序。此域包含已配置为测试TLS握手失败的服务器。您可以在以下网址找到不同测试服务器和故障原因的列表 https://badssl.com/
。在下面的示例中,我们将使用 incomplete-chain.badssl.com
服务器。我们预计OpenSSL将无法为对等证书构建验证链,因为它无法在链中找到证书的颁发者证书。让我们检查一下我们的期望是否正确:
xxxxxxxxxx
$ ./verify-callback incomplete-chain.badssl.com 443
verify_callback() called with depth=0, preverify_ok=0, error_code=20, error_string=unable to get local issuer certificate
Certificate Subject: C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com
Could not connect to server incomplete-chain.badssl.com on port 443
Errors from the OpenSSL error queue:
C0C158FD5D7F0000:error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:ssl/statem/statem_clnt.c:1882:
C0C158FD5D7F0000:error:0A000197:SSL routines:SSL_shutdown:shutdown while in init:ssl/ssl_lib.c:2242:
TLS communication failed
正如我们所看到的,我们的期望是正确的。我们在验证回调中得到了以下数据:
xxxxxxxxxx
depth=0, preverify_ok=0, error_code=20,
error_string=unable to get local issuer certificate
服务器证书(depth=0
)存在问题(preverify_ok=0
)。这里的问题是OpenSSL找不到颁发者证书(error_code=20
,error_string=unable to get local issuer certificate
)。
由此,我们可以得出结论,我们的 verify-callback
程序在成功和失败的情况下都能工作,并且可以在证书验证回调的帮助下诊断TLS握手问题。
证书链中的错误并不是TLS握手失败的唯一原因。有时,证书的私钥会被泄露,因此证书会被吊销。可以通过 证书吊销列表(Certificate Revocation Lists,CRLs)或 在线证书状态协议(Online Certificate Status Protocol,OCSP)检查证书吊销状态。在下一节中,我们将了解CRLs。
CRL是一种列出已吊销证书的数据结构。证书可能因多种原因被吊销,例如私钥泄露、私钥丢失、证书错误、证书所有者停止操作、证书被另一个证书取代等。
CRL通常可以从 证书颁发机构(Certificate Authority,CA)web服务器下载。这些可下载的CRL通常以 可分辨编码规则(Distinguished Encoding Rules,DER)格式表示。例如,www.example.org
网站证书的CRL可以从以下网址下载http://crl3.digicert.com/DigiCertTLSRSASHA2562020CA1-4.crl。
下载到文件后,可以使用 openssl
命令行工具使用 openssl crl
子命令查看CRL:
xxxxxxxxxx
$ openssl crl \
-in DigiCertTLSRSASHA2562020CA1-4.crl \
-inform DER \
-noout \
-text \
| less
我们将得到以下输出:
xxxxxxxxxx
Certificate Revocation List (CRL):
Version 2 (0x1)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = DigiCert Inc,
CN = DigiCert TLS RSA SHA256 2020 CA1
Last Update: May 22 07:00:10 2022 GMT
Next Update: May 29 07:00:10 2022 GMT
CRL extensions:
X509v3 Authority Key Identifier:
… hex bytes …
X509v3 CRL Number:
238
X509v3 Issuing Distribution Point: critical
Full Name:
URI:http://crl3.digicert.com/DigiCertTLSRSASHA2562020CA1-4.crl
Revoked Certificates:
Serial Number: 048A6B01889FA7BEF34AC92DAAC36079
Revocation Date: Jun 22 00:31:11 2021 GMT
CRL entry extensions:
X509v3 CRL Reason Code:
Key Compromise
Serial Number: 098AB8A98137F3432A18DE8C1B7F6D2F
Revocation Date: Jul 3 05:58:20 2021 GMT
CRL entry extensions:
X509v3 CRL Reason Code:
Key Compromise
… more certificates …
Signature Algorithm: sha256WithRSAEncryption
… hex bytes …
正如我们所看到的,CRL包含以下信息:
CRL必须由发行者的私钥签名。CRL的颁发者与证书的颁发者是同一实体,CRL可以检查证书的吊销情况。只有证书颁发者有权通过放入CRL来吊销证书。
吊销的证书由证书序列号标识。CRL中的吊销证书条目可能包含吊销原因,但这不是强制性的。
当涉及到以编程方式根据CRL检查证书时,可以使用两种方法:
X509_STORE_add_CRL()
函数向 X509_STORE
对象提供CRL。如果您已经拥有目标证书的CRL,这是一种很好的方法。缓存CRL是一个好主意,因为它们可能非常大——对于大型CA来说,大小可达数十兆字节。X509_STORE_set_lookup_crls()
函数提供的回调来查找。如果事先不知道检查的证书,这是一种很好的方法,这是TLS连接中服务器证书的常见情况。我们将在以下代码示例中使用此方法。现在,让我们编写一个小的 crl-check
程序,演示如何使用CRL查找回调函数。
CRL查找回调函数必须具有以下概要:
xxxxxxxxxx
X509_CRL)* X509_STORE_CTX_lookup_crls_fn(
const X509_STORE_CTX* x509_store_ctx,
const X509_NAME* x509_name);
此函数接受以下参数:
x509_store_ctx
:X.509证书验证上下文x509_name
:证书的主体,应使用CRL检查其吊销状态该函数返回一组 X509_CRL
对象,这些对象表示CRL。OpenSSL通常将堆栈用作列表。正如我们所看到的,如果需要,回调函数可以返回几个CRL。
我们将把 crl-check
程序建立在 verify-callback
程序的基础上,如 C程序中对等证书的自定义验证 部分所述。我们将采用 verify-callback
程序代码并对其进行进一步开发。
我们的 crl-check
程序将采用与 verify-callback
程序相同的三个命令行参数:
我们将使用一些以前没有使用过的OpenSSL函数。以下是相关的手册页及其文档:
xxxxxxxxxx
$ man SSL_CTX_get_cert_store
$ man X509_STORE_set_lookup_crls
$ man X509_STORE_set_flags
$ man X509_NAME_print_ex_fp
$ man X509_get_ext_d2i
$ man ASN1_STRING_get0_data
$ man OSSL_HTTP_get
$ man d2i_X509_CRL_bio
让我们开始crl检查程序的实现。首先,我们将注册CRL查找回调。
以下是我们如何注册CRL查找回调:
在我们新的crl检查程序的代码中,我们将引入一个具有以下签名的函数:
xxxxxxxxxx
STACK_OF(X509_CRL)* lookup_crls(
const X509_STORE_CTX* x509_store_ctx,
const X509_NAME* x509_name);
此函数将是我们的CRL查找回调函数。
为了确保我们的回调函数被调用,我们需要使用 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()
函数中:
xxxxxxxxxx
X509_STORE* x509_store = SSL_CTX_get_cert_store(ctx);
X509_STORE_set_lookup_crls(x509_store, lookup_crls);
X509_STORE_set_flags(x509_store, X509_V_FLAG_CRL_CHECK);
下一步是实现CRL查找回调。
让我们实现CRL查找回调函数 lookup_crls()
:
正如我们从函数签名中看到的,回调函数获得一个 X509_NAME
对象,证书Subject作为第二个参数。如果我们有一些CRL缓存,可以帮助我们根据证书主题获取所需的CRL,那么该参数将对我们有所帮助。但在这个例子中,我们没有。相反,我们将从 X509_STORE_CTX
对象获取当前证书,该对象作为第一个回调函数参数提供,从证书中获取CRL分发点,并从其中一个分发点下载相关CRL。
首先,让我们以与 verify_callback()
函数中相同的方式获取错误流:
xxxxxxxxxx
int ssl_ex_data_idx = SSL_get_ex_data_X509_STORE_CTX_idx();
SSL* ssl = X509_STORE_CTX_get_ex_data(
x509_store_ctx, ssl_ex_data_idx);
FILE* error_stream = SSL_get_app_data(ssl);
现在,让我们获取需要检查吊销的当前证书:
xxxxxxxxxx
X509* current_cert =
X509_STORE_CTX_get_current_cert(x509_store_ctx);
接下来,让我们获取并打印一些关于证书的有用信息:
xxxxxxxxxx
int depth = X509_STORE_CTX_get_error_depth(x509_store_ctx);
X509_NAME* current_cert_subject =
X509_get_subject_name(current_cert);
fprintf(
error_stream,
"lookup_crls() called with depth=%i\n",
depth);
fprintf(error_stream, "Looking up CRL for certificate: ");
X509_NAME_print_ex_fp(
error_stream,
current_cert_subject,
0,
XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
fprintf(error_stream, "\n");
请注意,这次我们使用了 X509_NAME_print_ex_fp()
函数,而不是像在 verify_callback()
函数中那样,根据BIO打印证书Subject。
下一步是从证书中获取CRL分发点。并非每个证书都定义了CRL分发点。幸运的是,我们将要测试的来自 www.example.org
的证书定义了它们,因此我们可以从它们那里下载CRL。以下是我们获取CRL分发点的方法:
xxxxxxxxxx
CRL_DIST_POINTS* crl_dist_points =
(CRL_DIST_POINTS*) X509_get_ext_d2i(
current_cert,
NID_crl_distribution_points,
NULL,
NULL);
CRL_DIST_POINTS
是 STACK_OF(DIST_POINT)
的类型定义,其中 DIST_POINT
是定义CRL分发点的结构。
接下来,我们必须遍历分发点,尝试从其中一个分发点下载CRL,并返回下载的CRL:
xxxxxxxxxx
int crl_dist_point_count =
sk_DIST_POINT_num(crl_dist_points);
for (int i = 0; i < crl_dist_point_count; i++) {
DIST_POINT* dist_point =
sk_DIST_POINT_value(crl_dist_points, i);
X509_CRL* crl =
download_crl_from_dist_point(
dist_point, error_stream);
if (!crl)
continue;
STACK_OF(X509_CRL)* crls = sk_X509_CRL_new_null();
sk_X509_CRL_push(crls, crl);
return crls;
}
return NULL;
lookup_crls()
函数到此结束。正如我们所看到的,如果无法下载CRL, lookup_crls()
函数将返回 NULL
。
您可能已经注意到,我们调用 download_crl_from_dist_point()
函数来下载CRL。我们也必须实现该函数。
让我们来看看下载CRL的功能代码:
download_crl_from_dist_point()
函数具有以下签名:
xxxxxxxxxx
X509_CRL* download_crl_from_dist_point(
const DIST_POINT* dist_point, FILE* error_stream);
分发点结构可以包含多个字符串,称为通用名称。让我们得到他们:
xxxxxxxxxx
const DIST_POINT_NAME* dist_point_name =
dist_point->distpoint;
if (!dist_point_name || dist_point_name->type != 0)
return NULL;
const GENERAL_NAMES* general_names =
dist_point_name->name.fullname;
if (!general_names)
return NULL;
然后,我们必须遍历通用名称:
xxxxxxxxxx
int general_name_count = sk_GENERAL_NAME_num(general_names);
for (int i = 0; i < general_name_count; i++) {
const GENERAL_NAME* general_name =
sk_GENERAL_NAME_value(general_names, i);
...
}
以下代码表示循环内的代码。首先,我们将在通用名称中查找URL:
xxxxxxxxxx
int general_name_type = 0;
const ASN1_STRING* general_name_asn1_string =
(const ASN1_STRING*) GENERAL_NAME_get0_value(
general_name, &general_name_type);
if (general_name_type != GEN_URI)
continue;
如果我们找到一个带有URL的通用名称,我们将得到它的文本表示:
xxxxxxxxxx
const char* url =
(const char*) ASN1_STRING_get0_data(
general_name_asn1_string);
在这个例子中,我们只支持HTTP URL,所以我们将跳过非HTTP URL。这很好,因为CRL分发点通常是HTTP URL:
xxxxxxxxxx
const char* http_url_prefix = "http://";
size_t http_url_prefix_len = strlen(http_url_prefix);
if (strncmp(url, http_url_prefix, http_url_prefix_len))
continue;
此时,我们发现了一个指向CRL的HTTP URL。让我们尝试下载它:
xxxxxxxxxx
fprintf(error_stream, "Found CRL URL: %s\n", url);
X509_CRL* crl = download_crl_from_http_url(url);
如果下载尝试失败,我们可以尝试另一个通用名称:
xxxxxxxxxx
if (!crl) {
fprintf(
error_stream,
"Failed to download CRL from %s\n", url);
continue;
}
如果我们已成功下载CRL,我们将返回它:
xxxxxxxxxx
fprintf(error_stream, "Downloaded CRL from %s\n", url);
return crl;
for
循环中的代码到此结束。
如果 for
循环完成,这意味着我们无法从当前的CRL分发点下载CRL。在这种情况下,我们将返回 NULL
:
xxxxxxxxxx
return NULL;
如果 download_crl_from_dist_point()
函数返回 NULL
,则调用 lookup_crls()
函数将尝试下一个分发点。
download_crl_from_dist_point()
函数调用 download_cr1_from_http_url()
函数从找到的url下载crl。让我们看看它的代码。
download_crl_from_http_url()
函数相对较短:
xxxxxxxxxx
X509_CRL* download_crl_from_http_url(const char* url) { BIO* bio = OSSL_HTTP_get(
url,
NULL /* proxy */,
NULL /* no_proxy */,
NULL /* wbio */,
NULL /* rbio */,
NULL /* bio_update_fn */,
NULL /* arg */,
65536 /* buf_size */,
NULL /* headers */,
NULL /* expected_content_type */,
1 /* expect_asn1 */,
50 * 1024 * 1024 /* max resp len */,
60 /* timeout */);
X509_CRL* crl = d2i_X509_CRL_bio(bio, NULL);
BIO_free(bio);
return crl;
}
在 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()
等。这些函数的名称由以下组件组成:
d2i
或 i2d
,其指定转换的方向。i
表示“内部”,内存中的内部C结构。 d
表示“DER”。因此, d2i
表示“DER到内部”, i2d
表示“内部到DER”。X509_CRL
。bio
后缀表示BIO,而 fp
后缀表示文件指针(file pointer)。如果没有后缀,DER编码对象将从内存中读取或写入内存。如前所述,并非所有证书都定义了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()
函数中:
xxxxxxxxxx
if (error_code == X509_V_ERR_UNABLE_TO_GET_CRL)
return 1;
这是我们必须添加到 crl-check
程序中以使其完整的最后几行。
我们的 crl-check
程序的完整源代码可以在本书的GitHub存储库中的 crl-check.c 文件中找到:
让我们运行我们的程序,看看它是如何工作的。我们将在 www.example.org
服务器上运行它,我们在前面的示例中也使用了该服务器:
xxxxxxxxxx
$ ./crl-check example.org 443
* lookup_crls() called with depth=0
Looking up CRL for certificate: C = US, ST = California, L = Los Angeles, O = Internet Corporation for Assigned Names and Numbers, CN = www.example.org
Found CRL URL: http://crl3.digicert.com/DigiCertTLSRSASHA2562020CA1-4.crl
Downloaded CRL from http://crl3.digicert.com/DigiCertTLSRSASHA2562020CA1-4.crl
* verify_callback() called with depth=2, preverify_ok=1, error_code=0, error_string=ok
Certificate Subject: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
* verify_callback() called with depth=1, preverify_ok=1, error_code=0, error_string=ok
Certificate Subject: C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
* verify_callback() called with depth=0, preverify_ok=1, error_code=0, error_string=ok
Certificate Subject: C = US, ST = California, L = Los Angeles, O = Internet Corporation for Assigned Names and Numbers, CN = www.example.org
TLS communication succeeded
正如我们所看到的,我们的 crl-check
程序成功地找到并下载了CRL,以便OpenSSL可以根据下载的CRL检查证书吊销。
让我们还检查一个负面情况——即证书被吊销。我们可以通过转到 revoked.badssl.com
服务器来查找已吊销的证书:
xxxxxxxxxx
$ ./crl-check revoked.badssl.com 443
* lookup_crls() called with depth=0
Looking up CRL for certificate: CN = revoked.badssl.com
Found CRL URL: http://crl3.digicert.com/RapidSSLTLSDVRSAMixedSHA2562020CA-1.crl
Downloaded CRL from http://crl3.digicert.com/RapidSSLTLSDVRSAMixedSHA2562020CA-1.crl
* verify_callback() called with depth=0, preverify_ok=0, error_code=23, error_string=certificate revoked
Certificate Subject: CN = revoked.badssl.com
Could not connect to server revoked.badssl.com on port 443
Errors from the OpenSSL error queue:
C081A6A4617F0000:error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:ssl/statem/statem_clnt.c:1882:
C081A6A4617F0000:error:0A000197:SSL routines:SSL_shutdown:shutdown while in init:ssl/ssl_lib.c:2242:
TLS communication failed
正如我们所看到的,服务器证书验证失败,因为证书已被吊销。我们可以在 error_code=23
,error_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查询。首先,让我们学习如何在命令行上检查证书的吊销。
可以使用 openssl
工具的 openssl ocsp
子命令在命令行上执行OCSP查询。让我们试试这个功能:
首先,我们需要服务器证书及其颁发者证书。我们可以使用 openssl s_client
子命令来获取它们:
xxxxxxxxxx
$ echo | openssl s_client \
-connect www.example.org:443 \
-showcerts
注意 -showcerts
开关。这指示 openssl
工具打印服务器发送到终端的证书。该命令将生成一个包含证书的长输出:
xxxxxxxxxx
Certificate chain
0 s:C = US, ST = California, L = Los Angeles, O = Internet\C2\A0Corporation\C2\A0for\C2\A0Assigned\C2\A0Names\C2\A0and\C2\A0Numbers, CN = www.example.org
i:C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
-----BEGIN CERTIFICATE-----
… base64 characters …
-----END CERTIFICATE-----
1 s:C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
i:C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
-----BEGIN CERTIFICATE-----
… base64 characters …
-----END CERTIFICATE-----
现在,我们可以将第一个PEM编码的证书从终端复制并粘贴到一些文件中。让我们将第一个打印的证书( CN = www.example.org
)保存到名为 www.example.org.cert.pem 的文件中,并将第二个证书( CN = DigiCert TLS RSA SHA256 2020 CA1
)保存到一个名为 DigiCert_Intermediate_CA.pem 的文件中。
我们已经获得了证书,但我们如何找到OCSP响应者URL?幸运的是,证书通常包含有关 权威信息访问(Authority Information Access)X509v3扩展中的OCSP响应者URL的信息。 www.example.org
网站的证书还包含该扩展名和OCSP响应者URL。我们可以使用 openssl x509
子命令的- ocsp_uri
开关提取此URL:
xxxxxxxxxx
$ openssl x509 \
-in www.example.org.cert.pem \
-noout \
-ocsp_uri
http://ocsp.digicert.com
然后,我们可以使用以下命令通过OCSP执行吊销检查:
xxxxxxxxxx
$ openssl ocsp \
-issuer DigiCert_Intermediate_CA1.pem \
-cert www.example.org.cert.pem \
-url http://ocsp.digicert.com
WARNING: no nonce in response
Response verify OK
www.example.org.cert.pem: good
This Update: Jun 12 05:09:01 2022 GMT
Next Update: Jun 19 04:24:01 2022 GMT
正如我们所看到的,证书仍然有效,这意味着它没有被撤销。Response verify OK
消息表示OCSP响应已成功验证:它已由预期的颁发者正确签名,并且没有过时。
如果我们在命令行中添加 -text
和 -resp_tex
t开关,我们将能够看到OCSP请求和响应的文本表示。运行以下命令:
xxxxxxxxxx
$ openssl ocsp \
-issuer DigiCert_Intermediate_CA1.pem \
-cert www.example.org.cert.pem \
-url http://ocsp.digicert.com \
-text \
-resp_text
运行上述命令将得到以下输出。输出的第一部分显示了OCSP请求:
xxxxxxxxxx
OCSP Request Data:
Version: 1 (0x0)
Requestor List:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash:
E4E395A229D3D4C1C31FF0980C0B4EC0098AABD8
Issuer Key Hash:
B76BA2EAA8AA848C79EAB4DA0F98B2C59576B9F4
Serial Number:
0FAA63109307BC3D414892640CCD4D9A
Request Extensions:
OCSP Nonce:
0410C5D14D224CCCEA68EEEC1478533D0DF8
输出的第二部分显示了OCSP响应以及响应和证书的验证状态:
xxxxxxxxxx
OCSP Response Data:
OCSP Response Status: successful (0x0)
Response Type: Basic OCSP Response
Version: 1 (0x0)
Responder Id: B76BA2EAA8AA848C79EAB4DA0F98B2C59576B9F4
Produced At: Jun 12 05:25:33 2022 GMT
Responses:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash:
E4E395A229D3D4C1C31FF0980C0B4EC0098AABD8
Issuer Key Hash:
B76BA2EAA8AA848C79EAB4DA0F98B2C59576B9F4
Serial Number:
0FAA63109307BC3D414892640CCD4D9A
Cert Status: good
This Update: Jun 12 05:09:01 2022 GMT
Next Update: Jun 19 04:24:01 2022 GMT
Signature Algorithm: sha256WithRSAEncryption
… hex bytes …
WARNING: no nonce in response
Response verify OK
www.example.org.cert.pem: good
This Update: Jun 12 05:09:01 2022 GMT
Next Update: Jun 19 04:24:01 2022 GMT
正如我们所看到的,OCSP请求和OCSP响应都相当简单。两者都包含一个复合证书ID和一个可选的随机数。响应包含重要的附加数据,如证书状态、有效性时间戳和签名。
现在我们已经通过OCSP检查了好证书的吊销状态,让我们来看看吊销的证书:
我们已经知道,我们可以在 revokeed.badssl.com
上找到一个已撤销的证书。与前面的示例一样,让我们连接到服务器并获取没有根CA证书的服务器证书链:
xxxxxxxxxx
$ echo | openssl s_client \
-connect revoked.badssl.com:443 \
-showcerts
让我们将命令输出中的证书保存到终端,将服务器证书保存到 revoked.badssl.com.cert.pem 文件,并将其颁发者证书保存到 RapidSSL_Intermediate_CA1.pem 文件。
接下来,让我们获取OCSP响应程序URL:
xxxxxxxxxx
$ openssl x509 \
-in revoked.badssl.com.cert.pem \
-noout \
-ocsp_uri
http://ocsp.digicert.com
最后,让我们检查服务器证书吊销状态:
xxxxxxxxxx
$ openssl ocsp \
-issuer RapidSSL_Intermediate_CA1.pem \
-cert revoked.badssl.com.cert.pem \
-url http://ocsp.digicert.com
WARNING: no nonce in response
Response verify OK
revoked.badssl.com.cert.pem: revoked
This Update: Jun 12 01:09:02 2022 GMT
Next Update: Jun 19 00:24:02 2022 GMT
Revocation Time: Oct 27 21:38:48 2021 GMT
正如我们所看到的,这一次,证书状态被报告为已吊销(revoked)。我们的观察证实, openssl ocsp
子命令可以获取良好证书和已吊销证书的吊销状态。
至此,我们已经学习了如何在命令行上使用OCSP。现在,让我们学习如何以编程方式使用OCSP。
我们可以通过以下方式以编程方式检查证书吊销状态:
现在,让我们编写一个小的 ocsp-check
程序,演示如何使用ocsp装订回调函数。
OCSP装订回调函数必须具有以下概要:
xxxxxxxxxx
int ocsp_callback(SSL* ssl, void* arg);
此函数接受以下参数:
ssl
:TLS连接对象arg
:用户数据指针回调函数应返回以下值之一:
0
要使用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
程序相同的三个命令行参数:
我们将使用一些以前没有使用过的OpenSSL函数。以下是相关的手册页及其文档:
xxxxxxxxxx
$ man SSL_CTX_set_tlsext_status_type
$ man OCSP_response_status
$ man SSL_get0_verified_chain
$ man SSL_get_SSL_CTX
$ man SSL_CTX_get_cert_store
$ man OCSP_basic_verify
$ man OCSP_cert_to_id
让我们从使用OCSP回调注册实现 ocsp-check
程序开始。
以下是我们如何注册OCSP回调:
首先,我们将声明以下函数。这将是我们的OCSP装订回调:
xxxxxxxxxx
int ocsp_callback(SSL* ssl, void* arg);
在 run_tls_client()
函数中,我们必须激活证书状态请求TLS扩展并提供OCSP装订回调:
xxxxxxxxxx
SSL_CTX_set_tlsext_status_type(ctx, TLSEXT_STATUSTYPE_ocsp);
SSL_CTX_set_tlsext_status_cb(ctx, ocsp_callback);
在这里,我们还可以使用 SSL_CTX_set_tlsext_status_arg()
函数提供用户数据空指针,但在这个例子中我们不需要它。我们已经使用 SSL_set_app_data()
函数提供了用户数据。
这些是我们提供和激活OCSP装订回调所需的唯一代码更改。现在,让我们实现回调本身。
以下是我们如何实现OCSP回调:
我们将通过定义函数的默认退出代码来开始回调的实现:
xxxxxxxxxx
int ocsp_callback(SSL* ssl, void* arg) {
int exit_code = 1;
在这个例子中,我想对服务器证书给予最大的怀疑。这意味着,只有当OCSP响应能够证明该证书时,我们才会考虑撤销该证书。如果OCSP响应有问题——例如,它不可解析或已过时——我们将忽略OCSP响应,并认为服务器证书仍然有效。我们这样做是因为TLS服务器从OCSP响应器获取OCSP响应时发生错误的可能性比证书被吊销的可能性大得多。
我们在OCSP装订回调中收到的第一个参数是指向SSL对象的指针,该对象存储有关当前TLS连接的信息。这意味着我们不必像 verify_callback()
函数那样以复杂的方式获取SSL对象。SSL对象对我们来说是即时可用的,我们可以很容易地从中获取用户数据:
xxxxxxxxxx
FILE* error_stream = SSL_get_app_data(ssl);
下一步非常重要,即获取OCSP响应:
xxxxxxxxxx
const unsigned char* resp = NULL;
long resp_len = SSL_get_tlsext_status_ocsp_resp(ssl, &resp);
if (resp_len <= 0 || !resp) {
if (error_stream)
fprintf(
error_stream,
"* ocsp_callback() called "
"without OCSP response\n");
goto cleanup;
}
前面的代码将获得作为DER编码数据的OCSP响应。
下一步是将该数据解码为 OCSP_RESPONSE
对象:
xxxxxxxxxx
OCSP_RESPONSE* ocsp_response =
d2i_OCSP_RESPONSE(NULL, &resp, resp_len);
if (!ocsp_response) {
if (error_stream)
fprintf(
error_stream,
"* ocsp_callback() could not decode "
"OCSP response\n");
goto cleanup;
}
接下来我们可以做的是打印解码的OCSP响应。从OpenSSL 3.0开始,OpenSSL库没有可以将OCSP响应打印到 FILE
流的函数。只有 OCSP_RESPONSE_print()
函数,它打印对BIO的响应。因此,我们必须使用 FILE
指针BIO:
xxxxxxxxxx
if (error_stream) {
BIO* bio = BIO_new_fp(error_stream, BIO_NOCLOSE);
assert(bio);
fprintf(
error_stream,
"* ocsp_callback() called "
"with the following OCSP response:\n");
fprintf(error_stream, " -----\n ");
OCSP_RESPONSE_print(bio, ocsp_response, 0);
fprintf(error_stream, " -----\n");
BIO_free(bio);
}
请注意,我们在创建BIO时使用了 BIO_NOCLOSE
标志。这非常重要。由于该标志,当我们释放BIO时,稍后对 BIO_free()
的调用将不会尝试关闭底层 FILE
流。
下一步是检查整个OCSP响应的总体状态:
xxxxxxxxxx
int res = OCSP_response_status(ocsp_response);
if (res != OCSP_RESPONSE_STATUS_SUCCESSFUL) {
if (error_stream)
fprintf(
error_stream,
"OCSP response status is not successful\n");
goto cleanup;
}
OCSP响应状态告诉我们OCSP响应程序是否可以成功处理OCSP请求并形成OCSP响应。
下一步是验证OCSP响应签名:
xxxxxxxxxx
OCSP_BASICRESP* ocsp_basicresp =
OCSP_response_get1_basic(ocsp_response);
STACK_OF(X509)* verified_chain =
SSL_get0_verified_chain(ssl);
SSL_CTX* ctx = SSL_get_SSL_CTX(ssl);
X509_STORE* x509_store = SSL_CTX_get_cert_store(ctx);
res = OCSP_basic_verify(
ocsp_basicresp, verified_chain, x509_store, 0);
if (res != 1) {
if (error_stream)
fprintf(
error_stream,
"OCSP response verification failed\n");
goto cleanup;
}
注意 SSL_get0_verified_chain()
函数调用。只有在构建了服务器证书的验证链后,才能调用此函数;否则,函数可能会返回部分或不正确的链。幸运的是,OCSP回调是在服务器证书验证后调用的,因此我们可以安全地调用 SSL_get0_verified_chain()
。
我们接下来要做的是在OCSP响应中查找TLS服务器证书的状态。OCSP响应可以包含有关多个证书的吊销信息,因此我们必须在响应中搜索所需证书的状态:
xxxxxxxxxx
X509* server_cert = sk_X509_value(verified_chain, 0);
X509* issuer_cert = sk_X509_value(verified_chain, 1);
OCSP_CERTID* server_cert_id =
OCSP_cert_to_id(NULL, server_cert, issuer_cert);
ASN1_GENERALIZEDTIME* revocation_time = NULL;
ASN1_GENERALIZEDTIME* this_update_time = NULL;
ASN1_GENERALIZEDTIME* next_update_time = NULL;
int revocation_status = V_OCSP_CERTSTATUS_UNKNOWN;
int revocation_reason = OCSP_REVOKED_STATUS_NOSTATUS;
res = OCSP_resp_find_status(
ocsp_basicresp,
server_cert_id,
&revocation_status,
&revocation_reason,
&revocation_time,
&this_update_time,
&next_update_time);
if (res != 1) {
if (error_stream)
fprintf(
error_stream,
"Server certificate status is not found "
" in the OCSP response\n");
goto cleanup;
}
我们刚才进行的 OCSP_resp_find_status()
调用为我们提供了有关证书吊销状态、生成OCSP响应的时间以及必须获取同一证书的下一个OCSP响应时间的信息。
让我们检查一下响应是否仍然有效:
xres = OCSP_check_validity(
this_update_time, next_update_time, 300, -1);
if (res != 1) {
if (error_stream)
fprintf(
error_stream,
"OCSP response is outdated\n");
goto cleanup;
}
现在我们已经验证了OCSP响应,让我们检查证书吊销状态,如果证书已吊销,则更改 exit_code
变量:
xxxxxxxxxx
switch (revocation_status) {
case V_OCSP_CERTSTATUS_REVOKED:
if (error_stream)
fprintf(
error_stream,
"Server certificate is revoked\n");
exit_code = 0;
break;
case V_OCSP_CERTSTATUS_GOOD:
if (error_stream)
fprintf(
error_stream,
"Server certificate is not revoked\n");
break;
default:
if (error_stream)
fprintf(
error_stream,
"Server certificate revocation status "
"is unknown\n");
}
我们将通过释放不再需要的对象并返回退出代码来完成OCSP装订回调函数:
xxxxxxxxxx
cleanup:
if (server_cert_id)
OCSP_CERTID_free(server_cert_id);
if (ocsp_basicresp)
OCSP_BASICRESP_free(ocsp_basicresp);
if (ocsp_response)
OCSP_RESPONSE_free(ocsp_response);
return exit_code;
}
我们的 ocsp-check
程序的完整源代码可以在本书的GitHub存储库中的 ocsp-check.c 文件中找到:
现在我们已经完成了 ocsp-check
程序的实现,让我们运行它。
我们可以这样运行 ocsp-check
程序:
xxxxxxxxxx
$ ./ocsp-check www.example.org 443
* verify_callback() called with depth=2, preverify_ok=1, error_code=0, error_string=ok
Certificate Subject: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
* verify_callback() called with depth=1, preverify_ok=1, error_code=0, error_string=ok
Certificate Subject: C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
* verify_callback() called with depth=0, preverify_ok=1, error_code=0, error_string=ok
Certificate Subject: C = US, ST = California, L = Los Angeles, O = Internet Corporation for Assigned Names and Numbers, CN = www.example.org
* ocsp_callback() called with the following OCSP response:
-----
OCSP Response Data:
…
Cert Status: good
…
-----
Server certificate is not revoked
TLS communication succeeded
正如我们所看到的,我们的 ocsp-check
程序可以通过OCSP成功检查服务器证书吊销状态。
在前面的部分中,我们学习了如何使用自定义验证回调、CRL和OCSP来广泛验证TLS服务器证书。这些是在TLS协议中使用X.509证书的重要方面。在下一节中,我们将了解在TLS中使用证书的另一个重要方面:TLS客户端证书。
在本节中,我们将学习如何使用TLS客户端证书:生成它们,将它们打包到公钥加密标准#12(Public Key Cryptography Standards #12,PKCS#12)容器中,从TLS服务器请求它们,并将它们提供给TLS客户端。
除了TLS服务器,TLS客户端还可以在TLS连接上提供证书。但是,根据TLS协议,默认情况下,TLS客户端不会发送其证书,即使客户端有证书。TLS客户端仅在TLS服务器请求时发送其证书。
TLS客户端证书的另一个特点是,客户端证书通常以PKCS#12格式与其证书私钥(或更确切地说是密钥对)一起存储。PKCS#12是一种文件格式,可以存储多个加密对象,如X.509证书和密钥对。这些存储的对象可以使用对称密码进行加密,并使用基于哈希的消息身份验证码(Hash-based Message Authentication Code,HMAC)进行身份验证。具有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证书及其密钥对:
xxxxxxxxxx
$ openssl req \
-newkey ED448 \
-subj "/CN=Client certificate" \
-addext "basicConstraints=critical,CA:FALSE" \
-noenc \
-keyout client_keypair.pem \
-out client_csr.pem
$ openssl x509 \
-req \
-in client_csr.pem \
-copy_extensions copyall \
-CA ca_cert.pem \
-CAkey ca_keypair.pem \
-days 3650 \
-out client_cert.pem
运行指定的命令后,我们将把客户端证书及其密钥对保存到PEM文件中。在下一节中,我们将学习如何将它们打包到PKCS#12容器中。
要将生成的客户端证书、其密钥对和CA证书打包到PKCS#12容器中,我们可以使用 openssl
命令行工具的 openssl pkcs12
子命令。此子命令的文档可在 openssl pkcs12
手册页上找到:
xxxxxxxxxx
$ man openssl-pkcs12
以下是我们如何将密钥对和证书打包到PKCS#12容器中:
xxxxxxxxxx
$ openssl pkcs12 \
-export \
-inkey client_keypair.pem \
-in client_cert.pem \
-certfile ca_cert.pem \
-passout 'pass:SuperPa$$w0rd' \
-out client_cert.p12
请注意,我们为PKCS#12容器提供了一个密码,即 SuperPa$$w0rd
。在命令行上提供密码不是很安全,因为命令可能会保存在命令的历史记录中,并可能在进程列表中可见。在这个例子中,我们这样做只是为了简单。请注意, openssl
工具支持更安全的密码提供方式,例如从文件或stdin读取密码。您可以在以下手册页上阅读有关向 openssl
工具提供密码的更多信息:
xxxxxxxxxx
$ man openssl-passphrase-options
至此,我们成功创建了PKCS#12容器文件。现在,我们可以检查里面有什么:
xxxxxxxxxx
$ openssl pkcs12 \
-in client_cert.p12 \
-passin 'pass:SuperPa$$w0rd' \
-noenc \
-info
MAC: sha256, Iteration 2048
MAC length: 32, salt length: 8
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
Certificate bag
Bag Attributes
localKeyID: 63 8B 00 96 4A 4D B7 E9 AF BD C2 09 A5 4A B8 3D A2 FB 40 85
subject=CN = Client certificate
issuer=CN = Root CA
-----BEGIN CERTIFICATE-----
… base64-encoded data …
-----END CERTIFICATE-----
Certificate bag
Bag Attributes: <No Attributes>
subject=CN = Root CA
issuer=CN = Root CA
-----BEGIN CERTIFICATE-----
… base64-encoded data …
-----END CERTIFICATE-----
PKCS7 Data
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
Bag Attributes
localKeyID: 63 8B 00 96 4A 4D B7 E9 AF BD C2 09 A5 4A B8 3D A2 FB 40 85
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
… base64-encoded data …
-----END PRIVATE KEY-----
正如我们所看到的,生成的PKCS#12容器文件包含客户端证书、CA证书和客户端证书密钥对。容器的内容使用AES-256-CBC加密,并使用HMAC-SHA256进行身份验证。
现在我们已经准备好了客户端证书,让我们实现一个小型TLS服务器程序来请求和验证它。
在本节中,我们将编写一个小型TLS服务器程序,用于请求和验证TLS客户端证书。我们将基于【第9章“建立tls连接并通过它们发送数据”】中的 tls-server
程序。我们将采用 tls-server
程序的代码并对其进行进一步开发。我们的新程序将被称为 tls-server2
。
我们的 tls-server2
程序将采用以下命令行参数:
为了能够验证客户端证书,我们必须加载相应的可信CA证书并启用对等证书验证。
以下是我们如何验证客户端证书:
首先,我们必须加载所需的可信CA证书。我们必须捕获第四个命令行参数并将其发送到 run_tls_server()
函数。让我们在 main()
函数中添加以下行:
xxxxxxxxxx
const char* trusted_cert_fname = argv[4];
我们还必须将 trusted_cert_fname
参数传递给 run_tls_server()
函数,并在加载和检查私钥的代码之后在 run_tls_server()
功能中添加以下代码:
xxxxxxxxxx
err = SSL_CTX_load_verify_locations(
ctx, trusted_cert_fname, NULL);
if (err <= 0) {
if (error_stream)
fprintf(
error_stream,
"Could not load trusted certificates\n");
goto failure;
}
我们还必须更改以下代码以请求客户端证书并启用其验证:
xxxxxxxxxx
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
SSL_VERIFY_PEER
标志指示OpenSSL请求客户端证书。请注意,如果客户端没有提供证书,则证书的缺失不会被视为服务器端的错误。通过将 SSL_VERIFY_FAIL_IF_NO_PEER_CERT
标志与 SSL_VERIFY_PEER
标志一起设置,可以更改此行为,如下所示:
xxxxxxxxxx
SSL_CTX_set_verify(
ctx,
SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
NULL);
如果 SSL_VERIFY_PEER
和 SSL_VERIIF_FAIL_If_NO_PEER_CERT
标志都已设置,并且客户端没有提供证书,则TLS握手将失败。
此代码足以请求客户端证书并对其进行验证。但是,打印客户端证书主题也很好,以确保TLS客户端已发送其证书,TLS服务器已对其进行了验证。为此,我们将引入一个 construct_response()
函数,该函数将构造一个包含客户端证书信息的服务器响应。在 tls-server2
中,我们将使用该函数创建的响应,而不是我们在原始 tls-server
程序中使用的静态响应。
该函数将具有以下签名:
xxxxxxxxxx
BIO* construct_response(SSL* ssl);
获取客户端证书需要ssl参数。该函数将返回一个包含服务器响应的内存BIO。
让我们实现 construct_response()
函数:
我们将首先创建一个内存BIO并在那里打印服务器响应头:
xxxxxxxxxx
BIO* mem_bio = BIO_new(BIO_s_mem());
const char* response_headers =
"HTTP/1.0 200 OK\r\n"
"Content-type: text/plain\r\n"
"Connection: close\r\n"
"Server: Example TLS server\r\n"
"\r\n";
BIO_puts(mem_bio, response_headers);
下一步是获取客户端证书:
xxxxxxxxxx
X509* peer_cert = SSL_get_peer_certificate(ssl);
请注意,在我们的示例中,我们没有使用 SSL_VERIFY_FAIL_IF_NO_PEER_CERT
标志,这就是为什么TLS客户端可以选择不提供证书的原因。在这种情况下, peer_cert
指针将为 NULL
。我们稍后必须考虑到这一点。
下一步是将有关客户端证书或其缺失的信息打印到内存BIO:
xxxxxxxxxx
if (peer_cert) {
X509_NAME* peer_cert_subject =
X509_get_subject_name(peer_cert);
BIO_puts(
mem_bio,
"The TLS client certificate subject:\n");
X509_NAME_print_ex(
mem_bio,
peer_cert_subject,
0,
XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB);
BIO_puts(mem_bio, "\n");
X509_free(peer_cert);
} else {
BIO_puts(
mem_bio,
"The TLS client has not provided a certificate\n");
}
我们通过返回内存BIO来完成 construct_response()
函数:
xxxxxxxxxx
return mem_bio;
非常好——我们已经实现了 construct_response()
函数!我们将从 handle_accepted_connection()
函数调用该函数,而不是从构造静态响应并计算其长度的代码调用。
以下代码来自原始 tls-server
程序:
xxxxxxxxxx
const char* response =
"HTTP/1.0 200 OK\r\n"
"Content-type: text/plain\r\n"
"Connection: close\r\n"
"Server: Example TLS server\r\n"
"\r\n"
"Hello from the TLS server!\n";
int response_length = strlen(response);
我们必须用以下代码替换它:
xxxxxxxxxx
BIO* mem_bio = construct_response(ssl);
char* response = NULL;
long response_length = BIO_get_mem_data(mem_bio, &response);
别忘了释放我们在新代码中引入的内存BIO!为了释放它,我们必须在 handle_accepted_connect()
函数的末尾添加以下代码:
xxxxxxxxxx
if (mem_bio)
BIO_free(mem_bio);
这些都是我们需要对TLS服务器代码进行的更改。
tls-server2
程序的完整源代码可以在本书的GitHub存储库 tls-server2.c 文件中找到:
让我们运行 tls-server2
程序,看看它是如何工作的:
我们还没有编写提供客户端证书的TLS客户端程序。但是,我们可以使用 openssl s_client
子命令或 curl
实用程序进行测试。我们需要两个终端窗口——一个用于TLS服务器,另一个用于SSL客户端。让我们在第一个终端窗口中启动 tls-server2
程序:
xxxxxxxxxx
$ ./tls-server2 \
4433 \
server_keypair.pem \
server_cert.pem \
ca_cert.pem
*** Listening on port 4433
让我们在第二个终端窗口中使用客户端证书作为TLS客户端启动 openssl
工具:
xxxxxxxxxx
$ openssl s_client \
-connect localhost:4433 \
-CAfile ca_cert.pem \
-key client_keypair.pem \
-cert client_cert.pem
当启动 openssl
时,它将连接到 tls-server2
并输出大量信息。之后只需按 Enter 键。 openssl
工具将输出以下内容:
xxxxxxxxxx
HTTP/1.0 200 OK
Content-type: text/plain
Connection: close
Server: Example TLS server
The TLS client certificate subject:
CN = Client certificate
让我们检查运行 tls-server2
的第一个终端窗口。我们将看到以下输出:
xxxxxxxxxx
*** Receiving from the client:
*** Receiving from the client finished
*** Sending to the client:
HTTP/1.0 200 OK
Content-type: text/plain
Connection: close
Server: Example TLS server
The TLS client certificate subject:
CN = Client certificate
*** Sending to the client finished
正如我们所看到的,我们的 tls-server2
程序已经请求、接收并成功验证了客户端证书,并报告了客户端证书的主题。
我们可以使用 curl
实用程序执行相同的测试。让我们在第二个终端窗口中运行它:
xxxxxxxxxx
$ curl \
https://localhost:4433 \
--cacert ca_cert.pem \
--cert 'client_cert.p12:SuperPa$$w0rd' \
--cert-type P12
The TLS client certificate subject:
CN = Client certificate
请注意,我们提供了PKCS#12格式的 curl
客户端证书。
正如我们所看到的,如果tls客户端提供了客户端证书,我们的 tls-server2
程序可以提供关于客户端证书的良好报告。但是如果TLS客户端不提供证书,会发生什么?
让我们使用 openssl
工具检查一下。让我们在没有证书的情况下启动 openssl s_client
:
xxxxxxxxxx
$ openssl s_client \
-connect localhost:4433 \
-CAfile ca_cert.pem
连接并按 Enter 键后,我们将得到以下报告:
xxxxxxxxxx
HTTP/1.0 200 OK
Content-type: text/plain
Connection: close
Server: Example TLS server
The TLS client has not provided a certificate
我们将使用 curl
得到类似的报告:
xxxxxxxxxx
$ curl https://localhost:4433 --cacert ca_cert.pem
The TLS client has not provided a certificate
由此,我们可以得出结论,我们的 tls-server2
程序按预期工作。它可以请求、验证和报告有关TLS客户端证书的信息,以及优雅地处理客户端证书的缺失。
tls-server2
程序将运行并接受连接,直到中止。您可以在运行它的终端窗口中按 Ctrl+C 中止它。
至此,我们已经学习了如何从服务器端请求客户端证书并对其进行验证。在下一节中,我们将学习如何将TLS客户端证书加载到客户端并将其用于TLS连接。
在本节中,我们将编写一个使用TLS客户端证书的小型TLS客户端程序。我们将基于【第9章“建立TLS连接并通过它们发送数据”】中的 tls-client
程序。在这里,我们将采用 tls-client
程序的代码并对其进行进一步开发。我们的新程序将被称为 tls-client2
。
我们的 tls-client2
程序将采用以下命令行参数:
与原始 tls-client
程序相比, tls-client2
多了两个参数。所有五个参数现在都是强制性的。
我们将从PKCS#12容器文件加载客户端证书。也可以使用 SSL_CTX_use_certificate_chain_file()
和 SSL_CTX_use_PrivateKey_file()
函数从PEM文件加载客户端证书及其密钥对,就像我们在 tls-server
和 tls-server2
程序中所做的那样。但是,我想演示从PKCS#12文件加载,因为客户端证书通常分布在PKCS#12文档中。
我们将使用一些以前没有使用过的OpenSSL函数。以下是相关的手册页及其文档:
xxxxxxxxxx
$ man BIO_new_file
$ man d2i_PKCS12_bio
$ man PKCS12_parse
$ man SSL_CTX_use_cert_and_key
$ man PKCS12_free
现在,让我们开始实现 tls-client2
程序,并对从 tls-client
程序继承的代码进行更改。
以下是我们需要改变的:
由于我们还有两个命令行参数,我们应该在 main()
函数的命名变量中捕获它们:
xxxxxxxxxx
const char* client_cert_fname = argv[4];
const char* client_cert_password = argv[5];
我们还必须将这两个新参数传递给 run_tls_client()
函数。因此, run_tls_client()
函数看起来像这样:
xxxxxxxxxx
int run_tls_client(
const char* hostname,
const char* port,
const char* trusted_cert_fname,
const char* client_cert_fname,
const char* client_cert_password,
FILE* error_stream);
main()
函数中的 run_tls_client()
调用如下:
xxxxxxxxxx
run_tls_client(
hostname,
port,
trusted_cert_fname,
client_cert_fname,
client_cert_password,
stderr);
在 run_tls_client()
函数中,加载受信任的证书后,我们必须向 load_client_certificate()
函数添加一个调用,将客户端证书加载到 SSL_CTX
对象中:
xxxxxxxxxx
int load_exit_code = load_client_certificate(
client_cert_fname,
client_cert_password,
ctx,
error_stream);
if (load_exit_code != 0)
goto failure;
现在,我们必须实现 load_client_certificate
函数。
load_client_certificate
函数将具有以下签名:
xxxxxxxxxx
client_certificate(
const char* client_cert_fname,
const char* client_cert_password,
SSL_CTX* ctx,
FILE* error_stream);
函数成功时返回 0
,失败时返回非零退出代码。
让我们继续函数的实现:
首先,我们必须定义函数退出代码,如果发生故障,可以更改该代码:
xxxxxxxxxx
int exit_code = 0;
然后,我们必须创建一个文件BIO并打开客户端证书文件进行读取:
xxxxxxxxxx
BIO* client_cert_bio = BIO_new_file(client_cert_fname, "rb");
if (!client_cert_bio) {
if (error_stream)
fprintf(
error_stream,
"Could not open client certificate file %s\n",
client_cert_fname);
goto failure;
}
下一步是将客户端证书文件加载到 PKCS12
对象中:
xxxxxxxxxx
PKCS12* pkcs12 = d2i_PKCS12_bio(client_cert_bio, NULL);
if (!pkcs12) {
if (error_stream)
fprintf(
error_stream,
"Could not load client certificate "
"from file %s\n",
client_cert_fname);
goto failure;
}
请注意,我们可以使用 fopen()
打开客户端证书文件,并使用 d2i_PKCS12_fp()
加载它。然而,我想演示另一种BIO类型的用法—— file
BIO。
d2i_PKCS12_bio()
函数已加载PKCS#12容器,但未对其进行解密或解析。下一步是验证密码,以便我们可以解密PKCS#12集装箱。此步骤是可选的,但对于故障排除可能很有用:
xxxxxxxxxx
int res = PKCS12_verify_mac(
pkcs12,
client_cert_password,
strlen(client_cert_password));
if (res != 1) {
if (error_stream)
fprintf(
error_stream,
"Invalid password was provided "
"for client certificate file %s\n",
client_cert_fname);
goto failure;
}
在我们验证了PKCS#12容器密码后,我们必须尝试解密它,然后解析并获取验证链的客户端证书、密钥对和CA证书:
xxxxxxxxxx
res = PKCS12_parse(
pkcs12,
client_cert_password,
&pkey,
&cert,
&cert_chain);
if (res != 1) {
if (error_stream)
fprintf(
error_stream,
"Could not decode client certificate "
"loaded from file %s\n",
client_cert_fname);
goto failure;
}
如果PKCS#12容器已成功解析,我们可以将客户端证书、其密钥对和CA证书设置为 SSL_CTX
对象:
xxxxxxxxxx
res = SSL_CTX_use_cert_and_key(
ctx,
cert,
pkey,
cert_chain,
1);
if (res != 1) {
if (error_stream)
fprintf(
error_stream,
"Could not use client certificate "
"loaded from file %s\n",
client_cert_fname);
goto failure;
}
请注意, SSL_CTX_use_cert_and_key()
函数将增加 cert
、 pkey
和 cert_chain
对象的引用计数。因此,我们必须在函数结束时释放它们。由于 SSL_CTX
对象仍将保存它们的引用,因此在函数结束时释放时,上述对象将不会被释放。相反,当 SSL_CTX
对象本身被释放时,它们将被释放。
当涉及到潜在的故障排除时,最好检查加载的客户端证书密钥对是否与客户端证书匹配:
xxxxxxxxxx
res = SSL_CTX_check_private_key(ctx);
if (res != 1) {
if (error_stream)
fprintf(
error_stream,
"Client keypair does not match "
"client certificate\n");
goto failure;
}
此时,我们已经成功加载了客户端证书、其密钥对和CA证书,并将其设置为 SSL_CTX
对象。
现在,让我们通过释放已使用的对象并返回退出代码来完成 load_client_certificate()
函数代码:
xxxxxxxxxx
goto cleanup;
failure:
exit_code = 1;
cleanup:
if (cert_chain)
sk_X509_pop_free(cert_chain, X509_free);
if (cert)
X509_free(cert);
if (pkey)
EVP_PKEY_free(pkey);
if (pkcs12)
PKCS12_free(pkcs12);
if (client_cert_bio)
BIO_free(client_cert_bio);
return exit_code;
}
请注意,BIO_free
(client_cert_bio
)调用将在释放文件BIO之前关闭底层文件。
至此,我们已经完成了 load_client_certificate()
函数和整个 tls-client2
程序的实现。我们 tls-client2
程序的完整源代码可以在本书的GitHub存储库 tls-client2.c 文件中找到:
现在,让我们试着运行 tls-client2
程序并检查它是如何工作的:
与前面的示例一样,我们需要两个终端窗口。在第一个窗口中,我们将运行 tls-server2
程序:
xxxxxxxxxx
$ ./tls-server2 \
4433 \
server_keypair.pem \
server_cert.pem \
ca_cert.pem
*** Listening on port 4433
在第二个终端窗口中,我们将运行 tls-client2
程序:
xxxxxxxxxx
$ ./run-tls-client2.sh
*** Sending to the server:
GET / HTTP/1.1
Host: localhost
Connection: close
User-Agent: Example TLS client
*** Sending to the server finished
*** Receiving from the server:
HTTP/1.0 200 OK
Content-type: text/plain
Connection: close
Server: Example TLS server
The TLS client certificate subject:
CN = Client certificate
*** Receiving from the server finished
TLS communication succeeded
正如我们所看到的, tls-client2
可以加载客户端证书并在tls连接中使用它。 tls-server2
程序报告它已在服务器端获得客户端证书。由此,我们可以得出结论, tls-client2
程序按预期工作。
至此,我们已经学习了如何使用TLS客户端证书。现在,让我们总结一下这一章。
本章与前几章不同。在前面的章节中,我们每章都讨论了一个大主题。然而,在本章中,我们讨论了几个小主题,并编写了更多的示例程序。
在本章中,我们学习了如何在TLS握手期间验证对等证书。我们还学习了CRL和OCSP方法。然后,我们学习了TLS客户端证书和PKCS#12容器。之后,我们学习了如何在服务器端请求和验证TLS客户端证书,以及如何在客户端加载和使用客户端证书。我们通过将支持TLS客户端证书的TLS服务器和TLS客户端程序相互连接来完成本章。
您从本章中获得的知识将帮助您在启用TLS的程序中对TLS连接执行高级客户端和服务器证书处理。
在下一章中,我们将了解TLS的更高级和特殊用途。