diff --git a/docs/curl.1 b/docs/curl.1 index 4d97227af3..90b2842883 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 " +(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 0000000000..a478065824 --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 @@ -0,0 +1,51 @@ +.\" ************************************************************************** +.\" * _ _ ____ _ +.\" * Project ___| | | | _ \| | +.\" * / __| | | | |_) | | +.\" * | (__| |_| | _ <| |___ +.\" * \___|\___/|_| \_\_____| +.\" * +.\" * Copyright (C) 1998 - 2014, Daniel Stenberg, , 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 + +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 d4ba61ae10..ab9aa7f649 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 d40b2dbbf4..ccd9c3bcb3 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 66033f2198..1a13606073 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: diff --git a/lib/url.c b/lib/url.c index da67edf78c..6db79deb26 100644 --- a/lib/url.c +++ b/lib/url.c @@ -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 8594c2f7d7..fd59d781d9 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 2d1fa5bd34..aacd2778f2 100644 --- a/lib/vtls/openssl.c +++ b/lib/vtls/openssl.c @@ -2362,6 +2362,107 @@ static CURLcode get_cert_chain(struct connectdata *conn, return CURLE_OK; } +/* + * 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 @@ -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 2fdae073fd..bd8707e575 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 4ef2690266..11a6a98e03 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 588a207231..bf025e4e8f 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 c255be0b96..2b26c58af0 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 fd2fd6ddd7..488fb08c47 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 new file mode 100644 index 0000000000..7e89b51a14 Binary files /dev/null and b/tests/certs/Server-localhost-sv.pub.der differ diff --git a/tests/certs/Server-localhost.nn-sv.pub.der b/tests/certs/Server-localhost.nn-sv.pub.der new file mode 100644 index 0000000000..b67ab96edd Binary files /dev/null and b/tests/certs/Server-localhost.nn-sv.pub.der differ diff --git a/tests/certs/Server-localhost0h-sv.pub.der b/tests/certs/Server-localhost0h-sv.pub.der new file mode 100644 index 0000000000..2b071d3ad8 Binary files /dev/null and b/tests/certs/Server-localhost0h-sv.pub.der differ diff --git a/tests/certs/scripts/genserv.sh b/tests/certs/scripts/genserv.sh index a70da9c76d..463952c571 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 252c8d55ed..662ab8c69e 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 0000000000..92f6085d1d --- /dev/null +++ b/tests/data/test2034 @@ -0,0 +1,57 @@ + + + +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 + + +https Server-localhost-sv.pem + + +simple HTTPS GET with public key pinning + + +--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.pub.der https://localhost:%HTTPSPORT/2034 + +# 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 /2034 HTTP/1.1 +Host: localhost:%HTTPSPORT +Accept: */* + + + + diff --git a/tests/data/test2035 b/tests/data/test2035 new file mode 100644 index 0000000000..8591be2718 --- /dev/null +++ b/tests/data/test2035 @@ -0,0 +1,43 @@ + + + +HTTPS +HTTP GET +PEM certificate + + + +# +# Server-side + + + +# +# Client-side + + +SSL + + +https Server-localhost-sv.pem + + +HTTPS wrong pinnedpubkey but right CN + + +--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey %SRCDIR/certs/Server-localhost-sv.der https://localhost:%HTTPSPORT/2035 + +# 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 + + +