tls_ephemeral_credentials.c 8.37 KB
Newer Older
1 2
/*
 * Copyright (C) 2016 American Civil Liberties Union (ACLU)
3
 * Copyright (C) 2016-2017 CZ.NIC, z.s.p.o.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
 * 
 * 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));
Ondřej Surý's avatar
Ondřej Surý committed
113
			/* goto bad_data; */
114 115 116 117 118 119 120 121 122
		bad_data:
			close(datafd);
			datafd = -1;
			gnutls_free(data.data);
			data.data = NULL;
		}
	}
	if (datafd == -1) {
		/* if loading failed, then generate ... */
123
#if GNUTLS_VERSION_NUMBER >= 0x030500
124
		if ((err = gnutls_x509_privkey_generate(privkey, GNUTLS_PK_ECDSA, GNUTLS_CURVE_TO_BITS(GNUTLS_ECC_CURVE_SECP256R1), 0)) < 0) {
125 126 127
#else
		if ((err = gnutls_x509_privkey_generate(privkey, GNUTLS_PK_RSA, gnutls_sec_param_to_pk_bits(GNUTLS_PK_RSA, GNUTLS_SEC_PARAM_MEDIUM), 0)) < 0) {
#endif
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
			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;
Ondřej Surý's avatar
Ondřej Surý committed
191
bad:
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
	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");
218 219 220 221
		if ((creds->ephemeral_servicename = strdup(INVALID_HOSTNAME)) == NULL) {
			kr_log_error("[tls] failed to allocate memory for ephemeral credentials\n");
			goto failure;
		}
222
	}		
223
	if ((privkey = get_ephemeral_privkey()) == NULL) {
224
		goto failure;
225 226
	}
	if ((cert = get_ephemeral_cert(privkey, creds->ephemeral_servicename, now - 60*15, creds->valid_until)) == NULL) {
227
		goto failure;
228
	}
229 230 231 232 233 234 235 236 237 238 239 240 241
	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;
}