【发布时间】:2016-08-01 18:25:12
【问题描述】:
我正在使用 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 内部行为有更多了解的人可以对此有所了解。
我注意到 openssl 实用程序提供的股票 s_client 在使用 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 告诉我连接失败的原因,我不需要访问原始证书来调试我的问题。在任何其他情况下,这都是正确的,但由于我将此客户端用作涉及 Internet 上使用无效证书的研究项目的一部分,因此我需要能够将失败的证书保存到文件中。
** - 据我所知,传递 NULL 作为 SSL_CTX_set_verify 的 verify_callback 参数告诉 OpenSSL 使用内置的默认函数进行证书验证。 The documentation 对此不是很清楚。
【问题讨论】:
-
您的“编辑 2”命名了正确的解决方案。当你传递 NULL 时,我看不出有什么不清楚的地方。