Skip to content

Instantly share code, notes, and snippets.

@jay
Last active December 22, 2015 04:14
Show Gist options
  • Save jay/bd785baaafa5a960ba9b to your computer and use it in GitHub Desktop.
Save jay/bd785baaafa5a960ba9b to your computer and use it in GitHub Desktop.
Use libcurl to retrieve URL via HTTP2 using a reordered cipher list.
/* Use libcurl to retrieve URL via HTTP2 using a reordered cipher list.
Usage: ReorderCipherList <url>
This program deprioritizes HTTP/2 blacklisted ciphers in the client cipher
list and then uses that reordered list in its client handshake with the server.
Note that according to HTTP/2 RFC 7540 the blacklist enforcement is described
using the RFC semantic 'MAY' which is the same as 'OPTIONAL'. In this code I
refer to the ciphers as just blacklisted or banned.
The reason banned ciphers are not removed entirely is because we don't know
whether or not a server will select HTTP/2 or if it enforces the ban. Unless
both of those things are true a server 'MAY' select a banned cipher.
libcurl uses the HTTP protocol version selected by the server. Therefore even
though we prefer HTTP/2 if it is not selected by the server then it isn't used.
Link to OpenSSL 1.0.2+ is required. BoringSSL or LibreSSL may work.
Define VERBOSE to see a dump of the cipher list before and after the reorder.
The format is CIPHER-NAME (hex id) (OK/BANNED). 'OK' only means a cipher is not
on the HTTP/2 blacklist, it does not mean the cipher is actually valid for the
SSL/TLS protocol requested by libcurl and will appear in the ClientHello.
curl issue #406:
'Use HTTP2-compatible cipher as first preference'
https://github.com/bagder/curl/pull/406
Copyright (C) 2015 Jay Satiro <[email protected]>
http://curl.haxx.se/docs/copyright.html
https://gist.github.com/jay/bd785baaafa5a960ba9b
*/
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* http://curl.haxx.se/download.html */
#include <curl/curl.h>
/* https://www.openssl.org/source/ */
#include <openssl/ssl.h>
#undef FALSE
#define FALSE 0
#undef TRUE
#define TRUE 1
/*
Check if a cipher id is in the TLS 1.2 cipher suite blacklist from RFC 7540.
https://tools.ietf.org/html/rfc7540#appendix-A
This macro is method #2 from https://github.com/jay/http2_blacklisted_ciphers
*/
#define IS_CIPHER_BANNED_FROM_HTTP2(id) ( \
(0x0000 <= id && id <= 0x00FF && \
"\xFF\xFF\xFF\xCF\xFF\xFF\xFF\xFF\x7F\x00\x00\x00\x80\x3F\x00\x00" \
"\xF0\xFF\xFF\x3F\xF3\xF3\xFF\xFF\x3F\x00\x00\x00\x00\x00\x00\x80" \
[(id & 0xFF) / 8] & (1 << (id % 8))) || \
(0xC000 <= id && id <= 0xC0FF && \
"\xFE\xFF\xFF\xFF\xFF\x67\xFE\xFF\xFF\xFF\x33\xCF\xFC\xCF\xFF\xCF" \
"\x3C\xF3\xFC\x3F\x33\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
[(id & 0xFF) / 8] & (1 << (id % 8))) \
)
/*
Dump a list of ciphers.
See ReorderCipherList to understand how this works.
*/
#ifdef VERBOSE
void DumpCipherList(STACK_OF(SSL_CIPHER) *st)
{
size_t i;
size_t total = (size_t)sk_SSL_CIPHER_num(st);
if(!total || total == (size_t)-1)
return;
for(i = 0; i < total; ++i) {
int id;
const char *name, *status;
SSL_CIPHER *cipher = sk_SSL_CIPHER_value(st, i);
if(!cipher)
continue;
id = (int)(cipher->id & 0xFFFFFF);
name = cipher->name ? cipher->name : "(null)";
status = IS_CIPHER_BANNED_FROM_HTTP2(id) ? "BANNED" : "OK";
fprintf(stderr, "%s (0x%X) (%s)\n", name, id, status);
}
return;
}
#endif
/*
Reorder the cipher list to deprioritize any HTTP/2 blacklisted ciphers.
This function is meant to be called after any cipher sorting done by OpenSSL
functions such as SSL_CTX_set_cipher_list.
TRUE on success, FALSE on failure
*/
int ReorderCipherList(SSL_CTX *ctx)
{
size_t i, modified, total;
int retcode = FALSE;
STACK_OF(SSL_CIPHER) *st, *duped_st = NULL;
if(!ctx || !ctx->cipher_list)
goto cleanup;
st = ctx->cipher_list;
/*
we can't use the stack's sort feature to deprioritize the blacklisted ciphers
because the sort isn't guaranteed to be a stable sort. also it could change
the value of st->sorted which we don't want to change. for example OpenSSL
1.0.2d shows st->sorted as 0 at this point; even though in libcurl we
specified to sort by strength and OpenSSL has done that for us before this
function is called. So the value of sorted has some meaning in OpenSSL that
is unknown to us, could change and could vary by fork.
instead we'll make a shallow copy of st in duped_st, and we'll work from
duped_st to reorder st in place. this is possibly safer than if we reordered
a copy of st and then changed ctx->cipher_list to point to that, in case
something somewhere else also has a pointer to the list.
*/
duped_st = sk_SSL_CIPHER_dup(st);
if(!duped_st)
goto cleanup;
/* the num of pointers is either a size_t or an int. */
total = (size_t)sk_SSL_CIPHER_num(duped_st);
if(total == (size_t)-1)
goto cleanup;
else if(total == 0) {
retcode = TRUE;
goto cleanup;
}
#ifdef VERBOSE
fprintf(stderr, "\nCipher list before reorder:\n");
DumpCipherList(st);
#endif
/* Copy pointers to ciphers that aren't banned */
for(modified = 0, i = 0; i < total; ++i) {
SSL_CIPHER *cipher = sk_SSL_CIPHER_value(duped_st, i);
/* OpenSSL/LibreSSL say cipher->id is 4 bytes and the first byte is
version, but actually the type is an unsigned long. BoringSSL cipher->id
is 4 bytes: the actual id OR'd with 0x03000000. */
if(!cipher || IS_CIPHER_BANNED_FROM_HTTP2((int)(cipher->id & 0xFFFFFF)))
continue;
sk_SSL_CIPHER_set(st, modified, cipher);
++modified;
sk_SSL_CIPHER_set(duped_st, i, NULL);
}
/* Copy pointers to ciphers that are banned */
for(i = 0; i < total; ++i) {
SSL_CIPHER *cipher = sk_SSL_CIPHER_value(duped_st, i);
if(!cipher)
continue;
sk_SSL_CIPHER_set(st, modified, cipher);
++modified;
}
/* Set any remaining pointers to NULL. This would only happen if there were
null pointers in the stack when it was duped. */
while(modified < total) {
sk_SSL_CIPHER_set(st, modified, NULL);
++modified;
}
#ifdef VERBOSE
fprintf(stderr, "\nCipher list after reorder:\n");
DumpCipherList(st);
#endif
retcode = TRUE;
cleanup:
sk_SSL_CIPHER_free(duped_st);
return retcode;
}
CURLcode ssl_ctx_callback(CURL *curl, void *ssl_ctx, void *userptr)
{
(void)curl;
(void)userptr;
return (ReorderCipherList((SSL_CTX *)ssl_ctx) ? CURLE_OK : CURLE_SSL_CIPHER);
}
/*
Retrieve URL via HTTP2 using a reordered cipher list.
TRUE on success, FALSE on failure
*/
int retrieve(const char *url)
{
CURL *curl = NULL;
CURLcode res = CURLE_FAILED_INIT;
int retcode = FALSE;
char errbuf[CURL_ERROR_SIZE] = { 0, };
curl = curl_easy_init();
if(!curl) {
fprintf(stderr, "Error: curl_easy_init failed.\n");
goto cleanup;
}
/*
CURLOPT_CAINFO
To verify SSL sites you may need to load a bundle of certificates.
You can download the default bundle here:
https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt
However your SSL backend might use a database in addition to or instead of
the bundle:
http://curl.haxx.se/docs/ssl-compared.html
*/
curl_easy_setopt(curl, CURLOPT_CAINFO, "curl-ca-bundle.crt");
if(curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, ssl_ctx_callback)) {
fprintf(stderr, "Error: libcurl build doesn't support SSL_CTX callback\n");
goto cleanup;
}
if(curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0)) {
fprintf(stderr, "Error: libcurl build doesn't support HTTP2\n");
goto cleanup;
}
#ifdef VERBOSE
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
#endif
curl_easy_setopt(curl, CURLOPT_URL, url);
res = curl_easy_perform(curl);
if(res != CURLE_OK) {
size_t len = strlen(errbuf);
fprintf(stderr, "\nlibcurl: (%d) ", res);
if(len)
fprintf(stderr, "%s%s", errbuf, ((errbuf[len - 1] != '\n') ? "\n" : ""));
fprintf(stderr, "%s\n\n", curl_easy_strerror(res));
goto cleanup;
}
retcode = TRUE;
cleanup:
curl_easy_cleanup(curl);
return retcode;
}
int main(int argc, char *argv[])
{
curl_version_info_data *curlver = NULL;
if(argc != 2) {
fprintf(stderr, "Usage: ReorderCipherList <url>\n"
"Retrieve URL via HTTP2 using a reordered cipher list.\n");
return EXIT_FAILURE;
}
if(curl_global_init(CURL_GLOBAL_ALL)) {
fprintf(stderr, "Fatal: The initialization of libcurl has failed.\n");
return EXIT_FAILURE;
}
if(atexit(curl_global_cleanup)) {
fprintf(stderr, "Fatal: atexit failed to register curl_global_cleanup.\n");
curl_global_cleanup();
return EXIT_FAILURE;
}
curlver = curl_version_info(CURLVERSION_NOW);
if(!(curlver->features & CURL_VERSION_SSL) || !curlver->ssl_version ||
(strncmp("OpenSSL", curlver->ssl_version, 7) &&
strncmp("BoringSSL", curlver->ssl_version, 9) &&
strncmp("LibreSSL", curlver->ssl_version, 8))) {
fprintf(stderr, "Fatal: libcurl is not using "
"OpenSSL, BoringSSL or LibreSSL.\n"
"Found SSL?:\t%s\n"
"SSL Version:\t%s\n",
(curlver->features & CURL_VERSION_SSL) ? "Yes" : "No",
curlver->ssl_version ? curlver->ssl_version : "(none)");
return EXIT_FAILURE;
}
if(!retrieve(argv[1])) {
fprintf(stderr, "Fatal: ReorderCipherList failed.\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment