【问题标题】:Get certificate information after connection error连接错误后获取证书信息
【发布时间】: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 functionfrees 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 时,我看不出有什么不清楚的地方。

标签: ssl openssl


【解决方案1】:

TLDR:使用回调。

包括证书链在内的所有会话参数只有在连接(握手)成功时才可用;这是因为如果握手不成功,就无法确信任何结果都是有效的。理论上,接收到的证书可能是一种特殊情况,但一种特殊情况会更复杂,而且您可能已经注意到 OpenSSL API 已经足够复杂,人们经常使用它。

如您所见,s_client 设置了一个验证回调,强制接受任何证书链,即使是无效的;这会导致握手成功并且包括证书链在内的参数可用。 s_client 旨在作为一种测试工具,无论数据是否真的安全。

如果您只想连接到经过验证的服务器,请使用默认验证逻辑。如果您想连接到未经验证的服务器并处理数据被拦截和/或篡改的风险(在您的情况下可能很小),请使用回调。它回调的原因是允许应用程序控制。

s_client 在第一次数据传输之前使用SSL_set_connect_state 引起握手,而不是显式调用SSL_connect,这一事实无关紧要,没有任何区别。

添加:您可以在使用回调后检测到错误——甚至不用!

首先要明确的是,我们这里所说的回调('verify'回调)是在内置链验证逻辑之外使用的。有一个 不同 回调,名称非常相似,即“证书验证”回调,这是您不想要的。引用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 函数修改其行为就足够了。

这里的“verify_callback 函数”是指set_verify 而不是set_verify_callback。实际上这并不准确。 part 始终使用内置逻辑,只有 part 的一部分被 cert verify 回调替换。但是您仍然不想这样做,只有验证回调。

SSL_[CTX_]set_verify[_depth] 页面继续描述内置逻辑:

SSL_CTX_set_verify_depth() 和 SSL_set_verify_depth() 设置在验证过程中使用链中深度证书的上限。 [...]

从最深的嵌套级别(根 CA 证书)开始检查证书链,并向上工作到对等方的证书。在每个级别检查签名和颁发者属性。每当发现验证错误时,错误号都会存储在 x509_ctx 中,并在 preverify_ok=0 时调用 verify_callback。 [...] 如果未发现证书错误,则在进入下一个级别之前调用 verify_callback 并使用 preverify_ok=1。

verify_callback 的返回值控制着进一步验证过程的策略。如果 verify_callback 返回 0,则验证过程立即以“验证失败”状态停止。 [,,,] 如果 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 使用,但这种情况相对较少)打印每个证书的主题名称和状态(标记为“验证返回”)来自内置逻辑,但始终返回 1,从而强制(其余的验证和)连接继续,无论发现任何错误。

注意第二段“每当发现验证错误时,错误号存储在 x509_ctx 和 [then] verify_callback 调用 preverify_ok=0”和第三段“调用进程可以检索错误代码使用 SSL_get_verify_result 的最后一次验证错误”——即使回调强制 ok=1 也是如此。

但是在检查参考资料时,我发现了一个 更好的解决方案,我在那个页面上错过了:如果你只是默认(或设置)mode SSL_VERIFY_NONE (添加了强调和说明):

客户端模式:如果不使用匿名密码(默认禁用),服务器将发送一个 将被检查的证书 [通过内置逻辑,即使 NONE 会让你认为它是'未检查]。在 TLS/SSL 握手 [完成] 后,可以使用 SSL_get_verify_result 函数检查证书验证过程的结果。无论验证结果如何,都会继续握手。

这实际上与强制 ok=1 并执行您想要的操作的回调相同。

【讨论】:

  • 有什么方法可以让我从自定义回调中访问默认验证逻辑吗?正如我的问题的第一个脚注中所指出的,我正在研究无效证书的使用,因此虽然连接的安全性对我来说并不是特别重要,但重要的是我不会检测到无效证书。我认为最适合我的目的是运行默认回调的自定义回调,将结果存储在某处,然后返回 1。
【解决方案2】:

可以使用SSL_CTX_set_cert_verify_callback(ctx, own_cert_verify_callback, &amp;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 以便以后使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-13
    • 2015-09-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多