0

I'm essentially trying to translate the openssl s_client commands here into C++ code which calls into the OpenSSL C library to retrieve a server's SSL cert.

So far, I think I have the non-SNI command

openssl s_client -showcerts -connect 1.2.3.4:999 </dev/null

more or less translated over with the following block of code

using X509_ptr = std::unique_ptr<X509, decltype(&::X509_free)>;
using BIO_chain_ptr = std::unique_ptr<BIO, decltype(&::BIO_free_all)>;
using SSL_CTX_ptr = std::unique_ptr<SSL_CTX, decltype(&::SSL_CTX_free)>;
using BIO_ADDR_ptr = std::unique_ptr<BIO_ADDR, decltype(&::BIO_ADDR_free)>;

std::string bio_to_str(BIO_ptr bio) {
    BUF_MEM* mem = NULL;
    BIO_get_mem_ptr(bio.get(), &mem);
    auto err = ERR_get_error();

    if (!mem || !mem->data || !mem->length)
    {
        cerr << "BIO_get_mem_ptr failed, error " << err << ", ";
        cerr << std::hex << "0x" << err;
        exit(2);
    }

    return std::string(mem->data, mem->length);
}

int GetServerCerts(
    const std::string& address, unsigned short port, const std::string& subject,
    std::string& outstr
) {
    // Code based largely off of
    // https://opensource.com/article/19/6/cryptography-basics-openssl-part-1
    int code;

    const SSL_METHOD* method = TLSv1_2_client_method();
    SSL_CTX_ptr ctx(SSL_CTX_new(method), SSL_CTX_free);
    BIO_chain_ptr bio(BIO_new_ssl_connect(ctx.get()), BIO_free_all);

    // The raw pointer is used here because the SSL* seems to be part of the bio
    // chain and is hence freed by BIO_free_all? Having it floating around at all 
    // seems dangerous though
    SSL* raw_ssl;
    BIO_get_ssl(bio.get(), &raw_ssl);
    SSL_set_mode(raw_ssl, SSL_MODE_AUTO_RETRY);
    BIO_set_conn_hostname(bio.get(), (address + ":" + std::to_string(port)).c_str());

    /* BEGIN BROKEN CODE
    unsigned char thing[] = { 0, 0, 0, 0 };
    inet_pton(AF_INET, address.c_str(), thing);
    BIO_ADDR_ptr bio_addr(BIO_ADDR_new(), BIO_ADDR_free);
    if (bio_addr.get() == NULL) { std::cout << "Failed to create BIO addr" << std::endl; return 1; }
    code = BIO_ADDR_rawmake(
        bio_addr.get(), AF_INET, 
        thing, 4,
        port
    );
    if (!code) { std::cout << "Failed to populate BIO addr" << std::endl; return 1; }
    code = BIO_set_conn_address(bio.get(), bio_addr.get());
    if (!code) { std::cout << "Failed to associate BIO addr" << std::endl; return 1; }
    */ // END BROKEN CODE

    std::cout << "Trying to make connection" << std::endl;
    auto conn_code = BIO_do_connect(bio.get());
    if (conn_code != 1) {
        std::cout << "Error creating connection from BIO: code " << conn_code << std::endl;
    }

    X509_ptr x509(SSL_get_peer_certificate(raw_ssl), X509_free);
    BIO_ptr cert_bio(BIO_new(BIO_s_mem()), BIO_free);
    int rc = PEM_write_bio_X509(cert_bio.get(), x509.get());
    int err = ERR_get_error();
    if (rc != 1) {
        cerr << "Writing X509 to BIO failed, error " << err << ", ";
        cerr << std::hex << "0x" << err;
        return 1;
    }
    outstr = bio_to_str(std::move(cert_bio));
    return 0;
}

afaik this version works, but you might notice that it totally ignores the subject parameter. I'd like to take that into account as well, ie translate the SNI command

openssl s_client -showcerts -servername subject -connect 1.2.3.4:999 </dev/null

You can see my attempt to do so in the commented broken code block: the problem when I uncomment that block and change the BIO_set_conn_hostname to BIO_set_conn_hostname(subject.c_str()), the call to BIO_do_connect will just block for a few seconds and then return an error code.

Two questions:

  1. What am I doing wrong here?

  2. Do I even need the SNI function to get the cert corresponding to "subjectname"? Perhaps that just acts as a filter on the certs the server would send over anyways? My understanding here is limited

General comments on the program (safety, improvements on those using statements, whether 0 should signal success or failure, etc) are also welcome

0 Answers0