cURL / Mailing Lists / curl-library / Single Mail

curl-library

[PATCH] Implement --pinnedpubkey option which will take a path to a public key in DER format and only connect if it matches (currently only implemented with OpenSSL). 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

From: moparisthebest <android_at_moparisthebest.org>
Date: Mon, 25 Aug 2014 19:47:07 -0400

---
 include/curl/curl.h |   6 +++
 lib/url.c           |   9 ++++
 lib/urldata.h       |   2 +
 lib/vtls/openssl.c  | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/tool_cfgable.c  |   1 +
 src/tool_cfgable.h  |   1 +
 src/tool_getparam.c |   6 +++
 src/tool_help.c     |   1 +
 src/tool_operate.c  |   3 ++
 9 files changed, 159 insertions(+)
diff --git a/include/curl/curl.h b/include/curl/curl.h
index d40b2db..ccd9c3b 100644
--- a/include/curl/curl.h
+++ b/include/curl/curl.h
@@ -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;
 
diff --git a/lib/url.c b/lib/url.c
index 89c3fd5..78870c9 100644
--- a/lib/url.c
+++ b/lib/url.c
@@ -1983,6 +1983,14 @@ CURLcode Curl_setopt(struct SessionHandle *data, CURLoption option,
     data->set.ssl.certinfo = (0 != va_arg(param, long))?TRUE:FALSE;
     break;
 #endif
+  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
@@ -5473,6 +5481,7 @@ static CURLcode create_conn(struct SessionHandle *data,
   */
   data->set.ssl.CApath = data->set.str[STRING_SSL_CAPATH];
   data->set.ssl.CAfile = data->set.str[STRING_SSL_CAFILE];
+  data->set.ssl.pinnedpubkey = data->set.str[STRING_SSL_PINNEDPUBLICKEY];
   data->set.ssl.CRLfile = data->set.str[STRING_SSL_CRLFILE];
   data->set.ssl.issuercert = data->set.str[STRING_SSL_ISSUERCERT];
   data->set.ssl.random_file = data->set.str[STRING_SSL_RANDOM_FILE];
diff --git a/lib/urldata.h b/lib/urldata.h
index 8594c2f..16ec349 100644
--- a/lib/urldata.h
+++ b/lib/urldata.h
@@ -378,6 +378,7 @@ struct ssl_config_data {
   void *fsslctxp;        /* parameter for call back */
   bool sessionid;        /* cache session IDs or not */
   bool certinfo;         /* gather lots of certificate info */
+  char *pinnedpubkey;    /* public key to verify peer against */
 
 #ifdef USE_TLS_SRP
   char *username; /* TLS username (for, e.g., SRP) */
@@ -1385,6 +1386,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 */
diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c
index da92854..3570581 100644
--- a/lib/vtls/openssl.c
+++ b/lib/vtls/openssl.c
@@ -2357,6 +2357,130 @@ static CURLcode get_cert_chain(struct connectdata *conn,
 }
 
 /*
+ * Modified from:
+ * https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning#OpenSSL
+ */
+static int pkp_pin_peer_pubkey(X509* cert, char *pinnedpubkey)
+{
+    // if a path wasn't specified, don't pin
+    if(NULL == pinnedpubkey) return TRUE;
+    if(NULL == cert) return FALSE;
+
+    FILE* fp = NULL;
+
+    /* Scratch */
+    int len1 = 0, len2 = 0;
+    unsigned char *buff1 = NULL, *buff2 = NULL;
+
+    /* Result is returned to caller */
+    int ret = 0, result = 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 > 0))
+            break; /* failed */
+
+        /* scratch */
+        unsigned char* temp = NULL;
+
+        /* http://www.openssl.org/docs/crypto/buffer.html */
+        buff1 = temp = OPENSSL_malloc(len1);
+        if(!(buff1 != NULL))
+            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, "rx");
+        if(NULL ==fp)
+            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 */
+        long size = ftell(fp);
+
+        /*
+         * Arbitrary size, but should be relatively small
+         * (less than 1K or 2K)
+         */
+        if(!(size != -1 && size > 0 && size < 2048))
+            break; /* failed */
+
+        /* Rewind to beginning to perform the read */
+        ret = fseek(fp, 0, SEEK_SET);
+        if(!(0 == ret))
+            break; /* failed */
+
+        /* Re-use buff2 and len2 */
+        buff2 = NULL; len2 = (int)size;
+
+        /* http://www.openssl.org/docs/crypto/buffer.html */
+        buff2 = OPENSSL_malloc(len2);
+        if(!(buff2 != NULL))
+            break; /* failed */
+
+        /* Returns number of elements read, which should be 1 */
+        ret = (int)fread(buff2, (size_t)len2, 1, fp);
+        if(!(ret == 1))
+            break; /* failed */
+
+        /* Re-use size. MIN and MAX macro below... */
+        size = len1 < len2 ? len1 : len2;
+
+        /*************************/
+        /*****    PAYDIRT    *****/
+        /*************************/
+        if(len1 != (int)size
+            || len2 != (int)size
+            || 0 != memcmp(buff1, buff2, (size_t)size)
+        )
+            break; /* failed */
+
+        /* The one good exit point */
+        result = TRUE;
+
+    } while(0);
+
+    if(fp != NULL)
+        fclose(fp);
+
+    /* http://www.openssl.org/docs/crypto/buffer.html */
+    if(NULL != buff2)
+        OPENSSL_free(buff2);
+
+    /* http://www.openssl.org/docs/crypto/buffer.html */
+    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
  * purposes only!
@@ -2479,6 +2603,12 @@ static CURLcode servercert(struct connectdata *conn,
       infof(data, "\t SSL certificate verify ok.\n");
   }
 
+  if(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;
diff --git a/src/tool_cfgable.c b/src/tool_cfgable.c
index 2fdae07..bd8707e 100644
--- a/src/tool_cfgable.c
+++ b/src/tool_cfgable.c
@@ -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);
diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h
index 4ef2690..11a6a98 100644
--- a/src/tool_cfgable.h
+++ b/src/tool_cfgable.h
@@ -110,6 +110,7 @@ struct OperationConfig {
   char *cacert;
   char *capath;
   char *crlfile;
+  char *pinnedpubkey;
   char *key;
   char *key_type;
   char *key_passwd;
diff --git a/src/tool_getparam.c b/src/tool_getparam.c
index 180878b..242a2d8 100644
--- a/src/tool_getparam.c
+++ b/src/tool_getparam.c
@@ -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},
@@ -1358,6 +1359,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;
diff --git a/src/tool_help.c b/src/tool_help.c
index c255be0..cad12a3 100644
--- a/src/tool_help.c
+++ b/src/tool_help.c
@@ -51,6 +51,7 @@ static const char *const helptext[] = {
   "     --basic         Use HTTP Basic Authentication (H)",
   "     --cacert FILE   CA certificate to verify peer against (SSL)",
   "     --capath DIR    CA directory to verify peer against (SSL)",
+  "     --pinnedpubkey FILE  Public key (DER) to verify peer against (SSL)",
   " -E, --cert CERT[:PASSWD]  Client certificate file and password (SSL)",
   "     --cert-type TYPE  Certificate file type (DER/PEM/ENG) (SSL)",
   "     --ciphers LIST  SSL ciphers to use (SSL)",
diff --git a/src/tool_operate.c b/src/tool_operate.c
index fd2fd6d..488fb08 100644
--- a/src/tool_operate.c
+++ b/src/tool_operate.c
@@ -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);
-- 
1.9.2
--------------080406020909040506070407
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: inline
LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t
LS0tLS0tLS0tLQpMaXN0IGFkbWluOiBodHRwOi8vY29vbC5oYXh4LnNlL2xpc3QvbGlzdGluZm8v
Y3VybC1saWJyYXJ5CkV0aXF1ZXR0ZTogIGh0dHA6Ly9jdXJsLmhheHguc2UvbWFpbC9ldGlxdWV0
dGUuaHRtbA==
--------------080406020909040506070407--
Received on 2001-09-17