1
0
mirror of https://github.com/moparisthebest/curl synced 2024-08-13 17:03:50 -04:00

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
This commit is contained in:
Travis Burtrum 2014-09-30 22:31:17 -04:00 committed by Daniel Stenberg
parent d1b56d0043
commit 93e450793c
20 changed files with 311 additions and 2 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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;

View File

@ -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:

View File

@ -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

View File

@ -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 */

View File

@ -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;

View File

@ -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);

View File

@ -110,6 +110,7 @@ struct OperationConfig {
char *cacert;
char *capath;
char *crlfile;
char *pinnedpubkey;
char *key;
char *key_type;
char *key_passwd;

View File

@ -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;

View File

@ -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 "

View File

@ -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);

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -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

57
tests/data/test2034 Normal file
View File

@ -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>

43
tests/data/test2035 Normal file
View File

@ -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>