Last active
December 22, 2015 04:14
-
-
Save jay/bd785baaafa5a960ba9b to your computer and use it in GitHub Desktop.
Use libcurl to retrieve URL via HTTP2 using a reordered cipher list.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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