/** * * /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 #include #include #include #include #include #include "context.h" /* 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; int 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) { \ err.size = sizeof(e); \ err.data = (uint8_t*)e; \ if (errorlist) \ getdns_list_set_bindata(errorlist, \ preverrs + errorcount, &err); \ 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, preverrs = 0, pins = 0, i; getdns_bindata err; getdns_dict * pin; getdns_bindata * data; if (errorlist) if (getdns_list_get_length(errorlist, &preverrs)) return GETDNS_RETURN_INVALID_PARAMETER; 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; size_t idx = 0; 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_set_dict(out, idx++, pin), r) goto fail; getdns_dict_destroy(pin); 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 _get_ssl_getdns_upstream_idx() { static volatile int idx = -1; if (idx < 0) { CRYPTO_w_lock(CRYPTO_LOCK_X509_STORE); if (idx < 0) idx = SSL_get_ex_new_index(0, "associated getdns upstream", NULL,NULL,NULL); CRYPTO_w_unlock(CRYPTO_LOCK_X509_STORE); } return idx; } getdns_upstream* _getdns_upstream_from_x509_store(X509_STORE_CTX *store) { int uidx = _get_ssl_getdns_upstream_idx(); 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) { int uidx = _get_ssl_getdns_upstream_idx(); 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; int i, len; unsigned char raw[4096]; unsigned char *next = raw; 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(store->untrusted); i++) { if (i > 0) { /* TODO: how do we ensure that the certificates in * each stage appropriately sign the previous one? * for now, to be safe, we only examine the end-entity * cert: */ return GETDNS_RETURN_GENERIC_ERROR; } x = sk_X509_value(store->untrusted, i); if (x->cert_info == NULL) continue; #if defined(STUB_DEBUG) && STUB_DEBUG DEBUG_STUB("%s %-30s: Name of cert: %d ", STUB_DEBUG_SETUP_TLS, __FUNCTION__, i); if (x->cert_info->subject != NULL) X509_NAME_print_ex_fp(stderr, x->cert_info->subject, 1, XN_FLAG_ONELINE); fprintf(stderr, "\n"); #endif if (x->cert_info->key == NULL) continue; /* digest the cert with sha256 */ len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(x), NULL); if (len > sizeof(raw)) { DEBUG_STUB("%s %-30s: Pubkey %d is larger than "PRIsz" octets\n", STUB_DEBUG_SETUP_TLS, __FUNCTION__, i, sizeof(raw)); continue; } i2d_X509_PUBKEY(X509_get_X509_PUBKEY(x), &next); if (next - raw != len) { DEBUG_STUB("%s %-30s: Pubkey %d claimed it needed %d octets, really needed "PRIsz"\n", STUB_DEBUG_SETUP_TLS, __FUNCTION__, 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 %-30s: Pubkey %d matched pin %p ("PRIsz")\n", STUB_DEBUG_SETUP_TLS, __FUNCTION__, i, p, sizeof(p->pin)); return GETDNS_RETURN_GOOD; } else DEBUG_STUB("%s %-30s: Pubkey %d did not match pin %p\n", STUB_DEBUG_SETUP_TLS, __FUNCTION__, i, p); } return ret; } /* pubkey-pinning.c */