]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
SSL: implement public key pinning
authormoparisthebest <admin@moparisthebest.com>
Wed, 1 Oct 2014 02:31:17 +0000 (22:31 -0400)
committerDaniel Stenberg <daniel@haxx.se>
Tue, 7 Oct 2014 12:44:19 +0000 (14:44 +0200)
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

20 files changed:
docs/curl.1
docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 [new file with mode: 0644]
docs/libcurl/symbols-in-versions
include/curl/curl.h
lib/strerror.c
lib/url.c
lib/urldata.h
lib/vtls/openssl.c
src/tool_cfgable.c
src/tool_cfgable.h
src/tool_getparam.c
src/tool_help.c
src/tool_operate.c
tests/certs/Server-localhost-sv.pub.der [new file with mode: 0644]
tests/certs/Server-localhost.nn-sv.pub.der [new file with mode: 0644]
tests/certs/Server-localhost0h-sv.pub.der [new file with mode: 0644]
tests/certs/scripts/genserv.sh
tests/data/Makefile.am
tests/data/test2034 [new file with mode: 0644]
tests/data/test2035 [new file with mode: 0644]

index 4d97227af313647d366e19b16dac6df935500fe1..90b28428834f85621be4f65d620f242432f437f3 100644 (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.
diff --git a/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3 b/docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3
new file mode 100644 (file)
index 0000000..a478065
--- /dev/null
@@ -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.
index d4ba61ae10b72d1f3ce8fc6f834469af4474174e..ab9aa7f6496abe55d88fe4572c22cdf5e175dea9 100644 (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
index d40b2dbbf43cb1f625e5a45b489408067e70c9f0..ccd9c3bcb31ad05f5af74c17020a306f8aa09f3a 100644 (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;
 
index 66033f219817a05ab44f07b39787d5f01f40ebd4..1a13606073115a3ca3d994d28b85b35d65bb08e6 100644 (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:
index da67edf78c6a3105ce1525a5e2420bb638a56fdb..6db79deb26c9b80556d744cbd1f629f0329e3a65 100644 (file)
--- a/lib/url.c
+++ b/lib/url.c
@@ -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
index 8594c2f7d732e8d200d9aef2778c3e8222435725..fd59d781d95ba3f74f7e8666c4dbf5dc8262af01 100644 (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 */
index 2d1fa5bd343c10bc42ff338291459908832df526..aacd2778f2e9ad05739ae1dbce0f2cb7992db26c 100644 (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;
index 2fdae073fd6f2cb07afe5b3fc1940df236ad6af7..bd8707e575234c8413a55554d0a03788be9960da 100644 (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);
index 4ef2690266d4793aee3f1d291259f50c02e342eb..11a6a98e03e5cd8c5a525cc520b71c827e05e965 100644 (file)
@@ -110,6 +110,7 @@ struct OperationConfig {
   char *cacert;
   char *capath;
   char *crlfile;
+  char *pinnedpubkey;
   char *key;
   char *key_type;
   char *key_passwd;
index 588a207231acdbb8428928fa67b637e19581a85a..bf025e4e8f9069e5c0eda403bee4e442a8738223 100644 (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;
index c255be0b96b94bf670711a337e82506b869fb3c6..2b26c58af0d302abf8f5cd32bb421cf7f69b20be 100644 (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       "
index fd2fd6ddd74e5798b0c6b9654a6e4ddb5ecfcb73..488fb08c479d37d50cfb390c3b0546320307d9d2 100644 (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);
diff --git a/tests/certs/Server-localhost-sv.pub.der b/tests/certs/Server-localhost-sv.pub.der
new file mode 100644 (file)
index 0000000..7e89b51
Binary files /dev/null and b/tests/certs/Server-localhost-sv.pub.der differ
diff --git a/tests/certs/Server-localhost.nn-sv.pub.der b/tests/certs/Server-localhost.nn-sv.pub.der
new file mode 100644 (file)
index 0000000..b67ab96
Binary files /dev/null and b/tests/certs/Server-localhost.nn-sv.pub.der differ
diff --git a/tests/certs/Server-localhost0h-sv.pub.der b/tests/certs/Server-localhost0h-sv.pub.der
new file mode 100644 (file)
index 0000000..2b071d3
Binary files /dev/null and b/tests/certs/Server-localhost0h-sv.pub.der differ
index a70da9c76dee2a62c45a12cc4787ea8b7faa95ce..463952c5715fb0a39e4b099ba45ac0affb2c322d 100755 (executable)
@@ -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
index 252c8d55edd02f8986c7a3be4576ba545c897160..662ab8c69eb7f83af243ef898741573868424f93 100644 (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
 
diff --git a/tests/data/test2034 b/tests/data/test2034
new file mode 100644 (file)
index 0000000..92f6085
--- /dev/null
@@ -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\r
+Host: localhost:%HTTPSPORT\r
+Accept: */*\r
+\r
+</protocol>
+</verify>
+</testcase>
diff --git a/tests/data/test2035 b/tests/data/test2035
new file mode 100644 (file)
index 0000000..8591be2
--- /dev/null
@@ -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>