]> granicus.if.org Git - curl/commitdiff
SSL: Add PEM format support for public key pinning
authormoparisthebest <admin@moparisthebest.com>
Mon, 24 Nov 2014 18:30:09 +0000 (19:30 +0100)
committerPatrick Monnerat <pm@datasphere.ch>
Mon, 24 Nov 2014 18:30:09 +0000 (19:30 +0100)
15 files changed:
docs/curl.1
docs/libcurl/opts/CURLOPT_PINNEDPUBLICKEY.3
lib/vtls/vtls.c
lib/vtls/vtls.h
src/tool_help.c
tests/certs/Makefile.am
tests/certs/Server-localhost-sv.pub.pem [new file with mode: 0644]
tests/certs/Server-localhost.nn-sv.pub.pem [new file with mode: 0644]
tests/certs/Server-localhost0h-sv.pub.pem [new file with mode: 0644]
tests/certs/scripts/genserv.sh
tests/data/Makefile.inc
tests/data/test2034
tests/data/test2035
tests/data/test2037 [new file with mode: 0644]
tests/data/test2038 [new file with mode: 0644]

index 5f88cffb8bd6281ed1fe3a099769156b1d0ae7d0..7d914645d84b725d6ef1b8d73de27794e66d47bc 100644 (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)
index d7c6932e37e946fa676225afd11583b153585690..2d8639275a1acb8c0a7ae18ee066d8c03c7464bd 100644 (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
index 1d1c62eba8a6677f2235c5ec3b88864476159d13..960f76c17da1c10ce618306fd2760d5583290688 100644 (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;
+
+    /* 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;
+    }
 
-    /* The one good exit point */
-    if(!memcmp(pubkey, buf, pubkeylen))
+    /*
+     * 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;
index f0adc76ff2673dc9b190a197a199d63b5969b499..d24a858f603bbb2f33f26f24b9c0afb571230f95 100644 (file)
 #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
index 2c22b827b2bc3b4b419d79ddd41a05767d5cac62..280d33a87c5aab2e58c05566d3f641750eda1592 100644 (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       "
index cd35bdff2d2539eeb0090f3d9894e7e71fd39bbc..ddb5c9fb1fff6ed2452b976db95a8572194da0ba 100644 (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 \
diff --git a/tests/certs/Server-localhost-sv.pub.pem b/tests/certs/Server-localhost-sv.pub.pem
new file mode 100644 (file)
index 0000000..2384643
--- /dev/null
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwJ3kmLLnk0YEKCdJ2/prhBWgB
+s3J3lzjkYBxxnZn3JnshtW2qnxR2B2ykKi197vZviljEk97+oSUP/1dJwNmU2Qd5
+v4xt+vEYgmegP9cxA4LsuTlpB+zskxdbGnKRk7JrmGZj/mEp562GDgS6v4tVV2Gl
+SvbK58bRuGVCq2dkFwIDAQAB
+-----END PUBLIC KEY-----
diff --git a/tests/certs/Server-localhost.nn-sv.pub.pem b/tests/certs/Server-localhost.nn-sv.pub.pem
new file mode 100644 (file)
index 0000000..3131e95
--- /dev/null
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDT1E7bY1w/OjpeOAmU5k1wnQ2v
+SeaCXQe39c2g369x8c+/1Zq9r3x4XVU/FL27LA5zndaCmtXm9iFdCJKicV+AX1zO
+8MI3N3kPTT3U8oBtRzZF0dKLei4ScUtHhvWMma/nDs+1yU16dfeydAxB46u7LJ1v
+VAgTWjrvfCf3PwsLcQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/tests/certs/Server-localhost0h-sv.pub.pem b/tests/certs/Server-localhost0h-sv.pub.pem
new file mode 100644 (file)
index 0000000..c403ac5
--- /dev/null
@@ -0,0 +1,6 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMqZErIuiQK+VM3K5t2kzzMsyl
+aGdaO7mGo5WIPuhjw+0AYBkDK11bVoraIV5xXNHj3lEYwRcUsTOQAFya5XMLqIic
+0AtUvOo6Od32ZYFLKZlMcdP3aX+A6OhtYUGDh+usLL0P6xv9ojeXbTFWuktR3bEB
+64n4Jd5bo+WyP0x3UwIDAQAB
+-----END PUBLIC KEY-----
index 463952c5715fb0a39e4b099ba45ac0affb2c322d..80876ec51fa997088137f18b7b4a84c544b46d8f 100755 (executable)
@@ -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
index 3edeceb2a127c493693a6a2c48019da00f95a8ec..ee2d84fab1534aacd098be85d7ea224f1cb86c7a 100644 (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
index 965c9a1b1790ec1e3249b0f989e322d705604380..9bf0a817c64e6d2698024bc716c0c76de4b26f4d 100644 (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
index 64282fabefcaceb960484f98b588959f6897b3a6..7002a5b8ec3d32a8995c788eeb09da7ce8ff0477 100644 (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
diff --git a/tests/data/test2037 b/tests/data/test2037
new file mode 100644 (file)
index 0000000..d630538
--- /dev/null
@@ -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\r
+Host: localhost:%HTTPSPORT\r
+Accept: */*\r
+\r
+</protocol>
+</verify>
+</testcase>
diff --git a/tests/data/test2038 b/tests/data/test2038
new file mode 100644 (file)
index 0000000..63d935a
--- /dev/null
@@ -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>