mirror of
https://github.com/moparisthebest/curl
synced 2024-12-21 15:48:49 -05:00
SSL: Pinned public key hash support
This commit is contained in:
parent
c00b18d540
commit
55b78c5ae9
11
docs/curl.1
11
docs/curl.1
@ -544,9 +544,11 @@ 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 PEM or DER format.
|
||||
.IP "--pinnedpubkey <pinned public key (hashes)>"
|
||||
(SSL) Tells curl to use the specified public key file (or hashes) to verify the
|
||||
peer. This can be a path to a file which contains a single public key in PEM or
|
||||
DER format, or any number of base64 encoded sha256 hashes preceded by
|
||||
\'sha256//\' and seperated by \';\'
|
||||
|
||||
When negotiating a TLS or SSL connection, the server sends a certificate
|
||||
indicating its identity. A public key is extracted from this certificate and
|
||||
@ -554,7 +556,8 @@ if it does not exactly match the public key provided to this option, curl will
|
||||
abort the connection before sending or receiving any data.
|
||||
|
||||
Added in 7.39.0 for OpenSSL, GnuTLS and GSKit. Added in 7.43.0 for NSS and
|
||||
wolfSSL/CyaSSL. Other SSL backends not supported.
|
||||
wolfSSL/CyaSSL. sha256 support added in 7.44.0 for OpenSSL,
|
||||
GnuTLS, NSS and wolfSSL/CyaSSL. Other SSL backends not supported.
|
||||
|
||||
If this option is used several times, the last one will be used.
|
||||
.IP "--cert-status"
|
||||
|
@ -28,8 +28,10 @@ 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 "PEM" or "DER".
|
||||
Pass a pointer to a zero terminated string as parameter. The string can be the
|
||||
file name of your pinned public key. The file format expected is "PEM" or "DER".
|
||||
The string can also be any number of base64 encoded sha256 hashes preceded by
|
||||
"sha256//" and seperated by ";"
|
||||
|
||||
When negotiating a TLS or SSL connection, the server sends a certificate
|
||||
indicating its identity. A public key is extracted from this certificate and
|
||||
@ -45,6 +47,9 @@ CURL *curl = curl_easy_init();
|
||||
if(curl) {
|
||||
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
|
||||
curl_easy_setopt(curl, CURLOPT_PINNEDPUBLICKEY, "/etc/publickey.der");
|
||||
/* OR
|
||||
curl_easy_setopt(curl, CURLOPT_PINNEDPUBLICKEY, "sha256//YhKJKSzoTt2b5FP18fvpHo7fJYqQCjAa3HWY3tvRMwE=;sha256//t62CeU2tQiqkexU74Gxa2eg7fRbEgoChTociMee9wno=");
|
||||
*/
|
||||
|
||||
/* Perform the request */
|
||||
curl_easy_perform(curl);
|
||||
@ -54,9 +59,14 @@ if(curl) {
|
||||
If you do not have the server's public key file you can extract it from the
|
||||
server's certificate.
|
||||
.nf
|
||||
# extract public key in pem format from certificate
|
||||
openssl x509 -in www.test.com.pem -pubkey -noout > www.test.com.pubkey.pem
|
||||
# convert public key from pem to der
|
||||
openssl asn1parse -noout -inform pem -in www.test.com.pubkey.pem -out www.test.com.pubkey.der
|
||||
# sha256 hash and base64 encode der to string for use
|
||||
openssl dgst -sha256 -binary www.test.com.pubkey.der | openssl base64
|
||||
.fi
|
||||
The public key is output in PEM format and contains a header, base64 data and a
|
||||
The public key in PEM format contains a header, base64 data and a
|
||||
footer:
|
||||
.nf
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
@ -65,7 +75,8 @@ footer:
|
||||
.fi
|
||||
.SH AVAILABILITY
|
||||
Added in 7.39.0 for OpenSSL, GnuTLS and GSKit. Added in 7.43.0 for
|
||||
NSS and wolfSSL/CyaSSL. Other SSL backends not supported.
|
||||
NSS and wolfSSL/CyaSSL. sha256 support added in 7.44.0 for OpenSSL,
|
||||
GnuTLS, NSS and wolfSSL/CyaSSL. Other SSL backends not supported.
|
||||
.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.
|
||||
|
@ -67,6 +67,7 @@ and that's a problem since options.h hasn't been included yet. */
|
||||
#include <cyassl/error.h>
|
||||
#endif
|
||||
#include <cyassl/ctaocrypt/random.h>
|
||||
#include <cyassl/ctaocrypt/sha256.h>
|
||||
|
||||
/* The last #include files should be: */
|
||||
#include "curl_memory.h"
|
||||
@ -770,4 +771,16 @@ int Curl_cyassl_random(struct SessionHandle *data,
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Curl_cyassl_sha256sum(const unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *sha256sum /* output */,
|
||||
size_t unused)
|
||||
{
|
||||
Sha256 SHA256pw;
|
||||
(void)unused;
|
||||
InitSha256(&SHA256pw);
|
||||
Sha256Update(&SHA256pw, tmp, tmplen);
|
||||
Sha256Final(&SHA256pw, sha256sum);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -42,6 +42,10 @@ CURLcode Curl_cyassl_connect_nonblocking(struct connectdata *conn,
|
||||
int Curl_cyassl_random(struct SessionHandle *data,
|
||||
unsigned char *entropy,
|
||||
size_t length);
|
||||
void Curl_cyassl_sha256sum(unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *sha256sum, /* output */
|
||||
size_t unused);
|
||||
|
||||
/* Set the API backend definition to Schannel */
|
||||
#define CURL_SSL_BACKEND CURLSSLBACKEND_CYASSL
|
||||
@ -65,6 +69,7 @@ int Curl_cyassl_random(struct SessionHandle *data,
|
||||
#define curlssl_check_cxn(x) ((void)x, -1)
|
||||
#define curlssl_data_pending(x,y) Curl_cyassl_data_pending(x,y)
|
||||
#define curlssl_random(x,y,z) Curl_cyassl_random(x,y,z)
|
||||
#define curlssl_sha256sum(a,b,c,d) Curl_cyassl_sha256sum(a,b,c,d)
|
||||
|
||||
#endif /* USE_CYASSL */
|
||||
#endif /* HEADER_CURL_CYASSL_H */
|
||||
|
@ -39,6 +39,7 @@
|
||||
#ifdef USE_GNUTLS_NETTLE
|
||||
#include <gnutls/crypto.h>
|
||||
#include <nettle/md5.h>
|
||||
#include <nettle/sha2.h>
|
||||
#else
|
||||
#include <gcrypt.h>
|
||||
#endif
|
||||
@ -1557,6 +1558,25 @@ void Curl_gtls_md5sum(unsigned char *tmp, /* input */
|
||||
#endif
|
||||
}
|
||||
|
||||
void Curl_gtls_sha256sum(const unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *sha256sum, /* output */
|
||||
size_t sha256len)
|
||||
{
|
||||
#if defined(USE_GNUTLS_NETTLE)
|
||||
struct sha256_ctx SHA256pw;
|
||||
sha256_init(&SHA256pw);
|
||||
sha256_update(&SHA256pw, (unsigned int)tmplen, tmp);
|
||||
sha256_digest(&SHA256pw, (unsigned int)sha256len, sha256sum);
|
||||
#elif defined(USE_GNUTLS)
|
||||
gcry_md_hd_t SHA256pw;
|
||||
gcry_md_open(&SHA256pw, GCRY_MD_SHA256, 0);
|
||||
gcry_md_write(SHA256pw, tmp, tmplen);
|
||||
memcpy(sha256sum, gcry_md_read (SHA256pw, 0), sha256len);
|
||||
gcry_md_close(SHA256pw);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Curl_gtls_cert_status_request(void)
|
||||
{
|
||||
#ifdef HAS_OCSP
|
||||
|
@ -48,6 +48,10 @@ void Curl_gtls_md5sum(unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *md5sum, /* output */
|
||||
size_t md5len);
|
||||
void Curl_gtls_sha256sum(const unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *sha256sum, /* output */
|
||||
size_t sha256len);
|
||||
|
||||
bool Curl_gtls_cert_status_request(void);
|
||||
|
||||
@ -77,6 +81,7 @@ bool Curl_gtls_cert_status_request(void);
|
||||
#define curlssl_data_pending(x,y) ((void)x, (void)y, 0)
|
||||
#define curlssl_random(x,y,z) Curl_gtls_random(x,y,z)
|
||||
#define curlssl_md5sum(a,b,c,d) Curl_gtls_md5sum(a,b,c,d)
|
||||
#define curlssl_sha256sum(a,b,c,d) Curl_gtls_sha256sum(a,b,c,d)
|
||||
#define curlssl_cert_status_request() Curl_gtls_cert_status_request()
|
||||
|
||||
#endif /* USE_GNUTLS */
|
||||
|
@ -2041,6 +2041,19 @@ void Curl_nss_md5sum(unsigned char *tmp, /* input */
|
||||
PK11_DestroyContext(MD5pw, PR_TRUE);
|
||||
}
|
||||
|
||||
void Curl_nss_sha256sum(const unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *sha256sum, /* output */
|
||||
size_t sha256len)
|
||||
{
|
||||
PK11Context *SHA256pw = PK11_CreateDigestContext(SEC_OID_SHA256);
|
||||
unsigned int SHA256out;
|
||||
|
||||
PK11_DigestOp(SHA256pw, tmp, curlx_uztoui(tmplen));
|
||||
PK11_DigestFinal(SHA256pw, sha256sum, &SHA256out, curlx_uztoui(sha256len));
|
||||
PK11_DestroyContext(SHA256pw, PR_TRUE);
|
||||
}
|
||||
|
||||
bool Curl_nss_cert_status_request(void)
|
||||
{
|
||||
#ifdef SSL_ENABLE_OCSP_STAPLING
|
||||
|
@ -56,6 +56,11 @@ void Curl_nss_md5sum(unsigned char *tmp, /* input */
|
||||
unsigned char *md5sum, /* output */
|
||||
size_t md5len);
|
||||
|
||||
void Curl_nss_sha256sum(const unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *sha256sum, /* output */
|
||||
size_t sha256len);
|
||||
|
||||
bool Curl_nss_cert_status_request(void);
|
||||
|
||||
bool Curl_nss_false_start(void);
|
||||
@ -89,6 +94,7 @@ bool Curl_nss_false_start(void);
|
||||
#define curlssl_data_pending(x,y) ((void)x, (void)y, 0)
|
||||
#define curlssl_random(x,y,z) Curl_nss_random(x,y,z)
|
||||
#define curlssl_md5sum(a,b,c,d) Curl_nss_md5sum(a,b,c,d)
|
||||
#define curlssl_sha256sum(a,b,c,d) Curl_nss_sha256sum(a,b,c,d)
|
||||
#define curlssl_cert_status_request() Curl_nss_cert_status_request()
|
||||
#define curlssl_false_start() Curl_nss_false_start()
|
||||
|
||||
|
@ -3183,6 +3183,18 @@ void Curl_ossl_md5sum(unsigned char *tmp, /* input */
|
||||
MD5_Final(md5sum, &MD5pw);
|
||||
}
|
||||
|
||||
void Curl_ossl_sha256sum(const unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *sha256sum /* output */,
|
||||
size_t unused)
|
||||
{
|
||||
SHA256_CTX SHA256pw;
|
||||
(void)unused;
|
||||
SHA256_Init(&SHA256pw);
|
||||
SHA256_Update(&SHA256pw, tmp, tmplen);
|
||||
SHA256_Final(sha256sum, &SHA256pw);
|
||||
}
|
||||
|
||||
bool Curl_ossl_cert_status_request(void)
|
||||
{
|
||||
#if (OPENSSL_VERSION_NUMBER >= 0x0090808fL) && !defined(OPENSSL_NO_TLSEXT) && \
|
||||
|
@ -72,6 +72,10 @@ void Curl_ossl_md5sum(unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *md5sum /* output */,
|
||||
size_t unused);
|
||||
void Curl_ossl_sha256sum(const unsigned char *tmp, /* input */
|
||||
size_t tmplen,
|
||||
unsigned char *sha256sum /* output */,
|
||||
size_t unused);
|
||||
|
||||
bool Curl_ossl_cert_status_request(void);
|
||||
|
||||
@ -104,6 +108,7 @@ bool Curl_ossl_cert_status_request(void);
|
||||
#define curlssl_data_pending(x,y) Curl_ossl_data_pending(x,y)
|
||||
#define curlssl_random(x,y,z) Curl_ossl_random(x,y,z)
|
||||
#define curlssl_md5sum(a,b,c,d) Curl_ossl_md5sum(a,b,c,d)
|
||||
#define curlssl_sha256sum(a,b,c,d) Curl_ossl_sha256sum(a,b,c,d)
|
||||
#define curlssl_cert_status_request() Curl_ossl_cert_status_request()
|
||||
|
||||
#define DEFAULT_CIPHER_SELECTION \
|
||||
|
@ -774,12 +774,78 @@ CURLcode Curl_pin_peer_pubkey(const char *pinnedpubkey,
|
||||
size_t size, pem_len;
|
||||
CURLcode pem_read;
|
||||
CURLcode result = CURLE_SSL_PINNEDPUBKEYNOTMATCH;
|
||||
#ifdef curlssl_sha256sum
|
||||
size_t pinkeylen;
|
||||
char *pinkeycopy, *begin_pos, *end_pos;
|
||||
unsigned char *sha256sumdigest = NULL, *expectedsha256sumdigest = NULL;
|
||||
#endif
|
||||
|
||||
/* if a path wasn't specified, don't pin */
|
||||
if(!pinnedpubkey)
|
||||
return CURLE_OK;
|
||||
if(!pubkey || !pubkeylen)
|
||||
return result;
|
||||
|
||||
#ifdef curlssl_sha256sum
|
||||
/* only do this if pinnedpubkey starts with "sha256//", length 8 */
|
||||
if(strncmp(pinnedpubkey, "sha256//", 8) == 0) {
|
||||
/* compute sha256sum of public key */
|
||||
sha256sumdigest = malloc(SHA256_DIGEST_LENGTH);
|
||||
if(!sha256sumdigest)
|
||||
return CURLE_OUT_OF_MEMORY;
|
||||
curlssl_sha256sum(pubkey, pubkeylen,
|
||||
sha256sumdigest, SHA256_DIGEST_LENGTH);
|
||||
|
||||
/* it starts with sha256//, copy so we can modify it */
|
||||
pinkeylen = strlen(pinnedpubkey) + 1;
|
||||
pinkeycopy = malloc(pinkeylen);
|
||||
if(!pinkeycopy) {
|
||||
Curl_safefree(sha256sumdigest);
|
||||
return CURLE_OUT_OF_MEMORY;
|
||||
}
|
||||
memcpy(pinkeycopy, pinnedpubkey, pinkeylen);
|
||||
/* point begin_pos to the copy, and start extracting keys */
|
||||
begin_pos = pinkeycopy;
|
||||
do {
|
||||
end_pos = strstr(begin_pos, ";sha256//");
|
||||
/*
|
||||
* if there is an end_pos, null terminate,
|
||||
* otherwise it'll go to the end of the original string
|
||||
*/
|
||||
if(end_pos)
|
||||
end_pos[0] = '\0';
|
||||
|
||||
/* decode base64 pinnedpubkey, 8 is length of "sha256//" */
|
||||
pem_read = Curl_base64_decode(begin_pos + 8,
|
||||
&expectedsha256sumdigest, &size);
|
||||
/* if not valid base64, don't bother comparing or freeing */
|
||||
if(!pem_read) {
|
||||
/* compare sha256 digests directly */
|
||||
if(SHA256_DIGEST_LENGTH == size &&
|
||||
!memcmp(sha256sumdigest, expectedsha256sumdigest,
|
||||
SHA256_DIGEST_LENGTH)) {
|
||||
result = CURLE_OK;
|
||||
Curl_safefree(expectedsha256sumdigest);
|
||||
break;
|
||||
}
|
||||
Curl_safefree(expectedsha256sumdigest);
|
||||
}
|
||||
|
||||
/*
|
||||
* change back the null-terminator we changed earlier,
|
||||
* and look for next begin
|
||||
*/
|
||||
if(end_pos) {
|
||||
end_pos[0] = ';';
|
||||
begin_pos = strstr(end_pos, "sha256//");
|
||||
}
|
||||
} while(end_pos && begin_pos);
|
||||
Curl_safefree(sha256sumdigest);
|
||||
Curl_safefree(pinkeycopy);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
fp = fopen(pinnedpubkey, "rb");
|
||||
if(!fp)
|
||||
return result;
|
||||
|
@ -41,6 +41,10 @@
|
||||
#define MD5_DIGEST_LENGTH 16 /* fixed size */
|
||||
#endif
|
||||
|
||||
#ifndef SHA256_DIGEST_LENGTH
|
||||
#define SHA256_DIGEST_LENGTH 32 /* fixed size */
|
||||
#endif
|
||||
|
||||
/* see http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg-04 */
|
||||
#define ALPN_HTTP_1_1_LENGTH 8
|
||||
#define ALPN_HTTP_1_1 "http/1.1"
|
||||
|
@ -156,8 +156,7 @@ static const char *const helptext[] = {
|
||||
" -o, --output FILE Write to FILE instead of stdout",
|
||||
" --pass PASS Pass phrase for the private key (SSL/SSH)",
|
||||
" --path-as-is Do not squash .. sequences in URL path",
|
||||
" --pinnedpubkey FILE Public key (PEM/DER) to verify peer against "
|
||||
"(OpenSSL/GnuTLS/NSS/wolfSSL/CyaSSL/GSKit only)",
|
||||
" --pinnedpubkey FILE/HASHES Public key to verify peer against (SSL)",
|
||||
" --post301 "
|
||||
"Do not switch to GET after following a 301 redirect (H)",
|
||||
" --post302 "
|
||||
|
@ -166,4 +166,4 @@ 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 test2037 test2038 test2039 \
|
||||
test2040
|
||||
test2040 test2041 test2042
|
||||
|
58
tests/data/test2041
Normal file
58
tests/data/test2041
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 base64-sha256 public key pinning
|
||||
</name>
|
||||
<command>
|
||||
--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey sha256//pyh+fICi9M8MFEZvherIT0cs3MN+cXNGoU9Giwyx1so= https://localhost:%HTTPSPORT/2041
|
||||
</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 /2041 HTTP/1.1
|
||||
Host: localhost:%HTTPSPORT
|
||||
Accept: */*
|
||||
|
||||
</protocol>
|
||||
</verify>
|
||||
</testcase>
|
44
tests/data/test2042
Normal file
44
tests/data/test2042
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 base64-sha256 pinnedpubkey but right CN
|
||||
</name>
|
||||
<command>
|
||||
--cacert %SRCDIR/certs/EdelCurlRoot-ca.crt --pinnedpubkey sha256//bSIggTf+ikMG0CtmDlpMVBd7yi7H1md4URogRPqerso= https://localhost:%HTTPSPORT/2042
|
||||
</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