]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
Use ephemeral X.509 credentials if none are configured
authorDaniel Kahn Gillmor <dkg@fifthhorseman.net>
Sat, 12 Nov 2016 23:09:25 +0000 (08:09 +0900)
committerOndřej Surý <ondrej@sury.org>
Fri, 6 Jan 2017 09:30:38 +0000 (10:30 +0100)
If kresd is configured to listen using TLS, but it has no credentials,
it should fall back to generating ephemeral credentials and using
them.

It stores the ephemerally-generated secret key in the same directory
as the cache, using the name "ephemeral_key.pem".  If the cache
persists, then the key will too, even if the daemon dies.  This means
that any set of daemons that share a cache will also share an
ephemeral secret key.

The ephemeral X.509 certificate that corresponds to the key will be
automatically generated (self-signed), will have a lifetime of about
90 days (matching Let's Encrypt policy).  The ephemeral cert is
never written to disk; it is always dynamically-generated by kresd.

This should make it very easy to get DNS-over-TLS working in
opportunistic mode.

daemon/daemon.mk
daemon/tls.c
daemon/tls.h
daemon/tls_ephemeral_credentials.c [new file with mode: 0644]

index be21698ebfb564a3ad3a7555b285ff38c2c80f4b..9402a793c9102fa15cf4e676f3fbb20a0a39b52a 100644 (file)
@@ -6,6 +6,7 @@ kresd_SOURCES := \
        daemon/bindings.c    \
        daemon/ffimodule.c   \
        daemon/tls.c         \
+       daemon/tls_ephemeral_credentials.c \
        daemon/main.c
 
 kresd_DIST := daemon/lua/kres.lua daemon/lua/kres-gen.lua daemon/lua/trust_anchors.lua
index 4ecf84e5fa0b92e5fe2abbb940f6411ffc71e127..a10cdaba0787d6d15bd2eb9bf8d7b72a094a407d 100644 (file)
@@ -120,8 +120,13 @@ struct tls_ctx_t *tls_new(struct worker_ctx *worker)
 
        struct network *net = &worker->engine->net;
        if (!net->tls_credentials) {
-               kr_log_error("[tls] x509 credentials are missing; no TLS\n");
-               return NULL;
+               net->tls_credentials = tls_get_ephemeral_credentials(worker->engine);
+               if (!net->tls_credentials) {
+                       kr_log_error("[tls] X.509 credentials are missing, and ephemeral credentials failed; no TLS\n");
+                       return NULL;
+               }
+               kr_log_error("[tls] Using ephemeral TLS credentials:\n");
+               tls_credentials_log_pins(net->tls_credentials);
        }
 
        time_t now = time(NULL);
@@ -487,6 +492,9 @@ void tls_credentials_free(struct tls_credentials *tls_credentials) {
        if (tls_credentials->tls_key) {
                free(tls_credentials->tls_key);
        }
+       if (tls_credentials->ephemeral_servicename) {
+               free(tls_credentials->ephemeral_servicename);
+       }
        free(tls_credentials);
 }
 
index 69c46bee538f7dd7e05a5cbda17dea71134e7b18..c0d1836c8d8084e433954750046b6bcffedc69d7 100644 (file)
@@ -31,6 +31,7 @@ struct tls_credentials {
        char *tls_key;
        gnutls_certificate_credentials_t credentials;
        time_t valid_until;
+       char *ephemeral_servicename;
 };
 
 /*! Toggle verbose logging from TLS context. */
@@ -63,3 +64,6 @@ void tls_credentials_free(struct tls_credentials *tls_credentials);
 /*! Log DNS-over-TLS OOB key-pin form of current credentials:
  * https://tools.ietf.org/html/rfc7858#appendix-A */
 void tls_credentials_log_pins(struct tls_credentials *tls_credentials);
+
+/*! Generate new ephemeral TLS credentials. */
+struct tls_credentials * tls_get_ephemeral_credentials(struct engine *engine);
diff --git a/daemon/tls_ephemeral_credentials.c b/daemon/tls_ephemeral_credentials.c
new file mode 100644 (file)
index 0000000..bb71154
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2016 American Civil Liberties Union (ACLU)
+ * 
+ * Initial Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+ * 
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <sys/file.h>
+#include <unistd.h>
+#include <gnutls/gnutls.h>
+#include <gnutls/x509.h>
+#include <gnutls/crypto.h>
+
+#include "daemon/worker.h"
+#include "daemon/tls.h"
+
+#define EPHEMERAL_PRIVKEY_FILENAME "ephemeral_key.pem"
+#define INVALID_HOSTNAME "dns-over-tls.invalid"
+#define EPHEMERAL_CERT_EXPIRATION_SECONDS 60*60*24*90
+
+/* This is an attempt to grab an exclusive, advisory, non-blocking
+ * lock based on a filename.  At the moment it's POSIX-only, but it
+ * should be abstract enough of an interface to make an implementation
+ * for non-posix systems if anyone cares. */
+typedef int lock;
+static bool _lock_is_invalid(lock lock)
+{
+       return lock == -1;
+}
+/* a blocking lock on a given filename */
+static lock _lock_filename(const char *fname)
+{
+       lock lockfd = open(fname, O_RDONLY|O_CREAT, 0400);
+       if (lockfd == -1)
+               return lockfd;
+       /* this should be a non-blocking lock */
+       if (flock(lockfd, LOCK_EX | LOCK_NB) != 0) {
+               close(lockfd);
+               return -1;
+       }
+       return lockfd; /* for cleanup later */
+}
+static void _lock_unlock(lock *lock, const char *fname)
+{
+       if (lock && !_lock_is_invalid(*lock)) {
+               flock(*lock, LOCK_UN);
+               close(*lock);
+               *lock = -1;
+               unlink(fname); /* ignore errors */
+       }
+}
+
+static gnutls_x509_privkey_t get_ephemeral_privkey ()
+{
+       gnutls_x509_privkey_t privkey = NULL;
+       int err;
+       gnutls_datum_t data = { .data = NULL, .size = 0 };
+       lock lock;
+       int datafd = -1;
+
+       /* Take a lock to ensure that two daemons started concurrently
+        * with a shared cache don't both create the same privkey: */
+       lock = _lock_filename(EPHEMERAL_PRIVKEY_FILENAME ".lock");
+       if (_lock_is_invalid(lock)) {
+               kr_log_error("[tls] unable to lock lockfile " EPHEMERAL_PRIVKEY_FILENAME ".lock\n");
+               goto done;
+       }
+       
+       if ((err = gnutls_x509_privkey_init (&privkey)) < 0) {
+               kr_log_error("[tls] gnutls_x509_privkey_init() failed: %d (%s)\n",
+                            err, gnutls_strerror_name(err));
+               goto done;
+       }
+
+       /* read from cache file (we assume that we've chdir'ed
+        * already, so we're just looking for the file in the
+        * cachedir. */
+       datafd = open(EPHEMERAL_PRIVKEY_FILENAME, O_RDONLY);
+       if (datafd != -1) {
+               struct stat stat;
+               ssize_t bytes_read;
+               if (fstat(datafd, &stat)) {
+                       kr_log_error("[tls] unable to stat ephemeral private key " EPHEMERAL_PRIVKEY_FILENAME "\n");
+                       goto bad_data;
+               }
+               data.data = gnutls_malloc(stat.st_size);
+               if (data.data == NULL) {
+                       kr_log_error("[tls] unable to allocate memory for reading ephemeral private key\n");
+                       goto bad_data;
+               }
+               data.size = stat.st_size;
+               bytes_read = read(datafd, data.data, stat.st_size);
+               if (bytes_read != stat.st_size) {
+                       kr_log_error("[tls] unable to read ephemeral private key\n");
+                       goto bad_data;
+               }
+               if ((err = gnutls_x509_privkey_import (privkey, &data, GNUTLS_X509_FMT_PEM)) < 0) {
+                       kr_log_error("[tls] gnutls_x509_privkey_import() failed: %d (%s)\n",
+                                    err, gnutls_strerror_name(err));
+                       goto bad_data;
+               }
+               if (0) {
+               bad_data:
+                       close(datafd);
+                       datafd = -1;
+                       gnutls_free(data.data);
+                       data.data = NULL;
+               }
+       }
+       if (datafd == -1) {
+               /* if loading failed, then generate ... */
+               if ((err = gnutls_x509_privkey_generate(privkey, GNUTLS_PK_ECDSA, GNUTLS_CURVE_TO_BITS(GNUTLS_ECC_CURVE_SECP256R1), 0)) < 0) {
+                       kr_log_error("[tls] gnutls_x509_privkey_init() failed: %d (%s)\n",
+                                    err, gnutls_strerror_name(err));
+                       gnutls_x509_privkey_deinit(privkey);
+                       goto done;
+               }
+               /* ... and save */
+               kr_log_info("[tls] Stashing ephemeral private key in " EPHEMERAL_PRIVKEY_FILENAME "\n");
+               if ((err = gnutls_x509_privkey_export2(privkey, GNUTLS_X509_FMT_PEM, &data)) < 0) {
+                       kr_log_error("[tls] gnutls_x509_privkey_export2() failed: %d (%s), not storing\n",
+                                    err, gnutls_strerror_name(err));
+               } else {
+                       datafd = open(EPHEMERAL_PRIVKEY_FILENAME, O_WRONLY|O_CREAT, 0600);
+                       if (datafd == -1) {
+                               kr_log_error("[tls] failed to open " EPHEMERAL_PRIVKEY_FILENAME " to store the ephemeral key\n");
+                       } else {
+                               ssize_t bytes_written;
+                               bytes_written = write(datafd, data.data, data.size);
+                               if (bytes_written != data.size)
+                                       kr_log_error("[tls] failed to write %d octets to " EPHEMERAL_PRIVKEY_FILENAME " (%ld written)\n",
+                                                    data.size, bytes_written);
+                       }
+               }
+       }
+ done:
+       _lock_unlock(&lock, EPHEMERAL_PRIVKEY_FILENAME ".lock");
+       if (datafd != -1)
+               close(datafd);
+       return privkey;
+}
+
+static gnutls_x509_crt_t get_ephemeral_cert(gnutls_x509_privkey_t privkey, const char *servicename, time_t invalid_before, time_t valid_until)
+{
+       gnutls_x509_crt_t cert = NULL;
+       int err;
+       /* need a random buffer of bytes */
+       uint8_t serial[16];
+       gnutls_rnd(GNUTLS_RND_NONCE, serial, sizeof(serial));
+       /* clear the left-most bit to avoid signedness confusion: */
+       serial[0] &= 0x8f;
+       size_t namelen = strlen(servicename);
+
+#define gtx(fn, ...)                                                   \
+       if ((err = fn ( __VA_ARGS__ )) != GNUTLS_E_SUCCESS) {           \
+               kr_log_error("[tls] " #fn "() failed: %d (%s)\n",       \
+                            err, gnutls_strerror_name(err));           \
+               goto bad; }
+
+       gtx(gnutls_x509_crt_init, &cert);
+       gtx(gnutls_x509_crt_set_activation_time, cert, invalid_before);
+       gtx(gnutls_x509_crt_set_ca_status, cert, 0);
+       gtx(gnutls_x509_crt_set_expiration_time, cert, valid_until);
+       gtx(gnutls_x509_crt_set_key, cert, privkey);
+       gtx(gnutls_x509_crt_set_key_purpose_oid, cert, GNUTLS_KP_TLS_WWW_CLIENT, 0);
+       gtx(gnutls_x509_crt_set_key_purpose_oid, cert, GNUTLS_KP_TLS_WWW_SERVER, 0);
+       gtx(gnutls_x509_crt_set_key_usage, cert, GNUTLS_KEY_DIGITAL_SIGNATURE);
+       gtx(gnutls_x509_crt_set_serial, cert, serial, sizeof(serial));
+       gtx(gnutls_x509_crt_set_subject_alt_name, cert, GNUTLS_SAN_DNSNAME, servicename, namelen, GNUTLS_FSAN_SET);
+       gtx(gnutls_x509_crt_set_dn_by_oid,cert, GNUTLS_OID_X520_COMMON_NAME, 0, servicename, namelen);
+       gtx(gnutls_x509_crt_set_version, cert, 3);
+       gtx(gnutls_x509_crt_sign2,cert, cert, privkey, GNUTLS_DIG_SHA256, 0); /* self-sign, since it doesn't look like we can just stub-sign */
+#undef gtx
+
+       return cert;
+ bad:
+       gnutls_x509_crt_deinit(cert);
+       return NULL;
+}
+
+struct tls_credentials * tls_get_ephemeral_credentials(struct engine *engine)
+{
+       struct tls_credentials *creds = NULL;
+       gnutls_x509_privkey_t privkey = NULL;
+       gnutls_x509_crt_t cert = NULL;
+       int err;
+       time_t now = time(NULL);
+
+       creds = calloc(1, sizeof(*creds));
+       if (!creds) {
+               kr_log_error("[tls] failed to allocate memory for ephemeral credentials\n");
+               return NULL;
+       }
+       if ((err = gnutls_certificate_allocate_credentials(&(creds->credentials))) < 0) {
+               kr_log_error("[tls] failed to allocate memory for ephemeral credentials\n");
+               goto failure;
+       }
+
+       creds->valid_until = now + EPHEMERAL_CERT_EXPIRATION_SECONDS;
+       creds->ephemeral_servicename = strdup(engine_get_hostname(engine));
+       if (creds->ephemeral_servicename == NULL) {
+               kr_log_error("[tls] could not get server's hostname, using '" INVALID_HOSTNAME "' instead\n");
+               creds->ephemeral_servicename = strdup(INVALID_HOSTNAME);
+       }               
+       privkey = get_ephemeral_privkey();
+       if (!privkey)
+               goto failure;
+       cert = get_ephemeral_cert(privkey, creds->ephemeral_servicename, now - 60*15, creds->valid_until);
+       if (!cert)
+               goto failure;
+
+       if ((err = gnutls_certificate_set_x509_key(creds->credentials, &cert, 1, privkey)) < 0) {
+               kr_log_error("[tls] failed to set up ephemeral credentials\n");
+               goto failure;
+       }
+       gnutls_x509_privkey_deinit(privkey);
+       gnutls_x509_crt_deinit(cert);
+       return creds;
+ failure:
+       gnutls_x509_privkey_deinit(privkey);
+       gnutls_x509_crt_deinit(cert);
+       tls_credentials_free(creds);
+       return NULL;
+}