mirror of
https://github.com/moparisthebest/curl
synced 2024-12-21 23:58:49 -05: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:
parent
d1b56d0043
commit
93e450793c
15
docs/curl.1
15
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.
|
||||
|
51
docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3
Normal file
51
docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3
Normal 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.
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -110,6 +110,7 @@ struct OperationConfig {
|
||||
char *cacert;
|
||||
char *capath;
|
||||
char *crlfile;
|
||||
char *pinnedpubkey;
|
||||
char *key;
|
||||
char *key_type;
|
||||
char *key_passwd;
|
||||
|
@ -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;
|
||||
|
@ -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 "
|
||||
|
@ -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);
|
||||
|
BIN
tests/certs/Server-localhost-sv.pub.der
Normal file
BIN
tests/certs/Server-localhost-sv.pub.der
Normal file
Binary file not shown.
BIN
tests/certs/Server-localhost.nn-sv.pub.der
Normal file
BIN
tests/certs/Server-localhost.nn-sv.pub.der
Normal file
Binary file not shown.
BIN
tests/certs/Server-localhost0h-sv.pub.der
Normal file
BIN
tests/certs/Server-localhost0h-sv.pub.der
Normal file
Binary file not shown.
@ -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
|
||||
|
@ -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
57
tests/data/test2034
Normal 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
43
tests/data/test2035
Normal 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>
|
Loading…
Reference in New Issue
Block a user