aboutsummaryrefslogtreecommitdiff
path: root/external/unbound/dnscrypt/dnscrypt.c
diff options
context:
space:
mode:
Diffstat (limited to 'external/unbound/dnscrypt/dnscrypt.c')
-rw-r--r--external/unbound/dnscrypt/dnscrypt.c531
1 files changed, 531 insertions, 0 deletions
diff --git a/external/unbound/dnscrypt/dnscrypt.c b/external/unbound/dnscrypt/dnscrypt.c
new file mode 100644
index 000000000..56903e651
--- /dev/null
+++ b/external/unbound/dnscrypt/dnscrypt.c
@@ -0,0 +1,531 @@
+
+#include "config.h"
+#include <stdlib.h>
+#include <fcntl.h>
+#ifdef HAVE_TIME_H
+#include <time.h>
+#endif
+#include <sys/time.h>
+#include <sys/types.h>
+#include "sldns/sbuffer.h"
+#include "util/config_file.h"
+#include "util/net_help.h"
+#include "util/netevent.h"
+#include "util/log.h"
+
+#include "dnscrypt/cert.h"
+#include "dnscrypt/dnscrypt.h"
+
+#include <ctype.h>
+
+/**
+ * \file
+ * dnscrypt functions for encrypting DNS packets.
+ */
+
+#define DNSCRYPT_QUERY_BOX_OFFSET \
+ (DNSCRYPT_MAGIC_HEADER_LEN + crypto_box_PUBLICKEYBYTES + crypto_box_HALF_NONCEBYTES)
+
+// 8 bytes: magic header (CERT_MAGIC_HEADER)
+// 12 bytes: the client's nonce
+// 12 bytes: server nonce extension
+// 16 bytes: Poly1305 MAC (crypto_box_ZEROBYTES - crypto_box_BOXZEROBYTES)
+
+#define DNSCRYPT_REPLY_BOX_OFFSET \
+ (DNSCRYPT_MAGIC_HEADER_LEN + crypto_box_HALF_NONCEBYTES + crypto_box_HALF_NONCEBYTES)
+
+/**
+ * Decrypt a query using the keypair that was found using dnsc_find_keypair.
+ * The client nonce will be extracted from the encrypted query and stored in
+ * client_nonce, a shared secret will be computed and stored in nmkey and the
+ * buffer will be decrypted inplace.
+ * \param[in] keypair the keypair that matches this encrypted query.
+ * \param[in] client_nonce where the client nonce will be stored.
+ * \param[in] nmkey where the shared secret key will be written.
+ * \param[in] buffer the encrypted buffer.
+ * \return 0 on success.
+ */
+static int
+dnscrypt_server_uncurve(const KeyPair *keypair,
+ uint8_t client_nonce[crypto_box_HALF_NONCEBYTES],
+ uint8_t nmkey[crypto_box_BEFORENMBYTES],
+ struct sldns_buffer* buffer)
+{
+ size_t len = sldns_buffer_limit(buffer);
+ uint8_t *const buf = sldns_buffer_begin(buffer);
+ uint8_t nonce[crypto_box_NONCEBYTES];
+ struct dnscrypt_query_header *query_header;
+
+ if (len <= DNSCRYPT_QUERY_HEADER_SIZE) {
+ return -1;
+ }
+
+ query_header = (struct dnscrypt_query_header *)buf;
+ memcpy(nmkey, query_header->publickey, crypto_box_PUBLICKEYBYTES);
+ if (crypto_box_beforenm(nmkey, nmkey, keypair->crypt_secretkey) != 0) {
+ return -1;
+ }
+
+ memcpy(nonce, query_header->nonce, crypto_box_HALF_NONCEBYTES);
+ memset(nonce + crypto_box_HALF_NONCEBYTES, 0, crypto_box_HALF_NONCEBYTES);
+
+ sldns_buffer_set_at(buffer,
+ DNSCRYPT_QUERY_BOX_OFFSET - crypto_box_BOXZEROBYTES,
+ 0, crypto_box_BOXZEROBYTES);
+
+ if (crypto_box_open_afternm
+ (buf + DNSCRYPT_QUERY_BOX_OFFSET - crypto_box_BOXZEROBYTES,
+ buf + DNSCRYPT_QUERY_BOX_OFFSET - crypto_box_BOXZEROBYTES,
+ len - DNSCRYPT_QUERY_BOX_OFFSET + crypto_box_BOXZEROBYTES, nonce,
+ nmkey) != 0) {
+ return -1;
+ }
+
+ while (*sldns_buffer_at(buffer, --len) == 0)
+ ;
+
+ if (*sldns_buffer_at(buffer, len) != 0x80) {
+ return -1;
+ }
+
+ memcpy(client_nonce, nonce, crypto_box_HALF_NONCEBYTES);
+ memmove(sldns_buffer_begin(buffer),
+ sldns_buffer_at(buffer, DNSCRYPT_QUERY_HEADER_SIZE),
+ len - DNSCRYPT_QUERY_HEADER_SIZE);
+
+ sldns_buffer_set_position(buffer, 0);
+ sldns_buffer_set_limit(buffer, len - DNSCRYPT_QUERY_HEADER_SIZE);
+
+ return 0;
+}
+
+
+/**
+ * Add random padding to a buffer, according to a client nonce.
+ * The length has to depend on the query in order to avoid reply attacks.
+ *
+ * @param buf a buffer
+ * @param len the initial size of the buffer
+ * @param max_len the maximum size
+ * @param nonce a nonce, made of the client nonce repeated twice
+ * @param secretkey
+ * @return the new size, after padding
+ */
+size_t
+dnscrypt_pad(uint8_t *buf, const size_t len, const size_t max_len,
+ const uint8_t *nonce, const uint8_t *secretkey)
+{
+ uint8_t *buf_padding_area = buf + len;
+ size_t padded_len;
+ uint32_t rnd;
+
+ // no padding
+ if (max_len < len + DNSCRYPT_MIN_PAD_LEN)
+ return len;
+
+ assert(nonce[crypto_box_HALF_NONCEBYTES] == nonce[0]);
+
+ crypto_stream((unsigned char *)&rnd, (unsigned long long)sizeof(rnd), nonce,
+ secretkey);
+ padded_len =
+ len + DNSCRYPT_MIN_PAD_LEN + rnd % (max_len - len -
+ DNSCRYPT_MIN_PAD_LEN + 1);
+ padded_len += DNSCRYPT_BLOCK_SIZE - padded_len % DNSCRYPT_BLOCK_SIZE;
+ if (padded_len > max_len)
+ padded_len = max_len;
+
+ memset(buf_padding_area, 0, padded_len - len);
+ *buf_padding_area = 0x80;
+
+ return padded_len;
+}
+
+uint64_t
+dnscrypt_hrtime(void)
+{
+ struct timeval tv;
+ uint64_t ts = (uint64_t)0U;
+ int ret;
+
+ ret = gettimeofday(&tv, NULL);
+ if (ret == 0) {
+ ts = (uint64_t)tv.tv_sec * 1000000U + (uint64_t)tv.tv_usec;
+ } else {
+ log_err("gettimeofday: %s", strerror(errno));
+ }
+ return ts;
+}
+
+/**
+ * Add the server nonce part to once.
+ * The nonce is made half of client nonce and the seconf half of the server
+ * nonce, both of them of size crypto_box_HALF_NONCEBYTES.
+ * \param[in] nonce: a uint8_t* of size crypto_box_NONCEBYTES
+ */
+static void
+add_server_nonce(uint8_t *nonce)
+{
+ uint64_t ts;
+ uint64_t tsn;
+ uint32_t suffix;
+ ts = dnscrypt_hrtime();
+ // TODO? dnscrypt-wrapper does some logic with context->nonce_ts_last
+ // unclear if we really need it, so skipping it for now.
+ tsn = (ts << 10) | (randombytes_random() & 0x3ff);
+#if (BYTE_ORDER == LITTLE_ENDIAN)
+ tsn =
+ (((uint64_t)htonl((uint32_t)tsn)) << 32) | htonl((uint32_t)(tsn >> 32));
+#endif
+ memcpy(nonce + crypto_box_HALF_NONCEBYTES, &tsn, 8);
+ suffix = randombytes_random();
+ memcpy(nonce + crypto_box_HALF_NONCEBYTES + 8, &suffix, 4);
+}
+
+/**
+ * Encrypt a reply using the keypair that was used with the query.
+ * The client nonce will be extracted from the encrypted query and stored in
+ * The buffer will be encrypted inplace.
+ * \param[in] keypair the keypair that matches this encrypted query.
+ * \param[in] client_nonce client nonce used during the query
+ * \param[in] nmkey shared secret key used during the query.
+ * \param[in] buffer the buffer where to encrypt the reply.
+ * \param[in] udp if whether or not it is a UDP query.
+ * \param[in] max_udp_size configured max udp size.
+ * \return 0 on success.
+ */
+static int
+dnscrypt_server_curve(const KeyPair *keypair,
+ uint8_t client_nonce[crypto_box_HALF_NONCEBYTES],
+ uint8_t nmkey[crypto_box_BEFORENMBYTES],
+ struct sldns_buffer* buffer,
+ uint8_t udp,
+ size_t max_udp_size)
+{
+ size_t dns_reply_len = sldns_buffer_limit(buffer);
+ size_t max_len = dns_reply_len + DNSCRYPT_MAX_PADDING + DNSCRYPT_REPLY_HEADER_SIZE;
+ size_t max_reply_size = max_udp_size - 20U - 8U;
+ uint8_t nonce[crypto_box_NONCEBYTES];
+ uint8_t *boxed;
+ uint8_t *const buf = sldns_buffer_begin(buffer);
+ size_t len = sldns_buffer_limit(buffer);
+
+ if(udp){
+ if (max_len > max_reply_size)
+ max_len = max_reply_size;
+ }
+
+
+ memcpy(nonce, client_nonce, crypto_box_HALF_NONCEBYTES);
+ memcpy(nonce + crypto_box_HALF_NONCEBYTES, client_nonce,
+ crypto_box_HALF_NONCEBYTES);
+
+ boxed = buf + DNSCRYPT_REPLY_BOX_OFFSET;
+ memmove(boxed + crypto_box_MACBYTES, buf, len);
+ len = dnscrypt_pad(boxed + crypto_box_MACBYTES, len,
+ max_len - DNSCRYPT_REPLY_HEADER_SIZE, nonce,
+ keypair->crypt_secretkey);
+ sldns_buffer_set_at(buffer,
+ DNSCRYPT_REPLY_BOX_OFFSET - crypto_box_BOXZEROBYTES,
+ 0, crypto_box_ZEROBYTES);
+
+ // add server nonce extension
+ add_server_nonce(nonce);
+
+ if (crypto_box_afternm
+ (boxed - crypto_box_BOXZEROBYTES, boxed - crypto_box_BOXZEROBYTES,
+ len + crypto_box_ZEROBYTES, nonce, nmkey) != 0) {
+ return -1;
+ }
+
+ sldns_buffer_write_at(buffer, 0, DNSCRYPT_MAGIC_RESPONSE, DNSCRYPT_MAGIC_HEADER_LEN);
+ sldns_buffer_write_at(buffer, DNSCRYPT_MAGIC_HEADER_LEN, nonce, crypto_box_NONCEBYTES);
+ sldns_buffer_set_limit(buffer, len + DNSCRYPT_REPLY_HEADER_SIZE);
+ return 0;
+}
+
+/**
+ * Read the content of fname into buf.
+ * \param[in] fname name of the file to read.
+ * \param[in] buf the buffer in which to read the content of the file.
+ * \param[in] count number of bytes to read.
+ * \return 0 on success.
+ */
+static int
+dnsc_read_from_file(char *fname, char *buf, size_t count)
+{
+ int fd;
+ fd = open(fname, O_RDONLY);
+ if (fd == -1) {
+ return -1;
+ }
+ if (read(fd, buf, count) != (ssize_t)count) {
+ close(fd);
+ return -2;
+ }
+ close(fd);
+ return 0;
+}
+
+/**
+ * Parse certificates files provided by the configuration and load them into
+ * dnsc_env.
+ * \param[in] env the dnsc_env structure to load the certs into.
+ * \param[in] cfg the configuration.
+ * \return the number of certificates loaded.
+ */
+static int
+dnsc_parse_certs(struct dnsc_env *env, struct config_file *cfg)
+{
+ struct config_strlist *head;
+ size_t signed_cert_id;
+
+ env->signed_certs_count = 0U;
+ for (head = cfg->dnscrypt_provider_cert; head; head = head->next) {
+ env->signed_certs_count++;
+ }
+ env->signed_certs = sodium_allocarray(env->signed_certs_count,
+ sizeof *env->signed_certs);
+
+ signed_cert_id = 0U;
+ for(head = cfg->dnscrypt_provider_cert; head; head = head->next, signed_cert_id++) {
+ if(dnsc_read_from_file(
+ head->str,
+ (char *)(env->signed_certs + signed_cert_id),
+ sizeof(struct SignedCert)) != 0) {
+ fatal_exit("dnsc_parse_certs: failed to load %s: %s", head->str, strerror(errno));
+ }
+ verbose(VERB_OPS, "Loaded cert %s", head->str);
+ }
+ return signed_cert_id;
+}
+
+/**
+ * Helper function to convert a binary key into a printable fingerprint.
+ * \param[in] fingerprint the buffer in which to write the printable key.
+ * \param[in] key the key to convert.
+ */
+void
+dnsc_key_to_fingerprint(char fingerprint[80U], const uint8_t * const key)
+{
+ const size_t fingerprint_size = 80U;
+ size_t fingerprint_pos = (size_t) 0U;
+ size_t key_pos = (size_t) 0U;
+
+ for (;;) {
+ assert(fingerprint_size > fingerprint_pos);
+ snprintf(&fingerprint[fingerprint_pos],
+ fingerprint_size - fingerprint_pos, "%02X%02X",
+ key[key_pos], key[key_pos + 1U]);
+ key_pos += 2U;
+ if (key_pos >= crypto_box_PUBLICKEYBYTES) {
+ break;
+ }
+ fingerprint[fingerprint_pos + 4U] = ':';
+ fingerprint_pos += 5U;
+ }
+}
+
+/**
+ * Find the keypair matching a DNSCrypt query.
+ * \param[in] dnscenv The DNSCrypt enviroment, which contains the list of keys
+ * supported by the server.
+ * \param[in] buffer The encrypted DNS query.
+ * \return a KeyPair * if we found a key pair matching the query, NULL otherwise.
+ */
+static const KeyPair *
+dnsc_find_keypair(struct dnsc_env* dnscenv, struct sldns_buffer* buffer)
+{
+ const KeyPair *keypairs = dnscenv->keypairs;
+ struct dnscrypt_query_header *dnscrypt_header;
+ size_t i;
+
+ if (sldns_buffer_limit(buffer) < DNSCRYPT_QUERY_HEADER_SIZE) {
+ return NULL;
+ }
+ dnscrypt_header = (struct dnscrypt_query_header *)sldns_buffer_begin(buffer);
+ for (i = 0U; i < dnscenv->keypairs_count; i++) {
+ if (memcmp(keypairs[i].crypt_publickey, dnscrypt_header->magic_query,
+ DNSCRYPT_MAGIC_HEADER_LEN) == 0) {
+ return &keypairs[i];
+ }
+ }
+ return NULL;
+}
+
+/**
+ * Insert local-zone and local-data into configuration.
+ * In order to be able to serve certs over TXT, we can reuse the local-zone and
+ * local-data config option. The zone and qname are infered from the
+ * provider_name and the content of the TXT record from the certificate content.
+ * returns the number of certtificate TXT record that were loaded.
+ * < 0 in case of error.
+ */
+static int
+dnsc_load_local_data(struct dnsc_env* dnscenv, struct config_file *cfg)
+{
+ size_t i, j;
+ // Insert 'local-zone: "2.dnscrypt-cert.example.com" deny'
+ if(!cfg_str2list_insert(&cfg->local_zones,
+ strdup(dnscenv->provider_name),
+ strdup("deny"))) {
+ log_err("Could not load dnscrypt local-zone: %s deny",
+ dnscenv->provider_name);
+ return -1;
+ }
+
+ // Add local data entry of type:
+ // 2.dnscrypt-cert.example.com 86400 IN TXT "DNSC......"
+ for(i=0; i<dnscenv->signed_certs_count; i++) {
+ const char *ttl_class_type = " 86400 IN TXT \"";
+ struct SignedCert *cert = dnscenv->signed_certs + i;
+ uint16_t rrlen = strlen(dnscenv->provider_name) +
+ strlen(ttl_class_type) +
+ 4 * sizeof(struct SignedCert) + // worst case scenario
+ 1 + // trailing double quote
+ 1;
+ char *rr = malloc(rrlen);
+ if(!rr) {
+ log_err("Could not allocate memory");
+ return -2;
+ }
+ snprintf(rr, rrlen - 1, "%s 86400 IN TXT \"", dnscenv->provider_name);
+ for(j=0; j<sizeof(struct SignedCert); j++) {
+ int c = (int)*((const uint8_t *) cert + j);
+ if (isprint(c) && c != '"' && c != '\\') {
+ snprintf(rr + strlen(rr), rrlen - 1 - strlen(rr), "%c", c);
+ } else {
+ snprintf(rr + strlen(rr), rrlen - 1 - strlen(rr), "\\%03d", c);
+ }
+ }
+ snprintf(rr + strlen(rr), rrlen - 1 - strlen(rr), "\"");
+ cfg_strlist_insert(&cfg->local_data, strdup(rr));
+ free(rr);
+ }
+ return dnscenv->signed_certs_count;
+}
+
+/**
+ * Parse the secret key files from `dnscrypt-secret-key` config and populates
+ * a list of secret/public keys supported by dnscrypt listener.
+ * \param[in] env The dnsc_env structure which will hold the keypairs.
+ * \param[in] cfg The config with the secret key file paths.
+ */
+static int
+dnsc_parse_keys(struct dnsc_env *env, struct config_file *cfg)
+{
+ struct config_strlist *head;
+ size_t keypair_id;
+
+ env->keypairs_count = 0U;
+ for (head = cfg->dnscrypt_secret_key; head; head = head->next) {
+ env->keypairs_count++;
+ }
+ env->keypairs = sodium_allocarray(env->keypairs_count,
+ sizeof *env->keypairs);
+
+ keypair_id = 0U;
+ for(head = cfg->dnscrypt_secret_key; head; head = head->next, keypair_id++) {
+ char fingerprint[80];
+ if(dnsc_read_from_file(
+ head->str,
+ (char *)(env->keypairs[keypair_id].crypt_secretkey),
+ crypto_box_SECRETKEYBYTES) != 0) {
+ fatal_exit("dnsc_parse_keys: failed to load %s: %s", head->str, strerror(errno));
+ }
+ verbose(VERB_OPS, "Loaded key %s", head->str);
+ if (crypto_scalarmult_base(env->keypairs[keypair_id].crypt_publickey,
+ env->keypairs[keypair_id].crypt_secretkey) != 0) {
+ fatal_exit("dnsc_parse_keys: could not generate public key from %s", head->str);
+ }
+ dnsc_key_to_fingerprint(fingerprint, env->keypairs[keypair_id].crypt_publickey);
+ verbose(VERB_OPS, "Crypt public key fingerprint for %s: %s", head->str, fingerprint);
+ }
+ return keypair_id;
+}
+
+
+/**
+ * #########################################################
+ * ############# Publicly accessible functions #############
+ * #########################################################
+ */
+
+int
+dnsc_handle_curved_request(struct dnsc_env* dnscenv,
+ struct comm_reply* repinfo)
+{
+ struct comm_point* c = repinfo->c;
+
+ repinfo->is_dnscrypted = 0;
+ if( !c->dnscrypt ) {
+ return 1;
+ }
+ // Attempt to decrypt the query. If it is not crypted, we may still need
+ // to serve the certificate.
+ verbose(VERB_ALGO, "handle request called on DNSCrypt socket");
+ if ((repinfo->keypair = dnsc_find_keypair(dnscenv, c->buffer)) != NULL) {
+ if(dnscrypt_server_uncurve(repinfo->keypair,
+ repinfo->client_nonce,
+ repinfo->nmkey,
+ c->buffer) != 0){
+ verbose(VERB_ALGO, "dnscrypt: Failed to uncurve");
+ comm_point_drop_reply(repinfo);
+ return 0;
+ }
+ repinfo->is_dnscrypted = 1;
+ sldns_buffer_rewind(c->buffer);
+ }
+ return 1;
+}
+
+int
+dnsc_handle_uncurved_request(struct comm_reply *repinfo)
+{
+ if(!repinfo->c->dnscrypt) {
+ return 1;
+ }
+ sldns_buffer_copy(repinfo->c->dnscrypt_buffer, repinfo->c->buffer);
+ if(!repinfo->is_dnscrypted) {
+ return 1;
+ }
+ if(dnscrypt_server_curve(repinfo->keypair,
+ repinfo->client_nonce,
+ repinfo->nmkey,
+ repinfo->c->dnscrypt_buffer,
+ repinfo->c->type == comm_udp,
+ repinfo->max_udp_size) != 0){
+ verbose(VERB_ALGO, "dnscrypt: Failed to curve cached missed answer");
+ comm_point_drop_reply(repinfo);
+ return 0;
+ }
+ return 1;
+}
+
+struct dnsc_env *
+dnsc_create(void)
+{
+ struct dnsc_env *env;
+ if (sodium_init() == -1) {
+ fatal_exit("dnsc_create: could not initialize libsodium.");
+ }
+ env = (struct dnsc_env *) calloc(1, sizeof(struct dnsc_env));
+ return env;
+}
+
+int
+dnsc_apply_cfg(struct dnsc_env *env, struct config_file *cfg)
+{
+ if(dnsc_parse_certs(env, cfg) <= 0) {
+ fatal_exit("dnsc_apply_cfg: no cert file loaded");
+ }
+ if(dnsc_parse_keys(env, cfg) <= 0) {
+ fatal_exit("dnsc_apply_cfg: no key file loaded");
+ }
+ randombytes_buf(env->hash_key, sizeof env->hash_key);
+ env->provider_name = cfg->dnscrypt_provider;
+
+ if(dnsc_load_local_data(env, cfg) <= 0) {
+ fatal_exit("dnsc_apply_cfg: could not load local data");
+ }
+ return 0;
+}