Achieving a Perfect SSL Labs Score with C(++)

A good article making the rounds details how to achieve a perfect SSL Labs Score with Go. In the related discussion (also on reddit) many noted that such a pursuit was impractical: if you’re causing connectivity issues for some of your users, achieving minor improvements in theoretical security might be Pyrrhic.

A perfect score is not a productive pursuit for most public web properties, and an A+ with a couple of 90s is perfectly adequate and very robustly secure for most scenarios.

Striving for 100 across the board is nonetheless an interesting, educational exercise. The Qualys people have done a remarkable job educating and informing, increasing the prevalence of best practice configurations, improving the average across the industry. It’s worth understanding the nuances of such an exercise even if not practically applicable for all situations.

It’s also worth considering that not all web endpoints are publicly consumable, and there are scenarios where cutting off less secure clients is an entirely rational choice. If your industrial endpoint is called from your industrial management process, it really doesn’t matter whether Android 2.2 or IE 6 users are incompatible.

score

So here’s how to create a trivial implementation of a perfect score HTTPS endpoint in C(++). It’s more wordy than the Go variant, though it’s a trivial exercise to parameterize and componentize for easy reuse. And as anyone who visits here regularly knows, in no universe am I advocating creating HTTPS endpoints in C++: I’m a big fan and (ab)user of Go, C#, Java, and various other languages and platforms, but it’s nice to have the options available when appropriate.

This was all done on a Ubuntu 16.04 machine with the typical build tools installed (e.g. make, git, build-essential, autoconf), though of course you could do it on most Linux variants, OSX, Ubuntu on Windows, etc. This exercise presumes that you have certificates available at /etc/letsencrypt/live/example.com/

(where example.com is replaced with your domain. Replace in code as appropriate, or make arguments)

Note that if you use the default letsencrypt certificates, which are currently 2048 bits, the SSL Test will still yield an A+ from the below code however it will yield a slightly imperfect score, with only a score of 90 for the key exchange. In practice a 2048-bit cert is considered more than adequate, so whether you sweat this and update to a 4096-bit cert is up to you (as mentioned in the Go entry, you can obtain a 4096-bit cert via the lego Go app, using the

--key-type "rsa4096"

argument).

1 – Install openssl and the openssl development library.

sudo apt-get update && sudo apt-get install openssl libssl-dev

2 – Create a DH param file. This is used by the OpenSSL for the DH key exchange.

sudo openssl dhparam -out /etc/letsencrypt/live/example.com/dh_param_2048.pem 2048

3 – Download, make, install the libevent v2.1.5 “beta”. Install as root and refresh the library cache (e.g. sudo ldconfig).

https://github.com/libevent/libevent/releases/tag/release-2.1.5-beta

4 – Start a new C++ application linked to libcrypto, libevent, libevent_openssl, libevent_pthreads and libssl.

5 – Add the necessary includes-

#include <iostream>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/stack.h>

#include <event.h>
#include <event2/listener.h>
#include <event2/bufferevent_ssl.h>
#include <evhttp.h>

6 – Initialize the SSL context-

SSL_CTX *
ssl_init(void) {
    SSL_CTX *server_ctx;

    SSL_load_error_strings();
    SSL_library_init();

    if (!RAND_poll())
        return nullptr;

    server_ctx = SSL_CTX_new(SSLv23_server_method());

    // Load our certificates
    if (!SSL_CTX_use_certificate_chain_file(server_ctx, "/etc/letsencrypt/live/example.com/fullchain.pem") ||
            !SSL_CTX_use_PrivateKey_file(server_ctx, "/etc/letsencrypt/live/example.com/privkey.pem", SSL_FILETYPE_PEM)) {
        std::cerr << "Couldn't read chain or private key" << std::endl;
        return nullptr;
    }

    // prepare the PFS context
    EC_KEY *ecdh = EC_KEY_new_by_curve_name(NID_secp384r1);
    if (!ecdh) return nullptr;
    
    if (SSL_CTX_set_tmp_ecdh(server_ctx, ecdh) != 1) {
        return nullptr;
    }

    bool pfsEnabled = false;
    FILE *paramFile = fopen("/etc/letsencrypt/live/example.com/dh_param_2048.pem", "r");
    if (paramFile) {
        DH *dh2048 = PEM_read_DHparams(paramFile, NULL, NULL, NULL);
        if (dh2048 != NULL) {
            if (SSL_CTX_set_tmp_dh(server_ctx, dh2048) == 1) {
                pfsEnabled = true;
            }
        }
        fclose(paramFile);
    }

    if (!pfsEnabled) {
        std::cerr << "Couldn't enable PFS. Validate DH Param file." << std::endl;
        return nullptr;
    }
    
    SSL_CTX_set_options(server_ctx,
            SSL_OP_SINGLE_DH_USE |
            SSL_OP_SINGLE_ECDH_USE |
            SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1);

    if (SSL_CTX_set_cipher_list(server_ctx, "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:AES256:!DHE:!RSA:!AES128:!RC4:!DES:!3DES:!DSS:!SRP:!PSK:!EXP:!MD5:!LOW:!aNULL:!eNULL") != 1) {
        std::cerr << "Cipher list could not be initialized." << std::endl;
        return nullptr;
    }

    return server_ctx;
}

The most notable aspects are the setup of PFS, including a strong, 384-bit elliptic curve. Additionally, deprecated transport options are disabled (in this case anything under TLSv1.2), as are weak ciphers.

ciphers

7 – Prepare a libevent callback that attaches a new SSL connection to each libevent connection-

struct bufferevent* initializeConnectionSSL(struct event_base *base, void *arg) {
    return bufferevent_openssl_socket_new(base,
            -1,
            SSL_new((SSL_CTX *)arg),
            BUFFEREVENT_SSL_ACCEPTING,
            BEV_OPT_CLOSE_ON_FREE);
}

8 – Hook it all together-

int main(int argc, char** argv) {
    SSL_CTX *ctx;
    ctx = ssl_init();
    if (ctx == nullptr) {
        std::cerr << "Failed to initialize SSL. Check certificate files." << std::endl;
        return EXIT_FAILURE;
    }

    if (!event_init()) {
        std::cerr << "Failed to init libevent." << std::endl; 
        return EXIT_FAILURE; 
    } 
    auto base = event_base_new(); 
    auto https = evhttp_new(base); 

    void (*requestHandler)(evhttp_request *req, void *) = [] (evhttp_request *req, void *) { 
         auto *outBuf = evhttp_request_get_output_buffer(req); 
         if (!outBuf) return; 
         switch (req->type) {
            case EVHTTP_REQ_GET:
                {
                    auto headers = evhttp_request_get_output_headers(req);
                    evhttp_add_header(headers, "Strict-Transport-Security", "max-age=63072000; includeSubDomains");
                    evbuffer_add_printf(outBuf, "<html><body><center><h1>Request for - %s</h1></center></body></html>", req->uri);
                    evhttp_send_reply(req, HTTP_OK, "", outBuf);
                }
                break;
            default:
                evhttp_send_reply(req, HTTP_BADMETHOD, "", nullptr);
                break;
        }
    };

    // add the callbacks
    evhttp_set_bevcb(https, initializeConnectionSSL, ctx);
    evhttp_set_gencb(https, requestHandler, nullptr);
    auto https_handle = evhttp_bind_socket_with_handle(https, "0.0.0.0", 443);

    event_base_dispatch(base);

    if (event_dispatch() == -1) {
        std::cerr << "Failed to run message loop." << std::endl;
        return EXIT_FAILURE;
    }

    return 0;
}

Should you strive for 100? Maybe not. Should you even have SSL termination in your C(++) apps?  Maybe not (terminate with something like nginx and you can take advantage of all of the modules available, including compression, rate limiting, easy resource ACLs, etc). But it is a tool at your disposal if the situation is appropriate. And of course the above is quickly hacked together, non-production ready sample code (with some small changes it can be made more scalable, achieving enormous performance levels on commodity servers), so use at your own risk.

Just another fun exercise. The lightweight version of this page can be found at https://dennisforbes.ca/index.php/2016/05/23/achieving-a-perfect-ssl-labs-score-with-c/amp/, per “Hanging Chads / New Projects / AMPlified“.

Note that this is not the promised “Adding Secure, Authenticated HTTPS Interop to a C(++) Project” piece that is still in work.  That undertaking is more involved with secure authentication and authorization, custom certificate authorities, and client certificates.