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:
parent
2008c92513
commit
be1a505189
@ -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)
|
||||
|
@ -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
|
||||
|
112
lib/vtls/vtls.c
112
lib/vtls/vtls.c
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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 "
|
||||
|
@ -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 \
|
||||
|
6
tests/certs/Server-localhost-sv.pub.pem
Normal file
6
tests/certs/Server-localhost-sv.pub.pem
Normal file
@ -0,0 +1,6 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwJ3kmLLnk0YEKCdJ2/prhBWgB
|
||||
s3J3lzjkYBxxnZn3JnshtW2qnxR2B2ykKi197vZviljEk97+oSUP/1dJwNmU2Qd5
|
||||
v4xt+vEYgmegP9cxA4LsuTlpB+zskxdbGnKRk7JrmGZj/mEp562GDgS6v4tVV2Gl
|
||||
SvbK58bRuGVCq2dkFwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
6
tests/certs/Server-localhost.nn-sv.pub.pem
Normal file
6
tests/certs/Server-localhost.nn-sv.pub.pem
Normal file
@ -0,0 +1,6 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDT1E7bY1w/OjpeOAmU5k1wnQ2v
|
||||
SeaCXQe39c2g369x8c+/1Zq9r3x4XVU/FL27LA5zndaCmtXm9iFdCJKicV+AX1zO
|
||||
8MI3N3kPTT3U8oBtRzZF0dKLei4ScUtHhvWMma/nDs+1yU16dfeydAxB46u7LJ1v
|
||||
VAgTWjrvfCf3PwsLcQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
6
tests/certs/Server-localhost0h-sv.pub.pem
Normal file
6
tests/certs/Server-localhost0h-sv.pub.pem
Normal file
@ -0,0 +1,6 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMqZErIuiQK+VM3K5t2kzzMsyl
|
||||
aGdaO7mGo5WIPuhjw+0AYBkDK11bVoraIV5xXNHj3lEYwRcUsTOQAFya5XMLqIic
|
||||
0AtUvOo6Od32ZYFLKZlMcdP3aX+A6OhtYUGDh+usLL0P6xv9ojeXbTFWuktR3bEB
|
||||
64n4Jd5bo+WyP0x3UwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
58
tests/data/test2037
Normal 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
44
tests/data/test2038
Normal 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>
|
Loading…
Reference in New Issue
Block a user