From f3d46818d7af759d90e8147554aaaf05cb0d75f6 Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Wed, 19 Nov 2014 13:23:21 -0500 Subject: [PATCH] SSL: Add PEM format support for public key pinning --- docs/curl.1 | 4 +- docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 | 2 +- lib/vtls/vtls.c | 108 +++++++++++++++++++++++++--- lib/vtls/vtls.h | 7 ++ src/tool_help.c | 3 +- tests/certs/Server-localhost-sv.pub.pem | 6 ++ tests/certs/Server-localhost.nn-sv.pub.pem | 6 ++ tests/certs/Server-localhost0h-sv.pub.pem | 6 ++ tests/certs/scripts/genserv.sh | 3 + tests/data/Makefile.inc | 2 +- tests/data/test2034 | 2 +- tests/data/test2035 | 2 +- tests/data/test2037 | 58 +++++++++++++++ tests/data/test2038 | 44 ++++++++++++ 14 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 tests/certs/Server-localhost-sv.pub.pem create mode 100644 tests/certs/Server-localhost.nn-sv.pub.pem create mode 100644 tests/certs/Server-localhost0h-sv.pub.pem create mode 100644 tests/data/test2037 create mode 100644 tests/data/test2038 diff --git a/docs/curl.1 b/docs/curl.1 index 5f88cff..7d91464 100644 --- a/docs/curl.1 +++ b/docs/curl.1 @@ -539,14 +539,14 @@ If this option is set, the default capath value will be ignored, and if it is used several times, the last one will be used. .IP "--pinnedpubkey " (SSL) Tells curl to use the specified public key file to verify the peer. The -file must contain a single public key in DER format. +file must contain a single public key in PEM or DER format. When negotiating a TLS or SSL connection, the server sends a certificate indicating its identity. A public key is extracted from this certificate and if it does not exactly match the public key provided to this option, curl will abort the connection before sending or receiving any data. -This is currently only implemented in the OpenSSL and GnuTLS backends. +This is currently only implemented in the OpenSSL, GnuTLS and GSKit backends. If this option is used several times, the last one will be used. (Added in 7.39.0) diff --git a/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 b/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 index d7c6932..2d86392 100644 --- a/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 +++ b/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 @@ -29,7 +29,7 @@ CURLOPT_PINNEDPUBLICKEY \- set pinned public key CURLcode curl_easy_setopt(CURL *handle, CURLOPT_PINNEDPUBLICKEY, char *pinnedpubkey); .SH DESCRIPTION Pass a pointer to a zero terminated string as parameter. The string should be -the file name of your pinned public key. The format expected is "DER". +the file name of your pinned public key. The format expected is "PEM" or "DER". When negotiating a TLS or SSL connection, the server sends a certificate indicating its identity. A public key is extracted from this certificate and diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c index 1d1c62e..a1dc241 100644 --- a/lib/vtls/vtls.c +++ b/lib/vtls/vtls.c @@ -69,6 +69,7 @@ #include "timeval.h" #include "curl_md5.h" #include "warnless.h" +#include "curl_base64.h" #define _MPRINTF_REPLACE /* use our functions only */ #include @@ -684,6 +685,64 @@ int Curl_ssl_random(struct SessionHandle *data, } /* + * Generic pem to der conversion + */ + +CURLcode Curl_pem_to_der(const char *pem, + unsigned char **der, size_t *der_len) +{ + char *stripped_pem = NULL, *begin_pos, *end_pos; + size_t pem_count = 0, stripped_pem_count = 0, pem_len = 0; + CURLcode result = CURLE_BAD_CONTENT_ENCODING; + + /* if pem is NULL or has a length of 0, exit */ + if(NULL == pem || 0 == (pem_len = strlen(pem))) + return result; + + begin_pos = strstr(pem, "-----BEGIN PUBLIC KEY-----"); + if(!begin_pos) + return CURLE_BAD_CONTENT_ENCODING; + + pem_count = begin_pos - pem; + /* Invalid if not at beginning AND not directly following \n */ + if(0 != pem_count && '\n' != pem[pem_count - 1]) + return CURLE_BAD_CONTENT_ENCODING; + + /* 26 is length of "-----BEGIN PUBLIC KEY-----" */ + pem_count += 26; + + /* Invalid if not directly following \n */ + end_pos = strstr(pem + pem_count, "\n-----END PUBLIC KEY-----"); + if(!end_pos) + return CURLE_BAD_CONTENT_ENCODING; + + pem_len = end_pos - pem; + + stripped_pem = malloc(pem_len - pem_count); + if(NULL == stripped_pem) + return CURLE_OUT_OF_MEMORY; + + /* + * Here we loop through the pem array one character at a time between the + * correct indices, and place each character that is not '\n' or '\r' + * into the stripped_pem array, which should represent the raw base64 string + */ + while(pem_count < pem_len) { + if('\n' != pem[pem_count] && '\r' != pem[pem_count]) + stripped_pem[stripped_pem_count++] = pem[pem_count]; + ++pem_count; + } + /* Place the null terminator in the correct place */ + stripped_pem[stripped_pem_count] = '\0'; + + result = Curl_base64_decode(stripped_pem, der, der_len); + + Curl_safefree(stripped_pem); + + return result; +} + +/* * Generic pinned public key check. */ @@ -691,8 +750,9 @@ CURLcode Curl_pin_peer_pubkey(const char *pinnedpubkey, const unsigned char *pubkey, size_t pubkeylen) { FILE *fp = NULL; - unsigned char *buf = NULL; - long size = 0; + unsigned char *buf = NULL, *pem_ptr = NULL; + size_t size = 0, pem_len = 0; + CURLcode pem_read = CURLE_OK; CURLcode result = CURLE_SSL_PINNEDPUBKEYNOTMATCH; /* if a path wasn't specified, don't pin */ @@ -708,32 +768,58 @@ CURLcode Curl_pin_peer_pubkey(const char *pinnedpubkey, /* Determine the file's size */ if(fseek(fp, 0, SEEK_END)) break; - size = ftell(fp); + size = (size_t)ftell(fp); if(fseek(fp, 0, SEEK_SET)) break; /* - * if the size of our certificate doesn't match the size of - * the file, they can't be the same, don't bother reading it + * if the size of our certificate is bigger than the file + * size than it can't match */ - if((long) pubkeylen != size) + if(pubkeylen > size || + (pubkeylen != size && size > MAX_PINNNED_PUBKEY_SIZE)) break; - /* Allocate buffer for the pinned key. */ - buf = malloc(pubkeylen); + /* + * Allocate buffer for the pinned key + * With 1 additional byte for null terminator in case of PEM key + */ + buf = malloc(size + 1); if(!buf) break; /* Returns number of elements read, which should be 1 */ - if((int) fread(buf, pubkeylen, 1, fp) != 1) + if((int) fread(buf, size, 1, fp) != 1) + break; + + /* If the sizes are the same, it can't be base64 encoded, must be der */ + if(pubkeylen == size) { + if(!memcmp(pubkey, buf, pubkeylen)) + result = CURLE_OK; + break; + } + + /* + * Otherwise we will assume it's PEM and try to decode it + * after placing null terminator + */ + buf[size] = '\0'; + pem_read = Curl_pem_to_der((const char *)buf, &pem_ptr, &pem_len); + /* if it wasn't read successfully, exit */ + if(CURLE_OK != pem_read) break; - /* The one good exit point */ - if(!memcmp(pubkey, buf, pubkeylen)) + /* + * if the size of our certificate doesn't match the size of + * the decoded file, they can't be the same, otherwise compare + */ + if(pubkeylen == pem_len && !memcmp(pubkey, pem_ptr, pubkeylen)) result = CURLE_OK; + } while(0); Curl_safefree(buf); + Curl_safefree(pem_ptr); fclose(fp); return result; diff --git a/lib/vtls/vtls.h b/lib/vtls/vtls.h index f0adc76..0f63244 100644 --- a/lib/vtls/vtls.h +++ b/lib/vtls/vtls.h @@ -33,6 +33,10 @@ #include "curl_schannel.h" /* Schannel SSPI version */ #include "curl_darwinssl.h" /* SecureTransport (Darwin) version */ +#ifndef MAX_PINNNED_PUBKEY_SIZE +#define MAX_PINNNED_PUBKEY_SIZE 1048576 /* ~1MB */ +#endif + #ifndef MD5_DIGEST_LENGTH #define MD5_DIGEST_LENGTH 16 /* fixed size */ #endif @@ -108,6 +112,9 @@ void Curl_ssl_md5sum(unsigned char *tmp, /* input */ size_t tmplen, unsigned char *md5sum, /* output */ size_t md5len); +/* Convert pem to der */ +CURLcode Curl_pem_to_der(const char *pem, + unsigned char **der, size_t *der_len); /* Check pinned public key. */ CURLcode Curl_pin_peer_pubkey(const char *pinnedpubkey, const unsigned char *pubkey, size_t pubkeylen); diff --git a/src/tool_help.c b/src/tool_help.c index 2c22b82..1b17981 100644 --- a/src/tool_help.c +++ b/src/tool_help.c @@ -152,7 +152,8 @@ static const char *const helptext[] = { " --oauth2-bearer TOKEN OAuth 2 Bearer Token (IMAP, POP3, SMTP)", " -o, --output FILE Write to FILE instead of stdout", " --pass PASS Pass phrase for the private key (SSL/SSH)", - " --pinnedpubkey FILE Public key (DER) to verify peer against (OpenSSL)", + " --pinnedpubkey FILE Public key (PEM/DER) to verify peer against " + "(OpenSSL/GnuTLS/GSKit only)", " --post301 " "Do not switch to GET after following a 301 redirect (H)", " --post302 " diff --git a/tests/certs/Server-localhost-sv.pub.pem b/tests/certs/Server-localhost-sv.pub.pem new file mode 100644 index 0000000..2384643 --- /dev/null +++ b/tests/certs/Server-localhost-sv.pub.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwJ3kmLLnk0YEKCdJ2/prhBWgB +s3J3lzjkYBxxnZn3JnshtW2qnxR2B2ykKi197vZviljEk97+oSUP/1dJwNmU2Qd5 +v4xt+vEYgmegP9cxA4LsuTlpB+zskxdbGnKRk7JrmGZj/mEp562GDgS6v4tVV2Gl +SvbK58bRuGVCq2dkFwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/certs/Server-localhost.nn-sv.pub.pem b/tests/certs/Server-localhost.nn-sv.pub.pem new file mode 100644 index 0000000..3131e95 --- /dev/null +++ b/tests/certs/Server-localhost.nn-sv.pub.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDT1E7bY1w/OjpeOAmU5k1wnQ2v +SeaCXQe39c2g369x8c+/1Zq9r3x4XVU/FL27LA5zndaCmtXm9iFdCJKicV+AX1zO +8MI3N3kPTT3U8oBtRzZF0dKLei4ScUtHhvWMma/nDs+1yU16dfeydAxB46u7LJ1v +VAgTWjrvfCf3PwsLcQIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/certs/Server-localhost0h-sv.pub.pem b/tests/certs/Server-localhost0h-sv.pub.pem new file mode 100644 index 0000000..c403ac5 --- /dev/null +++ b/tests/certs/Server-localhost0h-sv.pub.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMqZErIuiQK+VM3K5t2kzzMsyl +aGdaO7mGo5WIPuhjw+0AYBkDK11bVoraIV5xXNHj3lEYwRcUsTOQAFya5XMLqIic +0AtUvOo6Od32ZYFLKZlMcdP3aX+A6OhtYUGDh+usLL0P6xv9ojeXbTFWuktR3bEB +64n4Jd5bo+WyP0x3UwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/certs/scripts/genserv.sh b/tests/certs/scripts/genserv.sh index 463952c..80876ec 100755 --- a/tests/certs/scripts/genserv.sh +++ b/tests/certs/scripts/genserv.sh @@ -78,6 +78,9 @@ echo pseudo secrets generated echo "openssl rsa -in $PREFIX-sv.key -pubout -outform DER -out $PREFIX-sv.pub.der" $OPENSSL rsa -in $PREFIX-sv.key -pubout -outform DER -out $PREFIX-sv.pub.der +echo "openssl rsa -in $PREFIX-sv.key -pubout -outform PEM -out $PREFIX-sv.pub.pem" +$OPENSSL rsa -in $PREFIX-sv.key -pubout -outform PEM -out $PREFIX-sv.pub.pem + echo "openssl x509 -set_serial $SERIAL -extfile $PREFIX-sv.prm -days $DURATION -CA $CAPREFIX-ca.cacert -CAkey $CAPREFIX-ca.key -in $PREFIX-sv.csr -req -out $PREFIX-sv.crt -text -nameopt multiline -sha1" $OPENSSL x509 -set_serial $SERIAL -extfile $PREFIX-sv.prm -days $DURATION -CA $CAPREFIX-ca.cacert -CAkey $CAPREFIX-ca.key -in $PREFIX-sv.csr -req -out $PREFIX-sv.crt -text -nameopt multiline -sha1 diff --git a/tests/data/Makefile.inc b/tests/data/Makefile.inc index ee95857..b949ab7 100644 --- a/tests/data/Makefile.inc +++ b/tests/data/Makefile.inc @@ -156,4 +156,4 @@ test2000 test2001 test2002 test2003 test2004 test2005 test2006 test2007 \ test2008 test2009 test2010 test2011 test2012 test2013 test2014 test2015 \ test2016 test2017 test2018 test2019 test2020 test2021 test2022 test2023 \ test2024 test2025 test2026 test2027 test2028 test2029 test2030 test2031 \ -test2032 test2033 test2034 test2035 test2036 +test2032 test2033 test2034 test2035 test2036 test2037 test2038 diff --git a/tests/data/test2034 b/tests/data/test2034 index 965c9a1..9bf0a81 100644 --- a/tests/data/test2034 +++ b/tests/data/test2034 @@ -31,7 +31,7 @@ SSLpinning https Server-localhost-sv.pem -simple HTTPS GET with public key pinning +simple HTTPS GET with DER public key pinning --cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.pub.der https://localhost:%HTTPSPORT/2034 diff --git a/tests/data/test2035 b/tests/data/test2035 index 64282fa..7002a5b 100644 --- a/tests/data/test2035 +++ b/tests/data/test2035 @@ -23,7 +23,7 @@ SSLpinning https Server-localhost-sv.pem -HTTPS wrong pinnedpubkey but right CN +HTTPS wrong DER pinnedpubkey but right CN --cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.der https://localhost:%HTTPSPORT/2035 diff --git a/tests/data/test2037 b/tests/data/test2037 new file mode 100644 index 0000000..d630538 --- /dev/null +++ b/tests/data/test2037 @@ -0,0 +1,58 @@ + + + +HTTPS +HTTP GET +PEM certificate + + + +# +# Server-side + + +HTTP/1.1 200 OK +Date: Thu, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Content-Length: 7 + +MooMoo + + + +# +# Client-side + + +SSL +SSLpinning + + +https Server-localhost-sv.pem + + +simple HTTPS GET with PEM public key pinning + + +--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.pub.pem https://localhost:%HTTPSPORT/2037 + +# Ensure that we're running on localhost because we're checking the host name + +perl -e "print 'Test requires default test server host' if ( '%HOSTIP' ne '127.0.0.1' );" + + + +# +# Verify data after the test has been "shot" + + +^User-Agent:.* + + +GET /2037 HTTP/1.1 +Host: localhost:%HTTPSPORT +Accept: */* + + + + diff --git a/tests/data/test2038 b/tests/data/test2038 new file mode 100644 index 0000000..63d935a --- /dev/null +++ b/tests/data/test2038 @@ -0,0 +1,44 @@ + + + +HTTPS +HTTP GET +PEM certificate + + + +# +# Server-side + + + +# +# Client-side + + +SSL +SSLpinning + + +https Server-localhost-sv.pem + + +HTTPS wrong PEM pinnedpubkey but right CN + + +--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.pem https://localhost:%HTTPSPORT/2038 + +# Ensure that we're running on localhost because we're checking the host name + +perl -e "print 'Test requires default test server host' if ( '%HOSTIP' ne '127.0.0.1' );" + + + +# +# Verify data after the test has been "shot" + + +90 + + + -- 1.9.2