Skip to content

Commit

Permalink
http: pin TLS pubkeys instead of fixing cert store
Browse files Browse the repository at this point in the history
Currently, lpass bakes in the root CAs that are needed to validate
the peer certificate chain for lastpass.com and lastpass.eu.

This approach serves two purposes: first, it enables the certs to
validate if the root CAs are not in the system certificate store,
and second, it limits the risk that a compromised or untrustworthy
CA can produce a fake certificate for lastpass.

While the latter reason remains important, the former is not so
important today as pretty much every platform maintains their own
system certificate store that is reasonably up-to-date.

LastPass will soon be updating both the root CA and key for some
properties.  To prepare for that, at least the new root will need
to be added, so now is a good time to revisit this.

Key pinning has some advantages over cert pinning, namely that
even as certs expire and are reissued, key pinning can continue
to work without changes.  Also, HPKP is being implemented by the
major browsers and will bring more visibility to the key pins,
so let's follow suit here.

This change removes the baked-in certs and defers to the platform
certificate store for validating the chain, while adding a
mechanism for hashing and checking the subject public key info.
One of the certs in the chain must match one of the hashes for
the connection to be allowed.

For the interested, a fingerprint can be generated from a certificate
like so:

    openssl x509 -in some-certificate.crt -pubkey -noout | \
        openssl rsa -pubin -outform der | \
        openssl dgst -sha256 -binary | \
        openssl enc -base64

Public key pins are added for the existing Thawte (lastpass.com)
and AddTrust (lastpass.eu) roots.

Signed-off-by: Bob Copeland <[email protected]>

Backported from master.

Conflicts:
	Makefile
	http.c
  • Loading branch information
Bob Copeland committed Jan 28, 2016
1 parent 9bcbb16 commit 28b4dc5
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 29 deletions.
4 changes: 0 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ lpass: $(patsubst %.c,%.o,$(wildcard *.c))
%.1.html: %.1.txt
asciidoc -b html5 -a data-uri -a icons -a toc2 $<

http.c: certificate.h
certificate.h: thawte.pem
awk 'BEGIN {printf "#define CERTIFICATE_THAWTE \""} {printf "%s\\n", $$0} END {printf "\"\n"}' thawte.pem > certificate.h || rm -f certificate.h

install-doc: doc-man
@install -v -d "$(DESTDIR)$(MANDIR)/man1" && install -m 0644 -v lpass.1 "$(DESTDIR)$(MANDIR)/man1/lpass.1"

Expand Down
88 changes: 63 additions & 25 deletions http.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
#include "http.h"
#include "util.h"
#include "version.h"
#include "certificate.h"
#include "pins.h"
#include "cipher.h"
#include <stdarg.h>
#include <stdint.h>
#include <errno.h>
Expand Down Expand Up @@ -71,32 +72,69 @@ static size_t write_data(char *ptr, size_t size, size_t nmemb, void *data)
return len;
}

static CURLcode pin_certificate(CURL *curl, void *sslctx, void *parm)
static char *hash_subject_pubkey_info(X509 *cert)
{
UNUSED(curl);
UNUSED(parm);
X509_STORE *store;
X509 *cert = NULL;
BIO *bio = NULL;
CURLcode ret = CURLE_SSL_CACERT;

store = X509_STORE_new();
if (!store)
goto out;

bio = BIO_new_mem_buf(CERTIFICATE_THAWTE, -1);
while ((cert = PEM_read_bio_X509(bio, NULL, 0, NULL))) {
if (!X509_STORE_add_cert(store, cert)) {
X509_free(cert);
goto out;
_cleanup_free_ unsigned char *spki = NULL;
char *hash = NULL;
EVP_PKEY *pkey;
int len;

pkey = X509_get_pubkey(cert);
if (!pkey)
return NULL;

len = i2d_PUBKEY(pkey, &spki);
if (len <= 0)
goto free_pkey;

hash = cipher_sha256_b64(spki, len);
free_pkey:
EVP_PKEY_free(pkey);
return hash;
}

static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx)
{
int i, j;

/*
* Preverify checks the platform's certificate store; don't
* allow any chain that doesn't already validate according to
* that.
*/
if (!preverify_ok)
return 0;

/* check each certificate in the chain against our built-in pinlist. */
STACK_OF(X509) *chain = X509_STORE_CTX_get_chain(ctx);
if (!chain)
die("No certificate chain available");

bool found = false;
for (i=0; i < sk_X509_num(chain); i++) {
_cleanup_free_ char *spki_hash = NULL;
spki_hash = hash_subject_pubkey_info(sk_X509_value(chain, i));
if (!spki_hash)
continue;

for (j=0; j < (int) ARRAY_SIZE(PK_PINS); j++) {
if (strcmp(PK_PINS[j], spki_hash) == 0) {
found = true;
break;
}
}
X509_free(cert);
}
SSL_CTX_set_cert_store((SSL_CTX *)sslctx, store);
ret = CURLE_OK;
out:
BIO_free(bio);
return ret;

return found;
}

static CURLcode pin_keys(CURL *curl, void *sslctx, void *parm)
{
UNUSED(curl);
UNUSED(parm);
SSL_CTX_set_verify((SSL_CTX *)sslctx, SSL_VERIFY_PEER,
verify_callback);
return CURLE_OK;
}

char *http_post_lastpass(const char *page, const char *session, size_t *final_len, ...)
Expand Down Expand Up @@ -162,7 +200,7 @@ char *http_post_lastpass_v(const char *page, const char *session, size_t *final_
#else
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1);
curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, pin_certificate);
curl_easy_setopt(curl, CURLOPT_SSL_CTX_FUNCTION, pin_keys);
#endif
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &result);
Expand Down
9 changes: 9 additions & 0 deletions pins.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#ifndef PINS_H
#define PINS_H
const char *PK_PINS[] = {
/* current lastpass.com primary (Thawte) */
"HXXQgxueCIU5TTLHob/bPbwcKOKw6DkfsTWYHbxbqTY=",
/* current lastpass.eu primary (AddTrust) */
"lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=",
};
#endif

0 comments on commit 28b4dc5

Please sign in to comment.