1
0
mirror of https://github.com/moparisthebest/curl synced 2024-12-21 15:48:49 -05:00

SSL: Add PEM format support for public key pinning

This commit is contained in:
Travis Burtrum 2014-11-24 19:30:09 +01:00 committed by Patrick Monnerat
parent 2008c92513
commit be1a505189
15 changed files with 242 additions and 20 deletions

View File

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

View File

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

View File

@ -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>
@ -683,6 +684,64 @@ int Curl_ssl_random(struct SessionHandle *data,
return curlssl_random(data, entropy, length);
}
/*
* 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.
*/
@ -690,9 +749,11 @@ int Curl_ssl_random(struct SessionHandle *data,
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;
/* The one good exit point */
if(!memcmp(pubkey, buf, pubkeylen))
/* 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;
}
/*
* 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;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwJ3kmLLnk0YEKCdJ2/prhBWgB
s3J3lzjkYBxxnZn3JnshtW2qnxR2B2ykKi197vZviljEk97+oSUP/1dJwNmU2Qd5
v4xt+vEYgmegP9cxA4LsuTlpB+zskxdbGnKRk7JrmGZj/mEp562GDgS6v4tVV2Gl
SvbK58bRuGVCq2dkFwIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDT1E7bY1w/OjpeOAmU5k1wnQ2v
SeaCXQe39c2g369x8c+/1Zq9r3x4XVU/FL27LA5zndaCmtXm9iFdCJKicV+AX1zO
8MI3N3kPTT3U8oBtRzZF0dKLei4ScUtHhvWMma/nDs+1yU16dfeydAxB46u7LJ1v
VAgTWjrvfCf3PwsLcQIDAQAB
-----END PUBLIC KEY-----

View File

@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMqZErIuiQK+VM3K5t2kzzMsyl
aGdaO7mGo5WIPuhjw+0AYBkDK11bVoraIV5xXNHj3lEYwRcUsTOQAFya5XMLqIic
0AtUvOo6Od32ZYFLKZlMcdP3aX+A6OhtYUGDh+usLL0P6xv9ojeXbTFWuktR3bEB
64n4Jd5bo+WyP0x3UwIDAQAB
-----END PUBLIC KEY-----

View File

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

View File

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

View File

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

View File

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

58
tests/data/test2037 Normal file
View File

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

44
tests/data/test2038 Normal file
View File

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