diff options
author | moparisthebest <admin@moparisthebest.com> | 2014-11-24 19:30:09 +0100 |
---|---|---|
committer | Patrick Monnerat <pm@datasphere.ch> | 2014-11-24 19:30:09 +0100 |
commit | be1a5051890b7ad9339b0208424b94aa32c64776 (patch) | |
tree | d98e121223d1959808a8446e9000ecacaf70a1c8 | |
parent | 2008c92513cee98ded80ed9ffaf984edc0c97c33 (diff) |
SSL: Add PEM format support for public key pinning
-rw-r--r-- | docs/curl.1 | 4 | ||||
-rw-r--r-- | docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 | 2 | ||||
-rw-r--r-- | lib/vtls/vtls.c | 112 | ||||
-rw-r--r-- | lib/vtls/vtls.h | 4 | ||||
-rw-r--r-- | src/tool_help.c | 3 | ||||
-rw-r--r-- | tests/certs/Makefile.am | 8 | ||||
-rw-r--r-- | tests/certs/Server-localhost-sv.pub.pem | 6 | ||||
-rw-r--r-- | tests/certs/Server-localhost.nn-sv.pub.pem | 6 | ||||
-rw-r--r-- | tests/certs/Server-localhost0h-sv.pub.pem | 6 | ||||
-rwxr-xr-x | tests/certs/scripts/genserv.sh | 3 | ||||
-rw-r--r-- | tests/data/Makefile.inc | 2 | ||||
-rw-r--r-- | tests/data/test2034 | 2 | ||||
-rw-r--r-- | tests/data/test2035 | 2 | ||||
-rw-r--r-- | tests/data/test2037 | 58 | ||||
-rw-r--r-- | tests/data/test2038 | 44 |
15 files changed, 242 insertions, 20 deletions
diff --git a/docs/curl.1 b/docs/curl.1 index 5f88cffb8..7d914645d 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 <pinned public key>" (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 d7c6932e3..2d8639275 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 1d1c62eba..960f76c17 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 <curl/mprintf.h> @@ -684,15 +685,75 @@ int Curl_ssl_random(struct SessionHandle *data, } /* + * Public key pem to der conversion + */ + +static CURLcode pubkey_pem_to_der(const char *pem, + unsigned char **der, size_t *der_len) +{ + char *stripped_pem, *begin_pos, *end_pos; + size_t pem_count, stripped_pem_count = 0, pem_len; + CURLcode result; + + /* if no pem, exit. */ + if(!pem) + return CURLE_BAD_CONTENT_ENCODING; + + 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 + 1); + if(!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. */ 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; + FILE *fp; + unsigned char *buf = NULL, *pem_ptr = NULL; + long filesize; + size_t size, pem_len; + CURLcode pem_read; CURLcode result = CURLE_SSL_PINNEDPUBKEYNOTMATCH; /* if a path wasn't specified, don't pin */ @@ -708,32 +769,59 @@ CURLcode Curl_pin_peer_pubkey(const char *pinnedpubkey, /* Determine the file's size */ if(fseek(fp, 0, SEEK_END)) break; - size = ftell(fp); + filesize = ftell(fp); if(fseek(fp, 0, SEEK_SET)) break; + if(filesize < 0 || filesize > MAX_PINNED_PUBKEY_SIZE) + 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 then it can't match */ - if((long) pubkeylen != size) + size = curlx_sotouz((curl_off_t) filesize); + if(pubkeylen > 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; + } - /* The one good exit point */ - if(!memcmp(pubkey, buf, pubkeylen)) + /* + * Otherwise we will assume it's PEM and try to decode it + * after placing null terminator + */ + buf[size] = '\0'; + pem_read = pubkey_pem_to_der((const char *)buf, &pem_ptr, &pem_len); + /* if it wasn't read successfully, exit */ + if(pem_read) + break; + + /* + * 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 f0adc76ff..d24a858f6 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_PINNED_PUBKEY_SIZE +#define MAX_PINNED_PUBKEY_SIZE 1048576 /* 1MB */ +#endif + #ifndef MD5_DIGEST_LENGTH #define MD5_DIGEST_LENGTH 16 /* fixed size */ #endif diff --git a/src/tool_help.c b/src/tool_help.c index 2c22b827b..280d33a87 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/Makefile.am b/tests/certs/Makefile.am index cd35bdff2..ddb5c9fb1 100644 --- a/tests/certs/Makefile.am +++ b/tests/certs/Makefile.am @@ -40,6 +40,8 @@ CERTFILES = \ Server-localhost-sv.p12 \ Server-localhost-sv.pem \ Server-localhost-sv.prm \ + Server-localhost-sv.pub.der \ + Server-localhost-sv.pub.pem \ Server-localhost.nn-sv.crl \ Server-localhost.nn-sv.crt \ Server-localhost.nn-sv.csr \ @@ -48,6 +50,8 @@ CERTFILES = \ Server-localhost.nn-sv.key \ Server-localhost.nn-sv.pem \ Server-localhost.nn-sv.prm \ + Server-localhost.nn-sv.pub.der \ + Server-localhost.nn-sv.pub.pem \ Server-localhost0h-sv.crl \ Server-localhost0h-sv.crt \ Server-localhost0h-sv.csr \ @@ -56,7 +60,9 @@ CERTFILES = \ Server-localhost0h-sv.key \ Server-localhost0h-sv.p12 \ Server-localhost0h-sv.pem \ - Server-localhost0h-sv.prm + Server-localhost0h-sv.prm \ + Server-localhost0h-sv.pub.der \ + Server-localhost0h-sv.pub.pem SRPFILES = \ srp-verifier-conf \ diff --git a/tests/certs/Server-localhost-sv.pub.pem b/tests/certs/Server-localhost-sv.pub.pem new file mode 100644 index 000000000..2384643d6 --- /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 000000000..3131e9539 --- /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 000000000..c403ac575 --- /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 463952c57..80876ec51 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 3edeceb2a..ee2d84fab 100644 --- a/tests/data/Makefile.inc +++ b/tests/data/Makefile.inc @@ -158,4 +158,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 965c9a1b1..9bf0a817c 100644 --- a/tests/data/test2034 +++ b/tests/data/test2034 @@ -31,7 +31,7 @@ SSLpinning https Server-localhost-sv.pem </server> <name> -simple HTTPS GET with public key pinning +simple HTTPS GET with DER public key pinning </name> <command> --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 64282fabe..7002a5b8e 100644 --- a/tests/data/test2035 +++ b/tests/data/test2035 @@ -23,7 +23,7 @@ SSLpinning https Server-localhost-sv.pem </server> <name> -HTTPS wrong pinnedpubkey but right CN +HTTPS wrong DER pinnedpubkey but right CN </name> <command> --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 000000000..d630538e2 --- /dev/null +++ b/tests/data/test2037 @@ -0,0 +1,58 @@ +<testcase> +<info> +<keywords> +HTTPS +HTTP GET +PEM certificate +</keywords> +</info> + +# +# Server-side +<reply> +<data> +HTTP/1.1 200 OK +Date: Thu, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Content-Length: 7 + +MooMoo +</data> +</reply> + +# +# Client-side +<client> +<features> +SSL +SSLpinning +</features> +<server> +https Server-localhost-sv.pem +</server> + <name> +simple HTTPS GET with PEM public key pinning + </name> + <command> +--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.pub.pem https://localhost:%HTTPSPORT/2037 +</command> +# Ensure that we're running on localhost because we're checking the host name +<precheck> +perl -e "print 'Test requires default test server host' if ( '%HOSTIP' ne '127.0.0.1' );" +</precheck> +</client> + +# +# Verify data after the test has been "shot" +<verify> +<strip> +^User-Agent:.* +</strip> +<protocol> +GET /2037 HTTP/1.1
+Host: localhost:%HTTPSPORT
+Accept: */*
+
+</protocol> +</verify> +</testcase> diff --git a/tests/data/test2038 b/tests/data/test2038 new file mode 100644 index 000000000..63d935add --- /dev/null +++ b/tests/data/test2038 @@ -0,0 +1,44 @@ +<testcase> +<info> +<keywords> +HTTPS +HTTP GET +PEM certificate +</keywords> +</info> + +# +# Server-side +<reply> +</reply> + +# +# Client-side +<client> +<features> +SSL +SSLpinning +</features> +<server> +https Server-localhost-sv.pem +</server> + <name> +HTTPS wrong PEM pinnedpubkey but right CN + </name> + <command> +--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.pem https://localhost:%HTTPSPORT/2038 +</command> +# Ensure that we're running on localhost because we're checking the host name +<precheck> +perl -e "print 'Test requires default test server host' if ( '%HOSTIP' ne '127.0.0.1' );" +</precheck> +</client> + +# +# Verify data after the test has been "shot" +<verify> +<errorcode> +90 +</errorcode> +</verify> +</testcase> |