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:
What am I doing wrong here?
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