From 32fc47886a7b7314262efa2df0649c088772f9d2 Mon Sep 17 00:00:00 2001
From: moparisthebest
Date: Mon, 22 Feb 2016 23:01:36 -0500
Subject: [PATCH] Implement --pinnedpubkey option to pin public keys
---
src/gnutls.c | 73 ++++++++++++++++++-
src/init.c | 3 +
src/main.c | 6 ++
src/openssl.c | 73 ++++++++++++++++++-
src/options.h | 5 ++
src/utils.c | 229 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/utils.h | 10 +++
7 files changed, 394 insertions(+), 5 deletions(-)
diff --git a/src/gnutls.c b/src/gnutls.c
index d39371f..cf7cb72 100644
--- a/src/gnutls.c
+++ b/src/gnutls.c
@@ -37,6 +37,7 @@ as that of the covered work. */
#include
#include
+#include
#include
#include
#include
@@ -671,6 +672,63 @@ ssl_connect_wget (int fd, const char *hostname, int *continue_session)
return true;
}
+static bool pkp_pin_peer_pubkey(gnutls_x509_crt_t cert,
+ const char *pinnedpubkey)
+{
+ /* Scratch */
+ size_t len1 = 0, len2 = 0;
+ char *buff1 = NULL;
+
+ gnutls_pubkey_t key = NULL;
+
+ /* Result is returned to caller */
+ int ret = 0;
+ bool result = false;
+
+ /* if a path wasn't specified, don't pin */
+ if(NULL == pinnedpubkey)
+ return true;
+
+ if(NULL == cert)
+ return result;
+
+ do {
+ /* Begin Gyrations to get the public key */
+ gnutls_pubkey_init(&key);
+
+ ret = gnutls_pubkey_import_x509(key, cert, 0);
+ if(ret < 0)
+ break; /* failed */
+
+ ret = gnutls_pubkey_export(key, GNUTLS_X509_FMT_DER, NULL, &len1);
+ if(ret != GNUTLS_E_SHORT_MEMORY_BUFFER || len1 == 0)
+ break; /* failed */
+
+ buff1 = xmalloc(len1);
+ if(NULL == buff1)
+ break; /* failed */
+
+ len2 = len1;
+
+ ret = gnutls_pubkey_export(key, GNUTLS_X509_FMT_DER, buff1, &len2);
+ if(ret < 0 || len1 != len2)
+ break; /* failed */
+
+ /* End Gyrations */
+
+ /* The one good exit point */
+ result = wg_pin_peer_pubkey(pinnedpubkey, buff1, len1);
+ } while(0);
+
+ if(NULL != key)
+ gnutls_pubkey_deinit(key);
+
+ if(NULL != buff1)
+ xfree(buff1);
+
+ return result;
+}
+
#define _CHECK_CERT(flag,msg) \
if (status & (flag))\
{\
@@ -691,9 +749,10 @@ ssl_check_certificate (int fd, const char *host)
him about problems with the server's certificate. */
const char *severity = opt.check_cert ? _("ERROR") : _("WARNING");
bool success = true;
+ bool pinsuccess = opt.pinnedpubkey == NULL;
/* The user explicitly said to not check for the certificate. */
- if (opt.check_cert == CHECK_CERT_QUIET)
+ if (opt.check_cert == CHECK_CERT_QUIET && pinsuccess)
return success;
err = gnutls_certificate_verify_peers2 (ctx->session, &status);
@@ -727,7 +786,6 @@ ssl_check_certificate (int fd, const char *host)
success = false;
goto out;
}
-
cert_list = gnutls_certificate_get_peers (ctx->session, &cert_list_size);
if (!cert_list)
{
@@ -743,6 +801,7 @@ ssl_check_certificate (int fd, const char *host)
success = false;
goto crt_deinit;
}
+
if (now < gnutls_x509_crt_get_activation_time (cert))
{
logprintf (LOG_NOTQUIET, _("The certificate has not yet been activated\n"));
@@ -760,6 +819,13 @@ ssl_check_certificate (int fd, const char *host)
quote (host));
success = false;
}
+
+ pinsuccess = pkp_pin_peer_pubkey(cert, opt.pinnedpubkey);
+ if (!pinsuccess)
+ {
+ logprintf (LOG_ALWAYS, _("The public key does not match pinned public key!\n"));
+ success = false;
+ }
crt_deinit:
gnutls_x509_crt_deinit (cert);
}
@@ -770,5 +836,6 @@ ssl_check_certificate (int fd, const char *host)
}
out:
- return opt.check_cert == CHECK_CERT_ON ? success : true;
+ /* never return true if pinsuccess fails */
+ return !pinsuccess ? false : (opt.check_cert == CHECK_CERT_ON ? success : true);
}
diff --git a/src/init.c b/src/init.c
index 48859aa..4eae72e 100644
--- a/src/init.c
+++ b/src/init.c
@@ -254,6 +254,9 @@ static const struct {
{ "passiveftp", &opt.ftp_pasv, cmd_boolean },
{ "passwd", &opt.ftp_passwd, cmd_string },/* deprecated*/
{ "password", &opt.passwd, cmd_string },
+#ifdef HAVE_SSL
+ { "pinnedpubkey", &opt.pinnedpubkey, cmd_string },
+#endif
{ "postdata", &opt.post_data, cmd_string },
{ "postfile", &opt.post_file_name, cmd_file },
{ "preferfamily", NULL, cmd_spec_prefer_family },
diff --git a/src/main.c b/src/main.c
index 4641008..147a69a 100644
--- a/src/main.c
+++ b/src/main.c
@@ -350,6 +350,7 @@ static struct cmdline_option option_data[] =
{ "parent", 0, OPT__PARENT, NULL, optional_argument },
{ "passive-ftp", 0, OPT_BOOLEAN, "passiveftp", -1 },
{ "password", 0, OPT_VALUE, "password", -1 },
+ { IF_SSL ("pinnedpubkey"), 0, OPT_VALUE, "pinnedpubkey", -1 },
{ "post-data", 0, OPT_VALUE, "postdata", -1 },
{ "post-file", 0, OPT_VALUE, "postfile", -1 },
{ "prefer-family", 0, OPT_VALUE, "preferfamily", -1 },
@@ -784,6 +785,11 @@ HTTPS (SSL/TLS) options:\n"),
--ca-directory=DIR directory where hash list of CAs is stored\n"),
N_("\
--crl-file=FILE file with bundle of CRLs\n"),
+ N_("\
+ --pinnedpubkey=FILE/HASHES Public key (PEM/DER) file, or any number\n\
+ of base64 encoded sha256 hashes preceded by\n\
+ \'sha256//\' and seperated by \';\', to verify\n\
+ peer against\n"),
#if defined(HAVE_LIBSSL) || defined(HAVE_LIBSSL32)
N_("\
--random-file=FILE file with random data for seeding the SSL PRNG\n"),
diff --git a/src/openssl.c b/src/openssl.c
index 6701c0d..ed7c67d 100644
--- a/src/openssl.c
+++ b/src/openssl.c
@@ -650,6 +650,66 @@ static char *_get_rfc2253_formatted (X509_NAME *name)
return out ? out : xstrdup("");
}
+/*
+ * Heavily modified from:
+ * https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning#OpenSSL
+ */
+static bool pkp_pin_peer_pubkey(X509* cert, const char *pinnedpubkey)
+{
+ /* Scratch */
+ int len1 = 0, len2 = 0;
+ char *buff1 = NULL, *temp = NULL;
+
+ /* Result is returned to caller */
+ bool result = false;
+
+ /* if a path wasn't specified, don't pin */
+ if(!pinnedpubkey)
+ return true;
+
+ if(!cert)
+ return result;
+
+ do
+ {
+ /* Begin Gyrations to get the subjectPublicKeyInfo */
+ /* Thanks to Viktor Dukhovni on the OpenSSL mailing list */
+
+ /* https://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 */
+
+ /* https://www.openssl.org/docs/crypto/buffer.html */
+ buff1 = temp = OPENSSL_malloc(len1);
+ if(!buff1)
+ break; /* failed */
+
+ /* https://www.openssl.org/docs/crypto/d2i_X509.html */
+ len2 = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), (unsigned char **)&temp);
+
+ /*
+ * These checks are verifying we got back the same values as when we
+ * sized the buffer. It's pretty weak since they should always be the
+ * same. But it gives us something to test.
+ */
+ if((len1 != len2) || !temp || ((temp - buff1) != len1))
+ break; /* failed */
+
+ /* End Gyrations */
+
+ /* The one good exit point */
+ result = wg_pin_peer_pubkey(pinnedpubkey, buff1, len1);
+ } while(0);
+
+ /* https://www.openssl.org/docs/crypto/buffer.html */
+ if(NULL != buff1)
+ OPENSSL_free(buff1);
+
+ return result;
+}
+
/* Verify the validity of the certificate presented by the server.
Also check that the "common name" of the server, as presented by
its certificate, corresponds to HOST. (HOST typically comes from
@@ -673,6 +733,7 @@ ssl_check_certificate (int fd, const char *host)
long vresult;
bool success = true;
bool alt_name_checked = false;
+ bool pinsuccess = opt.pinnedpubkey == NULL;
/* If the user has specified --no-check-cert, we still want to warn
him about problems with the server's certificate. */
@@ -683,7 +744,7 @@ ssl_check_certificate (int fd, const char *host)
assert (conn != NULL);
/* The user explicitly said to not check for the certificate. */
- if (opt.check_cert == CHECK_CERT_QUIET)
+ if (opt.check_cert == CHECK_CERT_QUIET && pinsuccess)
return success;
cert = SSL_get_peer_certificate (conn);
@@ -877,6 +938,13 @@ ssl_check_certificate (int fd, const char *host)
}
}
+ pinsuccess = pkp_pin_peer_pubkey(cert, opt.pinnedpubkey);
+ if (!pinsuccess)
+ {
+ logprintf (LOG_ALWAYS, _("The public key does not match pinned public key!\n"));
+ success = false;
+ }
+
if (success)
DEBUGP (("X509 certificate successfully verified and matches host %s\n",
@@ -889,7 +957,8 @@ ssl_check_certificate (int fd, const char *host)
To connect to %s insecurely, use `--no-check-certificate'.\n"),
quotearg_style (escape_quoting_style, host));
- return opt.check_cert == CHECK_CERT_ON ? success : true;
+ /* never return true if pinsuccess fails */
+ return !pinsuccess ? false : (opt.check_cert == CHECK_CERT_ON ? success : true);
}
/*
diff --git a/src/options.h b/src/options.h
index 5cd5fb1..82d2860 100644
--- a/src/options.h
+++ b/src/options.h
@@ -236,6 +236,11 @@ struct options
char *ca_cert; /* CA certificate file to use */
char *crl_file; /* file with CRLs */
+ char *pinnedpubkey; /* Public key (PEM/DER) file, or any number
+ of base64 encoded sha256 hashes preceded by
+ \'sha256//\' and seperated by \';\', to verify
+ peer against */
+
char *random_file; /* file with random data to seed the PRNG */
char *egd_file; /* file name of the egd daemon socket */
bool https_only; /* whether to follow HTTPS only */
diff --git a/src/utils.c b/src/utils.c
index 5222851..5235440 100644
--- a/src/utils.c
+++ b/src/utils.c
@@ -31,6 +31,7 @@ as that of the covered work. */
#include "wget.h"
+#include "sha256.h"
#include
#include
#include
@@ -2521,6 +2522,234 @@ wg_hex_to_string (char *str_buffer, const char *hex_buffer, size_t hex_len)
str_buffer[2 * i] = '\0';
}
+#ifdef HAVE_SSL
+
+/*
+ * Public key pem to der conversion
+ */
+
+static bool wg_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;
+ ssize_t size;
+ unsigned char *base64data;
+
+ *der = NULL;
+ *der_len = 0;
+
+ /* if no pem, exit. */
+ if(!pem)
+ return false;
+
+ begin_pos = strstr(pem, "-----BEGIN PUBLIC KEY-----");
+ if(!begin_pos)
+ return false;
+
+ 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 false;
+
+ /* 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 false;
+
+ pem_len = end_pos - pem;
+
+ stripped_pem = xmalloc(pem_len - pem_count + 1);
+ if(!stripped_pem)
+ return false;
+
+ /*
+ * 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';
+
+ base64data = xmalloc(BASE64_LENGTH(stripped_pem_count));
+
+ size = base64_decode(stripped_pem, base64data);
+
+ if (size < 0) {
+ free(base64data); /* malformed base64 from server */
+ } else {
+ *der = base64data;
+ *der_len = (size_t) size;
+ }
+
+ xfree(stripped_pem);
+
+ return *der_len > 0;
+}
+
+/*
+ * Generic pinned public key check.
+ */
+
+bool wg_pin_peer_pubkey(const char *pinnedpubkey,
+ const char *pubkey, size_t pubkeylen)
+{
+ FILE *fp;
+ unsigned char *buf = NULL, *pem_ptr = NULL;
+ long filesize;
+ size_t size, pem_len;
+ bool pem_read;
+ bool result = false;
+
+ size_t pinkeylen;
+ ssize_t decoded_hash_length;
+ char *pinkeycopy, *begin_pos, *end_pos;
+ unsigned char *sha256sumdigest = NULL, *expectedsha256sumdigest = NULL;
+
+ /* if a path wasn't specified, don't pin */
+ if(!pinnedpubkey)
+ return true;
+ if(!pubkey || !pubkeylen)
+ return result;
+
+ /* only do this if pinnedpubkey starts with "sha256//", length 8 */
+ if(strncmp(pinnedpubkey, "sha256//", 8) == 0) {
+ /* compute sha256sum of public key */
+ sha256sumdigest = xmalloc(SHA256_DIGEST_SIZE);
+ if(!sha256sumdigest)
+ return false;
+ sha256_buffer(pubkey, pubkeylen, sha256sumdigest);
+
+ expectedsha256sumdigest = xmalloc(SHA256_DIGEST_SIZE + 1);
+ if(!expectedsha256sumdigest) {
+ xfree(sha256sumdigest);
+ return false;
+ }
+
+ /* it starts with sha256//, copy so we can modify it */
+ pinkeylen = strlen(pinnedpubkey) + 1;
+ pinkeycopy = xmalloc(pinkeylen);
+ if(!pinkeycopy) {
+ xfree(sha256sumdigest);
+ xfree(expectedsha256sumdigest);
+ return false;
+ }
+ 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//" */
+ decoded_hash_length = base64_decode(begin_pos + 8, expectedsha256sumdigest);
+ /* if valid base64, compare sha256 digests directly */
+ if(SHA256_DIGEST_SIZE == decoded_hash_length &&
+ !memcmp(sha256sumdigest, expectedsha256sumdigest,
+ SHA256_DIGEST_SIZE)) {
+ result = true;
+ break;
+ }
+
+ /*
+ * 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);
+ xfree(sha256sumdigest);
+ xfree(expectedsha256sumdigest);
+ xfree(pinkeycopy);
+ return result;
+ }
+
+ /* fall back to assuming this is a file path */
+ fp = fopen(pinnedpubkey, "rb");
+ if(!fp)
+ return result;
+
+ do {
+ /* Determine the file's size */
+ if(fseek(fp, 0, SEEK_END))
+ break;
+ 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 is bigger than the file
+ * size then it can't match
+ */
+ size = (size_t) filesize;
+ if(pubkeylen > size)
+ break;
+
+ /*
+ * Allocate buffer for the pinned key
+ * With 1 additional byte for null terminator in case of PEM key
+ */
+ buf = xmalloc(size + 1);
+ if(!buf)
+ break;
+
+ /* Returns number of elements read, which should be 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 = true;
+ break;
+ }
+
+ /*
+ * Otherwise we will assume it's PEM and try to decode it
+ * after placing null terminator
+ */
+ buf[size] = '\0';
+ pem_read = wg_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 = true;
+ } while(0);
+
+ if(NULL != buf)
+ xfree(buf);
+ if(NULL != pem_ptr)
+ xfree(pem_ptr);
+ fclose(fp);
+
+ return result;
+}
+
+#endif /* HAVE_SSL */
+
#ifdef TESTING
const char *
diff --git a/src/utils.h b/src/utils.h
index 76f4f8d..bb8f90d 100644
--- a/src/utils.h
+++ b/src/utils.h
@@ -37,6 +37,10 @@ as that of the covered work. */
/* Constant is using when we don`t know attempted size exactly */
#define UNKNOWN_ATTEMPTED_SIZE -3
+#ifndef MAX_PINNED_PUBKEY_SIZE
+#define MAX_PINNED_PUBKEY_SIZE 1048576 /* 1MB */
+#endif
+
/* Macros that interface to malloc, but know about type sizes, and
cast the result to the appropriate type. The casts are not
necessary in standard C, but Wget performs them anyway for the sake
@@ -161,4 +165,10 @@ void wg_hex_to_string (char *str_buffer, const char *hex_buffer, size_t hex_len)
extern unsigned char char_prop[];
+#ifdef HAVE_SSL
+/* Check pinned public key. */
+bool wg_pin_peer_pubkey(const char *pinnedpubkey,
+ const char *pubkey, size_t pubkeylen);
+#endif
+
#endif /* UTILS_H */
--
1.9.2