diff options
author | moparisthebest <admin@moparisthebest.com> | 2014-09-30 22:31:17 -0400 |
---|---|---|
committer | Daniel Stenberg <daniel@haxx.se> | 2014-10-07 14:44:19 +0200 |
commit | 93e450793ce289925dfd1d5e3b2d14e781f8dfd4 (patch) | |
tree | 3ceea898922e067a4a692204f6388ab633deebef | |
parent | d1b56d00439ab26d7fc43e37ab18ae331ddc400d (diff) |
SSL: implement public key pinning
Option --pinnedpubkey takes a path to a public key in DER format and
only connect if it matches (currently only implemented with OpenSSL).
Provides CURLOPT_PINNEDPUBLICKEY for curl_easy_setopt().
Extract a public RSA key from a website like so:
openssl s_client -connect google.com:443 2>&1 < /dev/null | \
sed -n '/-----BEGIN/,/-----END/p' | openssl x509 -noout -pubkey \
| openssl rsa -pubin -outform DER > google.com.der
-rw-r--r-- | docs/curl.1 | 15 | ||||
-rw-r--r-- | docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 | 51 | ||||
-rw-r--r-- | docs/libcurl/symbols-in-versions | 4 | ||||
-rw-r--r-- | include/curl/curl.h | 6 | ||||
-rw-r--r-- | lib/strerror.c | 3 | ||||
-rw-r--r-- | lib/url.c | 8 | ||||
-rw-r--r-- | lib/urldata.h | 1 | ||||
-rw-r--r-- | lib/vtls/openssl.c | 108 | ||||
-rw-r--r-- | src/tool_cfgable.c | 1 | ||||
-rw-r--r-- | src/tool_cfgable.h | 1 | ||||
-rw-r--r-- | src/tool_getparam.c | 6 | ||||
-rw-r--r-- | src/tool_help.c | 1 | ||||
-rw-r--r-- | src/tool_operate.c | 3 | ||||
-rw-r--r-- | tests/certs/Server-localhost-sv.pub.der | bin | 0 -> 162 bytes | |||
-rw-r--r-- | tests/certs/Server-localhost.nn-sv.pub.der | bin | 0 -> 162 bytes | |||
-rw-r--r-- | tests/certs/Server-localhost0h-sv.pub.der | bin | 0 -> 162 bytes | |||
-rwxr-xr-x | tests/certs/scripts/genserv.sh | 3 | ||||
-rw-r--r-- | tests/data/Makefile.am | 2 | ||||
-rw-r--r-- | tests/data/test2034 | 57 | ||||
-rw-r--r-- | tests/data/test2035 | 43 |
20 files changed, 311 insertions, 2 deletions
diff --git a/docs/curl.1 b/docs/curl.1 index 4d97227af..90b284288 100644 --- a/docs/curl.1 +++ b/docs/curl.1 @@ -530,6 +530,19 @@ OpenSSL-powered curl to make SSL-connections much more efficiently than using 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. + +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 backend, with more backends +expected to follow shortly. + +If this option is used several times, the last one will be used. .IP "-f, --fail" (HTTP) Fail silently (no output at all) on server errors. This is mostly done to better enable scripts etc to better deal with failed attempts. In @@ -2180,6 +2193,8 @@ unable to parse FTP file list FTP chunk callback reported error .IP 89 No connection available, the session will be queued +.IP 90 +SSL public key does not matched pinned public key .IP XX More error codes will appear here in future releases. The existing ones are meant to never change. diff --git a/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 b/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 new file mode 100644 index 000000000..a47806582 --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 @@ -0,0 +1,51 @@ +.\" ************************************************************************** +.\" * _ _ ____ _ +.\" * Project ___| | | | _ \| | +.\" * / __| | | | |_) | | +.\" * | (__| |_| | _ <| |___ +.\" * \___|\___/|_| \_\_____| +.\" * +.\" * Copyright (C) 1998 - 2014, Daniel Stenberg, <daniel@haxx.se>, et al. +.\" * +.\" * This software is licensed as described in the file COPYING, which +.\" * you should have received as part of this distribution. The terms +.\" * are also available at http://curl.haxx.se/docs/copyright.html. +.\" * +.\" * You may opt to use, copy, modify, merge, publish, distribute and/or sell +.\" * copies of the Software, and permit persons to whom the Software is +.\" * furnished to do so, under the terms of the COPYING file. +.\" * +.\" * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +.\" * KIND, either express or implied. +.\" * +.\" ************************************************************************** +.\" +.TH CURLOPT_PINNEDPUBLICKEY 3 "27 Aug 2014" "libcurl 7.38.0" "curl_easy_setopt options" +.SH NAME +CURLOPT_PINNEDPUBLICKEY \- set pinned public key +.SH SYNOPSIS +#include <curl/curl.h> + +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". + +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 backend, with more backends +expected to follow shortly. +.SH DEFAULT +NULL +.SH PROTOCOLS +All TLS based protocols: HTTPS, FTPS, IMAPS, POP3, SMTPS etc. +.SH EXAMPLE +TODO +.SH AVAILABILITY +If built TLS enabled. +.SH RETURN VALUE +Returns CURLE_OK if TLS enabled, CURLE_UNKNOWN_OPTION if not, or +CURLE_OUT_OF_MEMORY if there was insufficient heap space. diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions index d4ba61ae1..ab9aa7f64 100644 --- a/docs/libcurl/symbols-in-versions +++ b/docs/libcurl/symbols-in-versions @@ -74,12 +74,12 @@ CURLE_FTP_WEIRD_USER_REPLY 7.1 7.17.0 CURLE_FTP_WRITE_ERROR 7.1 7.17.0 CURLE_FUNCTION_NOT_FOUND 7.1 CURLE_GOT_NOTHING 7.9.1 +CURLE_HTTP2 7.38.0 CURLE_HTTP_NOT_FOUND 7.1 CURLE_HTTP_PORT_FAILED 7.3 7.12.0 CURLE_HTTP_POST_ERROR 7.1 CURLE_HTTP_RANGE_ERROR 7.1 7.17.0 CURLE_HTTP_RETURNED_ERROR 7.10.3 -CURLE_HTTP2 7.38.0 CURLE_INTERFACE_FAILED 7.12.0 CURLE_LDAP_CANNOT_BIND 7.1 CURLE_LDAP_INVALID_URL 7.10.8 @@ -120,6 +120,7 @@ CURLE_SSL_ENGINE_NOTFOUND 7.9.3 CURLE_SSL_ENGINE_SETFAILED 7.9.3 CURLE_SSL_ISSUER_ERROR 7.19.0 CURLE_SSL_PEER_CERTIFICATE 7.8 7.17.1 +CURLE_SSL_PINNEDPUBKEYNOTMATCH 7.39.0 CURLE_SSL_SHUTDOWN_FAILED 7.16.1 CURLE_TELNET_OPTION_SYNTAX 7.7 CURLE_TFTP_DISKFULL 7.15.0 7.17.0 @@ -429,6 +430,7 @@ CURLOPT_PASSWDDATA 7.4.2 7.11.1 7.15.5 CURLOPT_PASSWDFUNCTION 7.4.2 7.11.1 7.15.5 CURLOPT_PASSWORD 7.19.1 CURLOPT_PASV_HOST 7.12.1 7.16.0 7.15.5 +CURLOPT_PINNEDPUBLICKEY 7.39.0 CURLOPT_PORT 7.1 CURLOPT_POST 7.1 CURLOPT_POST301 7.17.1 7.19.1 diff --git a/include/curl/curl.h b/include/curl/curl.h index d40b2dbbf..ccd9c3bcb 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -521,6 +521,8 @@ typedef enum { CURLE_CHUNK_FAILED, /* 88 - chunk callback reported error */ CURLE_NO_CONNECTION_AVAILABLE, /* 89 - No connection available, the session will be queued */ + CURLE_SSL_PINNEDPUBKEYNOTMATCH, /* 90 - specified pinned public key did not + match */ CURL_LAST /* never use! */ } CURLcode; @@ -1611,6 +1613,10 @@ typedef enum { /* Pass in a bitmask of "header options" */ CINIT(HEADEROPT, LONG, 229), + /* The public key in DER form used to validate the peer public key + this option is used only if SSL_VERIFYPEER is true */ + CINIT(PINNEDPUBLICKEY, OBJECTPOINT, 230), + CURLOPT_LASTENTRY /* the last unused */ } CURLoption; diff --git a/lib/strerror.c b/lib/strerror.c index 66033f219..1a1360607 100644 --- a/lib/strerror.c +++ b/lib/strerror.c @@ -298,6 +298,9 @@ curl_easy_strerror(CURLcode error) case CURLE_NO_CONNECTION_AVAILABLE: return "The max connection limit is reached"; + case CURLE_SSL_PINNEDPUBKEYNOTMATCH: + return "SSL public key does not matched pinned public key"; + /* error codes not used by current libcurl */ case CURLE_OBSOLETE20: case CURLE_OBSOLETE24: @@ -1991,6 +1991,14 @@ CURLcode Curl_setopt(struct SessionHandle *data, CURLoption option, result = CURLE_NOT_BUILT_IN; #endif break; + case CURLOPT_PINNEDPUBLICKEY: + /* + * Set pinned public key for SSL connection. + * Specify file name of the public key in DER format. + */ + result = setstropt(&data->set.str[STRING_SSL_PINNEDPUBLICKEY], + va_arg(param, char *)); + break; case CURLOPT_CAINFO: /* * Set CA info for SSL connection. Specify file name of the CA certificate diff --git a/lib/urldata.h b/lib/urldata.h index 8594c2f7d..fd59d781d 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -1385,6 +1385,7 @@ enum dupstring { STRING_SET_URL, /* what original URL to work on */ STRING_SSL_CAPATH, /* CA directory name (doesn't work on windows) */ STRING_SSL_CAFILE, /* certificate file to verify peer against */ + STRING_SSL_PINNEDPUBLICKEY, /* public key file to verify peer against */ STRING_SSL_CIPHER_LIST, /* list of ciphers to use */ STRING_SSL_EGDSOCKET, /* path to file containing the EGD daemon socket */ STRING_SSL_RANDOM_FILE, /* path to file containing "random" data */ diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c index 2d1fa5bd3..aacd2778f 100644 --- a/lib/vtls/openssl.c +++ b/lib/vtls/openssl.c @@ -2363,6 +2363,107 @@ static CURLcode get_cert_chain(struct connectdata *conn, } /* + * Heavily modified from: + * https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning#OpenSSL + */ +static int pkp_pin_peer_pubkey(X509* cert, char *pinnedpubkey) +{ + /* Scratch */ + FILE* fp = NULL; + int len1 = 0, len2 = 0; + unsigned char *buff1 = NULL, *buff2 = NULL, *temp = NULL; + long size = 0; + + /* Result is returned to caller */ + int ret = 0, result = FALSE; + + /* if a path wasn't specified, don't pin */ + if(NULL == pinnedpubkey) return TRUE; + if(NULL == cert) return FALSE; + + do { + /* Begin Gyrations to get the subjectPublicKeyInfo */ + /* Thanks to Viktor Dukhovni on the OpenSSL mailing list */ + + /* http://groups.google.com/group/mailing.openssl.users/browse_thread + /thread/d61858dae102c6c7 */ + len1 = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), NULL); + if(len1 < 1) + break; /* failed */ + + /* http://www.openssl.org/docs/crypto/buffer.html */ + buff1 = temp = OPENSSL_malloc(len1); + if(NULL == buff1) + break; /* failed */ + + /* http://www.openssl.org/docs/crypto/d2i_X509.html */ + len2 = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), &temp); + + /* + * These checks are verifying we got back the same values as when we + * sized the buffer.Its pretty weak since they should always be the + * same. But it gives us something to test. + */ + if(len1 != len2 || temp == NULL || ((temp - buff1) != len1)) + break; /* failed */ + + /* End Gyrations */ + + /* See the warning above!!! */ + fp = fopen(pinnedpubkey, "r"); + + if(NULL == fp) + break; /* failed */ + + /* Seek to eof to determine the file's size */ + ret = fseek(fp, 0, SEEK_END); + if(0 != ret) + break; /* failed */ + + /* Fetch the file's size */ + size = ftell(fp); + + /* + * 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(len2 != size) + break; /* failed */ + + /* Rewind to beginning to perform the read */ + ret = fseek(fp, 0, SEEK_SET); + if(0 != ret) + break; /* failed */ + + /* http://www.openssl.org/docs/crypto/buffer.html */ + buff2 = OPENSSL_malloc(len2); + if(NULL == buff2) + break; /* failed */ + + /* Returns number of elements read, which should be 1 */ + ret = (int)fread(buff2, (size_t)len2, 1, fp); + if(1 != ret) + break; /* failed */ + + /* The one good exit point */ + result = (0 == memcmp(buff1, buff2, (size_t)len2)); + + } while(0); + + if(NULL != fp) + fclose(fp); + + /* http://www.openssl.org/docs/crypto/buffer.html */ + if(NULL != buff2) + OPENSSL_free(buff2); + + if(NULL != buff1) + OPENSSL_free(buff1); + + return result; +} + +/* * Get the server cert, verify it and show it etc, only call failf() if the * 'strict' argument is TRUE as otherwise all this is for informational * purposes only! @@ -2485,6 +2586,13 @@ static CURLcode servercert(struct connectdata *conn, infof(data, "\t SSL certificate verify ok.\n"); } + if(data->set.str[STRING_SSL_PINNEDPUBLICKEY] != NULL && + TRUE != pkp_pin_peer_pubkey(connssl->server_cert, + data->set.str[STRING_SSL_PINNEDPUBLICKEY])) { + failf(data, "SSL: public key does not matched pinned public key!"); + return CURLE_SSL_PINNEDPUBKEYNOTMATCH; + } + X509_free(connssl->server_cert); connssl->server_cert = NULL; connssl->connecting_state = ssl_connect_done; diff --git a/src/tool_cfgable.c b/src/tool_cfgable.c index 2fdae073f..bd8707e57 100644 --- a/src/tool_cfgable.c +++ b/src/tool_cfgable.c @@ -101,6 +101,7 @@ static void free_config_fields(struct OperationConfig *config) Curl_safefree(config->cacert); Curl_safefree(config->capath); Curl_safefree(config->crlfile); + Curl_safefree(config->pinnedpubkey); Curl_safefree(config->key); Curl_safefree(config->key_type); Curl_safefree(config->key_passwd); diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h index 4ef269026..11a6a98e0 100644 --- a/src/tool_cfgable.h +++ b/src/tool_cfgable.h @@ -110,6 +110,7 @@ struct OperationConfig { char *cacert; char *capath; char *crlfile; + char *pinnedpubkey; char *key; char *key_type; char *key_passwd; diff --git a/src/tool_getparam.c b/src/tool_getparam.c index 588a20723..bf025e4e8 100644 --- a/src/tool_getparam.c +++ b/src/tool_getparam.c @@ -215,6 +215,7 @@ static const struct LongShort aliases[]= { {"Em", "tlsauthtype", TRUE}, {"En", "ssl-allow-beast", FALSE}, {"Eo", "login-options", TRUE}, + {"Ep", "pinnedpubkey", TRUE}, {"f", "fail", FALSE}, {"F", "form", TRUE}, {"Fs", "form-string", TRUE}, @@ -1353,6 +1354,11 @@ ParameterError getparameter(char *flag, /* f or -long-flag */ GetStr(&config->login_options, nextarg); break; + case 'p': /* Pinned public key DER file */ + /* Pinned public key DER file */ + GetStr(&config->pinnedpubkey, nextarg); + break; + default: /* certificate file */ { char *certname, *passphrase; diff --git a/src/tool_help.c b/src/tool_help.c index c255be0b9..2b26c58af 100644 --- a/src/tool_help.c +++ b/src/tool_help.c @@ -152,6 +152,7 @@ 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)", " --post301 " "Do not switch to GET after following a 301 redirect (H)", " --post302 " diff --git a/src/tool_operate.c b/src/tool_operate.c index fd2fd6ddd..488fb08c4 100644 --- a/src/tool_operate.c +++ b/src/tool_operate.c @@ -1025,6 +1025,9 @@ static CURLcode operate_do(struct GlobalConfig *global, if(config->crlfile) my_setopt_str(curl, CURLOPT_CRLFILE, config->crlfile); + if(config->pinnedpubkey) + my_setopt_str(curl, CURLOPT_PINNEDPUBLICKEY, config->pinnedpubkey); + if(curlinfo->features & CURL_VERSION_SSL) { if(config->insecure_ok) { my_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); diff --git a/tests/certs/Server-localhost-sv.pub.der b/tests/certs/Server-localhost-sv.pub.der Binary files differnew file mode 100644 index 000000000..7e89b51a1 --- /dev/null +++ b/tests/certs/Server-localhost-sv.pub.der diff --git a/tests/certs/Server-localhost.nn-sv.pub.der b/tests/certs/Server-localhost.nn-sv.pub.der Binary files differnew file mode 100644 index 000000000..b67ab96ed --- /dev/null +++ b/tests/certs/Server-localhost.nn-sv.pub.der diff --git a/tests/certs/Server-localhost0h-sv.pub.der b/tests/certs/Server-localhost0h-sv.pub.der Binary files differnew file mode 100644 index 000000000..2b071d3ad --- /dev/null +++ b/tests/certs/Server-localhost0h-sv.pub.der diff --git a/tests/certs/scripts/genserv.sh b/tests/certs/scripts/genserv.sh index a70da9c76..463952c57 100755 --- a/tests/certs/scripts/genserv.sh +++ b/tests/certs/scripts/genserv.sh @@ -75,6 +75,9 @@ echo "openssl rsa -in $PREFIX-sv.key -out $PREFIX-sv.key" $OPENSSL rsa -in $PREFIX-sv.key -out $PREFIX-sv.key -passin pass:secret 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 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.am b/tests/data/Makefile.am index 252c8d55e..662ab8c69 100644 --- a/tests/data/Makefile.am +++ b/tests/data/Makefile.am @@ -138,7 +138,7 @@ 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 +test2032 test2033 test2034 test2035 EXTRA_DIST = $(TESTCASES) DISABLED diff --git a/tests/data/test2034 b/tests/data/test2034 new file mode 100644 index 000000000..92f6085d1 --- /dev/null +++ b/tests/data/test2034 @@ -0,0 +1,57 @@ +<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 +</features> +<server> +https Server-localhost-sv.pem +</server> + <name> +simple HTTPS GET with public key pinning + </name> + <command> +--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.pub.der https://localhost:%HTTPSPORT/2034 +</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 /2034 HTTP/1.1
+Host: localhost:%HTTPSPORT
+Accept: */*
+
+</protocol> +</verify> +</testcase> diff --git a/tests/data/test2035 b/tests/data/test2035 new file mode 100644 index 000000000..8591be271 --- /dev/null +++ b/tests/data/test2035 @@ -0,0 +1,43 @@ +<testcase> +<info> +<keywords> +HTTPS +HTTP GET +PEM certificate +</keywords> +</info> + +# +# Server-side +<reply> +</reply> + +# +# Client-side +<client> +<features> +SSL +</features> +<server> +https Server-localhost-sv.pem +</server> + <name> +HTTPS wrong pinnedpubkey but right CN + </name> + <command> +--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.der https://localhost:%HTTPSPORT/2035 +</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> |