Get certificate information after connection error

偶尔善良 提交于 2021-02-16 19:46:20

问题


I'm writing a simple SSL client using the OpenSSL library. I'd like to be able to print the certificate chain presented by the server after the connection completes. When the connection completes successfully, this isn't a problem. However, if the connection fails for some reason, I'm unable to get the failing certificate the server presented. Here's a SSCCE that demonstrates this.

#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;
}

(Fastest way to compile this is gcc sscce.c -lcrypto -lssl -o sscce).

Whenever SSL_connect(ssl) returns 1 (whenever the connection is successful), print_certificates(ssl) works as expected. However, if SSL_connect(ssl) returns anything other than 1 (the connection failed), print_certificates(ssl) does not print anything, because SSL_get_peer_cert_chain(ssl) returns null. While this is a logical behavior for servers that don't present a certificate, on servers that present an invalid certificate, not having access to the certificate makes it difficult to debug server configuration issues.

Interestingly, SSL_get_verify_result(ssl) returns the correct error code when the connection fails, despite the fact that I can't get the certificate chain myself.* I've looked through the OpenSSL codebase a little bit to try and figure out why this is, and while I'm still struggling to understand how everything fits together, it looks like there's some code in the ssl_verify_cert_chain function that frees the certificates after performing the error checking. I'm guessing that in the example code above, SSL_connect, once it has the complete certificate chain, runs some built-in validation code that frees the certificate before it gets to print_certificates. This confuses me, because I don't understand why the certificates would be freed when validation fails, but not when it succeeds. Perhaps someone who knows more about OpenSSL's internal behavior can shed some light on this.

I've noted that the stock s_client provided by the openssl utility, when run with the showcerts option (openssl s_client -showcerts -connect expired.badssl.com:443) does not exhibit this behavior. Regardless of whether the connection succeeds or fails, the certificates are printed. The print_certificates function in my SSCCE is just a modified version of the s_client cert printing code, but s_client doesn't use SSL_connect, so it's unsurprising that it exhibits different behavior. I note that s_client sets a custom certificate verify callback (defined here), but I'm reluctant to use anything but the default** verify function.

tl;dr SSL_get_peer_cert_chain(ssl) returns null if the server presents an invalid certificate chain. How can I get around this to print the failing certificate chain?

Edit: I've confirmed that this problem still occurs when I set the BIO state to blocking, and also, (for what it's worth), when the above code is compiled using LibreSSL.

Edit 2: I found that creating a function that just returns 1 and passing that to SSL_CTX_set_verify as the callback function (instead of NULL) causes SSL_get_peer_cert_chain(ssl) to return the certificate chain as expected, despite invalid certs in the chain. However, I'm reluctant to call this a solution to the problem, since I pretty clearly must be overriding one of OpenSSL's built-in functions here.

* - The obvious response to this is that since OpenSSL tells me why the connection failed, I don't need to access the raw cert in order to debug my issues. In any other situation, this would be true, but since I'm using this client as part of a research project involving invalid certificate use on the internet, I need to be able to save the failing certificates to a file.

** - As far as I can tell, passing NULL as the verify_callback parameter of SSL_CTX_set_verify tells OpenSSL to use a builtin default function for certificate validation. The documentation isn't tremendously clear on this.


回答1:


TLDR: use the callback.

All session parameters including the cert chain are available only if the connection (handshake) succeeded; this is because if the handshake did not succeed there is no way to have confidence any of the results are valid. In theory the received certs could be a special case, but a special case would be more complicated and as you may have noticed the OpenSSL API is already complicated enough people routinely screw up using it.

As you saw s_client sets a verify callback that forces acceptance of any cert chain, even an invalid one; this causes the handshake to succeed and parameters including the cert chain to be available. s_client is intended as a test tool where it doesn't matter if the data is really secure or not.

If you want to connect only to verified servers, use the default verify logic. If you want to connect to unverified servers and handle risk (possibly minimal in your case) of data being intercepted and/or tampered, use the callback. The reason it is a callback is to allow applications control.

The fact s_client uses SSL_set_connect_state to cause handshake before the first data transfer, rather than explicitly calling SSL_connect, is irrelevant and makes no difference.

ADDED: you can detect an error after using the callback -- or even without!

First to be clear, the callback we are talking about here (the 'verify' callback) is used in addition to the builtin chain verification logic. There is a different callback, with a very similar name, the 'cert verify' callback, which you do NOT want. Quoting the man page

The actual verification procedure is performed either using the built-in verification procedure or using another application provided verification function set with SSL_CTX_set_cert_verify_callback. The following descriptions apply in the case of the built-in procedure. An application provided procedure also has access to the verify depth information and the verify_callback() function, but the way this information is used may be different.

As the (conveniently hyperlinked) SSL_[CTX_]set_cert_verify_callback man page says

[by default] the built-in verification function is used. If a verification callback callback [that's clearly a typo] is specified via SSL_CTX_set_cert_verify_callback(), the supplied callback function is called instead. [...]

Providing a complete verification procedure including certificate purpose settings etc is a complex task. The built-in procedure is quite powerful and in most cases it should be sufficient to modify its behaviour using the verify_callback function.

Where 'the verify_callback function' means the set_verify one not the set_verify_callback one. Actually this is not exact; part of the builtin logic is always used and only part of it is replaced by the cert verify callback. But you still don't want to do that, only the verify callback.

The SSL_[CTX_]set_verify[_depth] page continues to describe the builtin logic:

SSL_CTX_set_verify_depth() and SSL_set_verify_depth() set the limit up to which depth certificates in a chain are used during the verification procedure. [...]

The certificate chain is checked starting with the deepest nesting level (the root CA certificate) and worked upward to the peer's certificate. At each level signatures and issuer attributes are checked. Whenever a verification error is found, the error number is stored in x509_ctx and verify_callback is called with preverify_ok=0. [...] If no error is found for a certificate, verify_callback is called with preverify_ok=1 before advancing to the next level.

The return value of verify_callback controls the strategy of the further verification process. If verify_callback returns 0, the verification process is immediately stopped with "verification failed" state. [,,,] If verify_callback returns 1, the verification process is continued. If verify_callback always returns 1, the TLS/SSL handshake will not be terminated with respect to verification failures and the connection will be established. The calling process can however retrieve the error code of the last verification error using SSL_get_verify_result or by maintaining its own error storage managed by verify_callback.

If no verify_callback is specified, the default callback will be used. Its return value is identical to preverify_ok, so that any verification failure will lead to a termination of the TLS/SSL handshake with an alert message, if SSL_VERIFY_PEER is set.

So a callback has the option to force acceptance after the builtin logic has found an error (call with 0, return 1) or force failure even if the builtin logic considers the cert chain okay (call with 1, return 0) but if it doesn't the builtin logic controls. The special callback used by s_client (and by s_server when client authentication is used, but that is relatively rare) prints the subject name of each cert with the status (labelled 'verify return') from the builtin logic, but always returns 1 thereby forcing the (rest of the verification and) connection to continue regardless of any error found.

Notice in the second paragraph "Whenever a verification error is found, the error number is stored in x509_ctx and [then] verify_callback is called with preverify_ok=0" and in the third paragraph "The calling process can however retrieve the error code of the last verification error using SSL_get_verify_result" -- this is true even if the callback forced ok=1.

But in checking the references for this I found an even better solution I had missed right on that page: if you just default (or set) mode SSL_VERIFY_NONE (emphasis and clarifications added):

Client mode: if not using an anonymous cipher (by default disabled), the server will send a certificate which will be checked [by the builtin logic, even though NONE would make you think it isn't checked]. The result of the certificate verification process can be checked after the TLS/SSL handshake [is completed] using the SSL_get_verify_result function. The handshake will be continued regardless of the verification result.

This is effectively the same as a callback that forces ok=1 and does what you want.




回答2:


It's possible to use SSL_CTX_set_cert_verify_callback(ctx, own_cert_verify_callback, &cert) to use own verification callback with given pointer to STACK_OF(X509)*. Callback then looks like this:

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;
}

So default function for certificate verification is used and when certificate is invalid, shallow copy is placed to the arg so it can be used later.



来源:https://stackoverflow.com/questions/38705628/get-certificate-information-after-connection-error

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!