getdns/src/pubkey-pinning.c

468 lines
14 KiB
C

/**
*
* /brief functions for Public Key Pinning
*
*/
/*
* Copyright (c) 2015, Daniel Kahn Gillmor
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the names of the copyright holders nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL Verisign, Inc. BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* getdns Public Key Pinning
*
* a public key pinset is a list of dicts. each dict should have a
* "digest" and a "value".
*
* "digest": a string indicating the type of digest. at the moment, we
* only support a "digest" of "sha256".
*
* "value": a binary representation of the digest provided.
*
* given a such a pinset, we should be able to validate a chain
* properly according to section 2.6 of RFC 7469.
*/
#include "config.h"
#include "debug.h"
#include <getdns/getdns.h>
#include <openssl/evp.h>
#include <openssl/bio.h>
#include <openssl/sha.h>
#include <openssl/x509.h>
#include <string.h>
#include "context.h"
#include "util-internal.h"
#if OPENSSL_VERSION_NUMBER < 0x10100000 || defined(HAVE_LIBRESSL)
#define X509_STORE_CTX_get0_untrusted(store) store->untrusted
#endif
/* we only support sha256 at the moment. adding support for another
digest is more complex than just adding another entry here. in
particular, you'll probably need a match for a particular cert
against all supported algorithms. better to wait on doing that
until it is a better-understood problem (i.e. wait until hpkp is
updated and follow the guidance in rfc7469bis)
*/
static const getdns_bindata sha256 = {
.size = sizeof("sha256") - 1,
.data = (uint8_t*)"sha256"
};
#define PIN_PREFIX "pin-sha256=\""
#define PIN_PREFIX_LENGTH (sizeof(PIN_PREFIX) - 1)
/* b64 turns every 3 octets (or fraction thereof) into 4 octets */
#define B64_ENCODED_SHA256_LENGTH (((SHA256_DIGEST_LENGTH + 2)/3) * 4)
/* convert an HPKP-style pin description to an appropriate getdns data
structure. An example string is: (with the quotes, without any
leading or trailing whitespace):
pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="
getdns_build_pin_from_string returns a dict created from ctx, or
NULL if the string did not match. If ctx is NULL, the dict is
created via getdns_dict_create().
It is the caller's responsibility to call getdns_dict_destroy when
it is no longer needed.
*/
getdns_dict* getdns_pubkey_pin_create_from_string(
getdns_context* context,
const char* str)
{
BIO *bio = NULL;
size_t i;
uint8_t buf[SHA256_DIGEST_LENGTH];
char inbuf[B64_ENCODED_SHA256_LENGTH + 1];
getdns_bindata value = { .size = SHA256_DIGEST_LENGTH, .data = buf };
getdns_dict* out = NULL;
/* we only do sha256 right now, make sure this is well-formed */
if (!str || strncmp(PIN_PREFIX, str, PIN_PREFIX_LENGTH))
return NULL;
for (i = PIN_PREFIX_LENGTH; i < PIN_PREFIX_LENGTH + B64_ENCODED_SHA256_LENGTH - 1; i++)
if (!((str[i] >= 'a' && str[i] <= 'z') ||
(str[i] >= 'A' && str[i] <= 'Z') ||
(str[i] >= '0' && str[i] <= '9') ||
(str[i] == '+') || (str[i] == '/')))
return NULL;
if (str[i++] != '=')
return NULL;
if (str[i++] != '"')
return NULL;
if (str[i++] != '\0')
return NULL;
/* openssl needs a trailing newline to base64 decode */
memcpy(inbuf, str + PIN_PREFIX_LENGTH, B64_ENCODED_SHA256_LENGTH);
inbuf[B64_ENCODED_SHA256_LENGTH] = '\n';
bio = BIO_push(BIO_new(BIO_f_base64()),
BIO_new_mem_buf(inbuf, sizeof(inbuf)));
if (BIO_read(bio, buf, sizeof(buf)) != sizeof(buf))
goto fail;
if (context)
out = getdns_dict_create_with_context(context);
else
out = getdns_dict_create();
if (out == NULL)
goto fail;
if (getdns_dict_set_bindata(out, "digest", &sha256))
goto fail;
if (getdns_dict_set_bindata(out, "value", &value))
goto fail;
return out;
fail:
BIO_free_all(bio);
getdns_dict_destroy(out);
return NULL;
}
/* Test whether a given pinset is reasonable, including:
* is it well-formed?
* are there at least two pins?
* are the digests used sane?
if errorlist is NULL, the sanity check just returns success or
failure.
if errorlist is not NULL, we append human-readable strings to
report the errors.
*/
#define PKP_SC_ERR(e) { \
if (errorlist) \
_getdns_list_append_const_bindata(errorlist, \
sizeof(e), e); \
errorcount++; \
}
#define PKP_SC_HARDERR(e, val) { \
PKP_SC_ERR(e); return val; \
}
getdns_return_t getdns_pubkey_pinset_sanity_check(
const getdns_list* pinset,
getdns_list* errorlist)
{
size_t errorcount = 0, pins = 0, i;
getdns_dict * pin;
getdns_bindata * data;
if (getdns_list_get_length(pinset, &pins))
PKP_SC_HARDERR("Can't get length of pinset",
GETDNS_RETURN_INVALID_PARAMETER);
if (pins < 2)
PKP_SC_ERR("This pinset has fewer than 2 pins");
for (i = 0; i < pins; i++)
{
/* is it a dict? */
if (getdns_list_get_dict(pinset, i, &pin)) {
PKP_SC_ERR("Could not retrieve a pin");
} else {
/* does the pin have the right digest type? */
if (getdns_dict_get_bindata(pin, "digest", &data)) {
PKP_SC_ERR("Pin has no 'digest' entry");
} else {
if (data->size != sha256.size ||
memcmp(data->data, sha256.data, sha256.size))
PKP_SC_ERR("Pin has 'digest' other than sha256");
}
/* if it does, is the value the right length? */
if (getdns_dict_get_bindata(pin, "value", &data)) {
PKP_SC_ERR("Pin has no 'value' entry");
} else {
if (data->size != SHA256_DIGEST_LENGTH)
PKP_SC_ERR("Pin has the wrong size 'value' (should be 32 octets for sha256)");
}
/* should we choke if it has some other key? for
* extensibility, we will not treat this as an
* error.*/
}
}
if (errorcount > 0)
return GETDNS_RETURN_GENERIC_ERROR;
return GETDNS_RETURN_GOOD;
}
getdns_return_t
_getdns_get_pubkey_pinset_from_list(const getdns_list *pinset_list,
struct mem_funcs *mf,
sha256_pin_t **pinset_out)
{
getdns_return_t r;
size_t pins, i;
sha256_pin_t *out = NULL, *onext = NULL;
getdns_dict * pin;
getdns_bindata * data = NULL;
if (r = getdns_list_get_length(pinset_list, &pins), r)
return r;
for (i = 0; i < pins; i++)
{
if (r = getdns_list_get_dict(pinset_list, i, &pin), r)
goto fail;
/* does the pin have the right digest type? */
if (r = getdns_dict_get_bindata(pin, "digest", &data), r)
goto fail;
if (data->size != sha256.size ||
memcmp(data->data, sha256.data, sha256.size)) {
r = GETDNS_RETURN_INVALID_PARAMETER;
goto fail;
}
/* if it does, is the value the right length? */
if (r = getdns_dict_get_bindata(pin, "value", &data), r)
goto fail;
if (data->size != SHA256_DIGEST_LENGTH) {
r = GETDNS_RETURN_INVALID_PARAMETER;
goto fail;
}
/* make a new pin */
onext = GETDNS_MALLOC(*mf, sha256_pin_t);
if (onext == NULL) {
r = GETDNS_RETURN_MEMORY_ERROR;
goto fail;
}
onext->next = out;
memcpy(onext->pin, data->data, SHA256_DIGEST_LENGTH);
out = onext;
}
*pinset_out = out;
return GETDNS_RETURN_GOOD;
fail:
while (out) {
onext = out->next;
GETDNS_FREE(*mf, out);
out = onext;
}
return r;
}
getdns_return_t
_getdns_get_pubkey_pinset_list(getdns_context *ctx,
const sha256_pin_t *pinset_in,
getdns_list **pinset_list)
{
getdns_list *out = getdns_list_create_with_context(ctx);
getdns_return_t r;
uint8_t buf[SHA256_DIGEST_LENGTH];
getdns_bindata value = { .size = SHA256_DIGEST_LENGTH, .data = buf };
getdns_dict *pin = NULL;
if (out == NULL)
return GETDNS_RETURN_MEMORY_ERROR;
while (pinset_in) {
pin = getdns_dict_create_with_context(ctx);
if (pin == NULL) {
r = GETDNS_RETURN_MEMORY_ERROR;
goto fail;
}
if (r = getdns_dict_set_bindata(pin, "digest", &sha256), r)
goto fail;
memcpy(buf, pinset_in->pin, sizeof(buf));
if (r = getdns_dict_set_bindata(pin, "value", &value), r)
goto fail;
if (r = _getdns_list_append_this_dict(out, pin), r)
goto fail;
pin = NULL;
pinset_in = pinset_in->next;
}
*pinset_list = out;
return GETDNS_RETURN_GOOD;
fail:
getdns_dict_destroy(pin);
getdns_list_destroy(out);
return r;
}
/* this should only happen once ever in the life of the library. it's
used to associate a getdns_context_t with an SSL_CTX, to be able to
do custom verification.
see doc/HOWTO/proxy_certificates.txt as an example
*/
static int
#if OPENSSL_VERSION_NUMBER < 0x10100000 || defined(HAVE_LIBRESSL)
_get_ssl_getdns_upstream_idx(void)
#else
_get_ssl_getdns_upstream_idx(X509_STORE *store)
#endif
{
static volatile int idx = -1;
if (idx < 0) {
#if OPENSSL_VERSION_NUMBER < 0x10100000 || defined(HAVE_LIBRESSL)
CRYPTO_w_lock(CRYPTO_LOCK_X509_STORE);
#else
X509_STORE_lock(store);
#endif
if (idx < 0)
idx = SSL_get_ex_new_index(0, "associated getdns upstream",
NULL,NULL,NULL);
#if OPENSSL_VERSION_NUMBER < 0x10100000 || defined(HAVE_LIBRESSL)
CRYPTO_w_unlock(CRYPTO_LOCK_X509_STORE);
#else
X509_STORE_unlock(store);
#endif
}
return idx;
}
getdns_upstream*
_getdns_upstream_from_x509_store(X509_STORE_CTX *store)
{
#if OPENSSL_VERSION_NUMBER < 0x10100000 || defined(HAVE_LIBRESSL)
int uidx = _get_ssl_getdns_upstream_idx();
#else
int uidx = _get_ssl_getdns_upstream_idx(X509_STORE_CTX_get0_store(store));
#endif
int sslidx = SSL_get_ex_data_X509_STORE_CTX_idx();
const SSL *ssl;
/* all *_get_ex_data() should return NULL on failure anyway */
ssl = X509_STORE_CTX_get_ex_data(store, sslidx);
if (ssl)
return (getdns_upstream*) SSL_get_ex_data(ssl, uidx);
else
return NULL;
/* TODO: if we want more details about errors somehow, we
* might call ERR_get_error (see CRYPTO_set_ex_data(3ssl))*/
}
getdns_return_t
_getdns_associate_upstream_with_SSL(SSL *ssl,
getdns_upstream *upstream)
{
#if OPENSSL_VERSION_NUMBER < 0x10100000 || defined(HAVE_LIBRESSL)
int uidx = _get_ssl_getdns_upstream_idx();
#else
int uidx = _get_ssl_getdns_upstream_idx(SSL_CTX_get_cert_store(SSL_get_SSL_CTX(ssl)));
#endif
if (SSL_set_ex_data(ssl, uidx, upstream))
return GETDNS_RETURN_GOOD;
else
return GETDNS_RETURN_GENERIC_ERROR;
/* TODO: if we want more details about errors somehow, we
* might call ERR_get_error (see CRYPTO_set_ex_data(3ssl))*/
}
getdns_return_t
_getdns_verify_pinset_match(const sha256_pin_t *pinset,
X509_STORE_CTX *store)
{
getdns_return_t ret = GETDNS_RETURN_GENERIC_ERROR;
X509 *x, *prev;
int i, len;
unsigned char raw[4096];
unsigned char *next;
unsigned char buf[sizeof(pinset->pin)];
const sha256_pin_t *p;
if (pinset == NULL || store == NULL)
return GETDNS_RETURN_GENERIC_ERROR;
/* start at the base of the chain (the end-entity cert) and
* make sure that some valid element of the chain does match
* the pinset. */
/* Testing with OpenSSL 1.0.1e-1 on debian indicates that
* store->untrusted holds the chain offered by the server in
* the order that the server offers it. If the server offers
* bogus certificates (that is, matching and valid certs that
* belong to private keys that the server does not control),
* the the verification will succeed (including this pinset
* check), but the handshake will fail outside of this
* verification. */
/* TODO: how do we handle raw public keys? */
for (i = 0; i < sk_X509_num(X509_STORE_CTX_get0_untrusted(store)); i++, prev = x) {
x = sk_X509_value(X509_STORE_CTX_get0_untrusted(store), i);
#if defined(STUB_DEBUG) && STUB_DEBUG
DEBUG_STUB("%s %-35s: Name of cert: %d ",
STUB_DEBUG_SETUP_TLS, __FUNC__, i);
X509_NAME_print_ex_fp(stderr, X509_get_subject_name(x), 1, XN_FLAG_ONELINE);
fprintf(stderr, "\n");
#endif
if (i > 0) {
/* we ensure that "prev" is signed by "x" */
EVP_PKEY *pkey = X509_get_pubkey(x);
int verified;
if (!pkey) {
DEBUG_STUB("%s %-35s: Could not get pubkey from cert %d (%p)\n",
STUB_DEBUG_SETUP_TLS, __FUNC__, i, (void*)x);
return GETDNS_RETURN_GENERIC_ERROR;
}
verified = X509_verify(prev, pkey);
EVP_PKEY_free(pkey);
if (!verified) {
DEBUG_STUB("%s %-35s: cert %d (%p) was not signed by cert %d\n",
STUB_DEBUG_SETUP_TLS, __FUNC__, i-1, (void*)prev, i);
return GETDNS_RETURN_GENERIC_ERROR;
}
}
/* digest the cert with sha256 */
len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(x), NULL);
if (len > (int)sizeof(raw)) {
DEBUG_STUB("%s %-35s: Pubkey %d is larger than "PRIsz" octets\n",
STUB_DEBUG_SETUP_TLS, __FUNC__, i, sizeof(raw));
continue;
}
next = raw;
i2d_X509_PUBKEY(X509_get_X509_PUBKEY(x), &next);
if (next - raw != len) {
DEBUG_STUB("%s %-35s: Pubkey %d claimed it needed %d octets, really needed "PRIsz"\n",
STUB_DEBUG_SETUP_TLS, __FUNC__, i, len, next - raw);
continue;
}
SHA256(raw, len, buf);
/* compare it */
for (p = pinset; p; p = p->next)
if (0 == memcmp(buf, p->pin, sizeof(p->pin))) {
DEBUG_STUB("%s %-35s: Pubkey %d matched pin %p ("PRIsz")\n",
STUB_DEBUG_SETUP_TLS, __FUNC__, i, (void*)p, sizeof(p->pin));
return GETDNS_RETURN_GOOD;
} else
DEBUG_STUB("%s %-35s: Pubkey %d did not match pin %p\n",
STUB_DEBUG_SETUP_TLS, __FUNC__, i, (void*)p);
}
return ret;
}
/* pubkey-pinning.c */