我正在使用OpenSSL库编写一个简单的SSL客户端。我希望能够在连接完成后打印服务器提供的证书链。连接成功完成后,这不是问题。但是,如果由于某种原因连接失败,我将无法获得服务器提供的失败证书。这是一个SSCCE来证明这一点。
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <unistd.h>
#include <openssl/ssl.h>
#include <openssl/x509_vfy.h>
#include <openssl/err.h>
#define CIPHER_LIST "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"
// #define HOST "google.com" // works
#define HOST "expired.badssl.com" // does not print presented certificate
void print_certificates(SSL *ssl){
STACK_OF(X509) * sk = SSL_get_peer_cert_chain(ssl);
X509* cert = NULL;
char sbuf[1024];
char ibuf[1024];
if(sk == NULL){
printf("Cert chain is null!\n");
}
for (int i = 0; i < sk_X509_num(sk); i++) {
cert = sk_X509_value(sk, i);
fprintf(stdout, "Subject: %s\n", X509_NAME_oneline(X509_get_subject_name(cert), sbuf, 1024));
fprintf(stdout, "Issuer: %s\n", X509_NAME_oneline(X509_get_issuer_name(cert), ibuf, 1024));
PEM_write_X509(stdout, cert);
}
}
void verify_cert(SSL *ssl, char* host){
print_certificates(ssl);
X509* cert = SSL_get_peer_certificate(ssl);
if(cert) { X509_free(cert); }
int res = SSL_get_verify_result(ssl);
if(!(X509_V_OK == res)){
printf("ERROR (NOT VERIFIED - %s): %s\n", X509_verify_cert_error_string(res), host);
return;
}
printf("SUCCESS: %s\n", host);
fflush(stdout);
}
int main(int argc, char **argv){
SSL * ssl = NULL;
SSL_CTX *ctx = NULL;
BIO *bio = NULL;
int res;
SSL_library_init();
SSL_load_error_strings();
const SSL_METHOD* method = SSLv23_method();
if(method == NULL)
goto End;
ctx = SSL_CTX_new(method);
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
if (ctx == NULL)
goto End;
SSL_CTX_set_options(ctx, SSL_OP_ALL | SSL_OP_NO_SSLv2 |
SSL_OP_NO_SSLv3);
res = SSL_CTX_set_default_verify_paths(ctx);
if (res != 1)
goto End;
bio = BIO_new_ssl_connect(ctx);
if (bio == NULL)
return 0;
BIO_set_conn_hostname(bio, HOST);
BIO_set_conn_port(bio, "443");
BIO_set_nbio(bio, 1);
BIO_get_ssl(bio, &ssl);
if(ssl == NULL)
goto End;
SSL_set_cipher_list(ssl, CIPHER_LIST);
res = SSL_set_tlsext_host_name(ssl, HOST);
int still_connecting = 1;
while(still_connecting){
int res = SSL_connect(ssl);
if (res <= 0){
unsigned long error = SSL_get_error(ssl, res);
if ( (error != SSL_ERROR_WANT_CONNECT) &&
(error != SSL_ERROR_WANT_READ) && (error != SSL_ERROR_WANT_WRITE) )
{
printf("Connection encountered fatal error\n");
ERR_print_errors_fp(stdout);
still_connecting = 0;
}
}
else{
printf("Connection completed succesfully\n");
still_connecting = 0;
}
}
verify_cert(ssl, HOST);
End:
return 0;
}
(最快的编译方法是gcc sscce.c -lcrypto -lssl -o sscce
)。
每当SSL_connect(ssl)
returns 1时(只要连接成功),print_certificates(ssl)
就会按预期工作。但是,如果SSL_connect(ssl)
返回除1之外的任何内容(连接失败),则print_certificates(ssl)
不会打印任何内容,因为SSL_get_peer_cert_chain(ssl)
返回null。虽然这是不提供证书的服务器的逻辑行为,但在提供无效证书的服务器上,无法访问证书会导致调试服务器配置问题变得困难。
有趣的是,SSL_get_verify_result(ssl)
在连接失败时返回正确的错误代码,尽管我自己无法获得证书链。*我已经通过OpenSSL代码库稍微查看了一下为什么会这样,虽然我仍然在努力理解所有内容是如何组合在一起的,但ssl_verify_cert_chain
function frees the certificates after performing the error checking中有一些代码。我猜测在上面的示例代码SSL_connect
中,一旦它具有完整的证书链,就运行一些内置的验证代码,在证书到达print_certificates
之前释放它。这让我感到困惑,因为我不明白为什么证书在验证失败时会被释放,而不是在成功时释放。或许更了解OpenSSL内部行为的人可以对此有所了解。
我注意到s_client
实用程序提供的库存openssl
在使用showcerts选项(openssl s_client -showcerts -connect expired.badssl.com:443
)运行时不会出现此行为。无论连接成功还是失败,都会打印证书。我的SSCCE中的print_certificates
函数只是s_client cert printing code的修改版本,但s_client不使用SSL_connect
,因此它表现出不同的行为并不令人惊讶。我注意到s_client sets a custom certificate verify callback(defined here),但我不愿意使用除默认**验证函数之外的任何内容。
tl;如果服务器提供无效的证书链,则dr SSL_get_peer_cert_chain(ssl)
将返回null。如何解决此问题以打印失败的证书链?
编辑:我已经确认当我将BIO状态设置为阻塞时,仍然会出现此问题,并且,当使用LibreSSL编译上述代码时,(以及它的价值)。
编辑2:我发现创建一个只返回1并将其作为回调函数(而不是NULL)传递给SSL_CTX_set_verify
的函数会导致SSL_get_peer_cert_chain(ssl)
按预期返回证书链,尽管无效链中的证书。但是,我不愿意将此称为问题的解决方案,因为我显然必须在此处覆盖OpenSSL的内置函数之一。
* - 对此的明显反应是,由于OpenSSL告诉我连接失败的原因,我不需要访问原始证书来调试我的问题。在任何其他情况下,这都是事实,但由于我使用此客户端作为涉及互联网上无效证书使用的研究项目的一部分,我需要能够将失败的证书保存到文件中。
** - 据我所知,传递NULL作为SSL_CTX_set_verify
的verify_callback参数告诉OpenSSL使用内置默认函数进行证书验证。 The documentation对此并不十分清楚。
答案 0 :(得分:2)
TLDR:使用回调。
只有连接(握手)成功,所有会话参数(包括证书链)才可用;这是因为如果握手没有成功,就没有办法确信任何结果都是有效的。从理论上讲,收到的证书可能是一个特例,但是一个特殊情况会更复杂,因为您可能已经注意到OpenSSL API已经很复杂,人们经常搞砸使用它。
如您所见s_client
设置验证回调,强制接受任何证书链,即使是无效的证书链;这会导致握手成功,并且包括cert链的参数可用。 s_client
旨在作为一种测试工具,如果数据确实是安全的,那么它并不重要。
如果只想连接到已验证的服务器,请使用默认验证逻辑。如果您想要连接到未经验证的服务器并处理被截获和/或篡改的数据的风险(在您的情况下可能很小),使用回调。 回调的原因是允许应用程序控制。
事实s_client
使用SSL_set_connect_state
在第一次数据传输之前导致握手,而不是显式调用SSL_connect
,这是无关紧要的,并没有任何区别。
ADDED:您可以在使用回调后检测到错误 - 甚至没有!
首先要明确的是,我们在这里讨论的回调(&#39;验证&#39;回调)除内置链验证逻辑外还使用 。有一个不同的回调,名称非常相似,&#39; cert verify&#39;回调,你不想要。引用the man page
使用内置验证程序或使用SSL_CTX_set_cert_verify_callback设置的其他应用程序提供的验证功能执行实际验证程序。以下描述适用于内置过程。应用程序提供的过程也可以访问验证深度信息和verify_callback()函数,但使用此信息的方式可能不同。
正如(方便地超链接)SSL_[CTX_]set_cert_verify_callback
man page所说
[默认]使用内置验证功能。如果通过SSL_CTX_set_cert_verify_callback()指定了验证回调回调[显然是一个错字],则会调用提供的回调函数。 [...]
提供包括证书目的设置等的完整验证程序是一项复杂的任务。内置过程非常强大,在大多数情况下,使用verify_callback函数修改其行为应该足够了。
在哪里&#39; verify_callback函数&#39;表示set_verify
而不是set_verify_callback
。实际上这并不准确;始终使用内置逻辑的部分,并且只有 part 替换为cert验证回调。但你仍然不想这样做,只有验证回调。
SSL_[CTX_]set_verify[_depth]
页面继续描述内置逻辑:
SSL_CTX_set_verify_depth()和SSL_set_verify_depth()设置在验证过程中使用链中的深度证书的限制。 [...]
从最深的嵌套级别(根CA证书)开始检查证书链,并向上处理到对等证书。在每个级别检查签名和颁发者属性。每当发现验证错误时,错误号存储在x509_ctx中,并且使用preverify_ok = 0调用verify_callback。 [...]如果没有找到证书的错误,则在进入下一级别之前使用preverify_ok = 1调用verify_callback。
verify_callback的返回值控制进一步验证过程的策略。如果verify_callback返回0,则验证过程立即停止,并且#34;验证失败&#34;州。 [,,,]如果verify_callback返回1,则继续验证过程。如果verify_callback始终返回1,则不会因验证失败而终止TLS / SSL握手,并且将建立连接。但是,调用进程可以使用SSL_get_verify_result或通过维护由verify_callback管理的自己的错误存储来检索上一个验证错误的错误代码。
如果未指定verify_callback,则将使用默认回调。它的返回值与preverify_ok相同,因此如果设置了SSL_VERIFY_PEER,任何验证失败都会导致TLS / SSL握手终止并发出警报消息。
所以回调有选项强制接受内置逻辑发现错误(调用0,返回1)或强制失败,即使内置逻辑认为证书链正常(调用使用1,返回0)但如果它没有内置逻辑控件。
使用客户端身份验证时s_client
(和s_server
使用的特殊回调,但这种情况相对较少)打印每个证书的主题名称及其状态(标记为& #39;从内置逻辑验证返回&#39;)但返回 1,从而强制(其余的验证和)连接继续,无论发现任何错误。
请注意第二段&#34;每当发现验证错误时,错误号存储在x509_ctx中,然后使用preverify_ok = 0&#34;调用[then] verify_callback。然后在第三段&#34;调用过程可以使用SSL_get_verify_result&#34;检索上一个验证错误的错误代码。 - 即使回调强制ok = 1,也是如此。
但是在查看参考文献时,我发现了更好的解决方案我错过了该页面:如果您只是默认(或设置)模式SSL_VERIFY_NONE
(强调和澄清补充):
客户端模式:如果不使用匿名密码(默认情况下已禁用),服务器将发送证书,该证书将被内置逻辑检查,即使NONE会让您认为它不是#39; t勾选]。使用SSL_get_verify_result函数在TLS / SSL握手[完成]之后,可以检查证书验证过程的结果。无论验证结果如何,握手都将继续。
这实际上与强制ok = 1并完成你想要的回调相同。
答案 1 :(得分:0)
可以使用SSL_CTX_set_cert_verify_callback(ctx, own_cert_verify_callback, &cert)
通过给定指向STACK_OF(X509)*
的指针使用自己的验证回调。回调如下所示:
static int own_cert_verify_callback(X509_STORE_CTX * ctx, void * arg) {
int result = 0;
STACK_OF(X509) ** untrusted_chain = (STACK_OF(X509) **) (arg);
result = X509_verify_cert(ctx);
if (result != 1) {
*untrusted_chain = X509_chain_up_ref(X509_STORE_CTX_get0_untrusted(ctx));
}
return result;
}
因此使用默认的证书验证功能,当证书无效时,会将浅表副本放置到arg中,以便以后使用。