From 0cd371a4a8b068cadee303bc6b50374c90ef8e24 Mon Sep 17 00:00:00 2001
From: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
Date: Tue, 2 Aug 2016 21:41:17 -0400
Subject: [PATCH] Log key-pinning strings for TLS keys

RFC 7858 explicitly defines an out-of-band key pinning profile as one
authentication mechanism.  It uses the same format for representing
the pin as HPKP does (RFC 7469).

By logging this pin directly upon first use of the X.509 credentials,
we make it a little bit easier for an admin to publish part of a
pinset.

For ideal operation (including preparation for key rollover), a backup
public key should also be provided, but this is not defined
functionally here.
---
 contrib/base64.c   | 268 +++++++++++++++++++++++++++++++++++++++++++++
 contrib/base64.h   | 107 ++++++++++++++++++
 contrib/contrib.mk |   5 +-
 daemon/tls.c       |  77 ++++++++++++-
 daemon/tls.h       |   3 +
 5 files changed, 457 insertions(+), 3 deletions(-)
 create mode 100644 contrib/base64.c
 create mode 100644 contrib/base64.h

diff --git a/contrib/base64.c b/contrib/base64.c
new file mode 100644
index 000000000..58e1b31a7
--- /dev/null
+++ b/contrib/base64.c
@@ -0,0 +1,268 @@
+/*  Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "contrib/base64.h"
+#include "libknot/errcode.h"
+
+#include <stdlib.h>
+#include <stdint.h>
+
+/*! \brief Maximal length of binary input to Base64 encoding. */
+#define MAX_BIN_DATA_LEN	((INT32_MAX / 4) * 3)
+
+/*! \brief Base64 padding character. */
+static const uint8_t base64_pad = '=';
+/*! \brief Base64 alphabet. */
+static const uint8_t base64_enc[] =
+	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+/*! \brief Indicates bad Base64 character. */
+#define KO	255
+/*! \brief Indicates Base64 padding character. */
+#define PD	 64
+
+/*! \brief Transformation and validation table for decoding Base64. */
+static const uint8_t base64_dec[256] = {
+	[  0] = KO, ['+'] = 62, ['V'] = 21, [129] = KO, [172] = KO, [215] = KO,
+	[  1] = KO, [ 44] = KO, ['W'] = 22, [130] = KO, [173] = KO, [216] = KO,
+	[  2] = KO, [ 45] = KO, ['X'] = 23, [131] = KO, [174] = KO, [217] = KO,
+	[  3] = KO, [ 46] = KO, ['Y'] = 24, [132] = KO, [175] = KO, [218] = KO,
+	[  4] = KO, ['/'] = 63, ['Z'] = 25, [133] = KO, [176] = KO, [219] = KO,
+	[  5] = KO, ['0'] = 52, [ 91] = KO, [134] = KO, [177] = KO, [220] = KO,
+	[  6] = KO, ['1'] = 53, [ 92] = KO, [135] = KO, [178] = KO, [221] = KO,
+	[  7] = KO, ['2'] = 54, [ 93] = KO, [136] = KO, [179] = KO, [222] = KO,
+	[  8] = KO, ['3'] = 55, [ 94] = KO, [137] = KO, [180] = KO, [223] = KO,
+	[  9] = KO, ['4'] = 56, [ 95] = KO, [138] = KO, [181] = KO, [224] = KO,
+	[ 10] = KO, ['5'] = 57, [ 96] = KO, [139] = KO, [182] = KO, [225] = KO,
+	[ 11] = KO, ['6'] = 58, ['a'] = 26, [140] = KO, [183] = KO, [226] = KO,
+	[ 12] = KO, ['7'] = 59, ['b'] = 27, [141] = KO, [184] = KO, [227] = KO,
+	[ 13] = KO, ['8'] = 60, ['c'] = 28, [142] = KO, [185] = KO, [228] = KO,
+	[ 14] = KO, ['9'] = 61, ['d'] = 29, [143] = KO, [186] = KO, [229] = KO,
+	[ 15] = KO, [ 58] = KO, ['e'] = 30, [144] = KO, [187] = KO, [230] = KO,
+	[ 16] = KO, [ 59] = KO, ['f'] = 31, [145] = KO, [188] = KO, [231] = KO,
+	[ 17] = KO, [ 60] = KO, ['g'] = 32, [146] = KO, [189] = KO, [232] = KO,
+	[ 18] = KO, ['='] = PD, ['h'] = 33, [147] = KO, [190] = KO, [233] = KO,
+	[ 19] = KO, [ 62] = KO, ['i'] = 34, [148] = KO, [191] = KO, [234] = KO,
+	[ 20] = KO, [ 63] = KO, ['j'] = 35, [149] = KO, [192] = KO, [235] = KO,
+	[ 21] = KO, [ 64] = KO, ['k'] = 36, [150] = KO, [193] = KO, [236] = KO,
+	[ 22] = KO, ['A'] =  0, ['l'] = 37, [151] = KO, [194] = KO, [237] = KO,
+	[ 23] = KO, ['B'] =  1, ['m'] = 38, [152] = KO, [195] = KO, [238] = KO,
+	[ 24] = KO, ['C'] =  2, ['n'] = 39, [153] = KO, [196] = KO, [239] = KO,
+	[ 25] = KO, ['D'] =  3, ['o'] = 40, [154] = KO, [197] = KO, [240] = KO,
+	[ 26] = KO, ['E'] =  4, ['p'] = 41, [155] = KO, [198] = KO, [241] = KO,
+	[ 27] = KO, ['F'] =  5, ['q'] = 42, [156] = KO, [199] = KO, [242] = KO,
+	[ 28] = KO, ['G'] =  6, ['r'] = 43, [157] = KO, [200] = KO, [243] = KO,
+	[ 29] = KO, ['H'] =  7, ['s'] = 44, [158] = KO, [201] = KO, [244] = KO,
+	[ 30] = KO, ['I'] =  8, ['t'] = 45, [159] = KO, [202] = KO, [245] = KO,
+	[ 31] = KO, ['J'] =  9, ['u'] = 46, [160] = KO, [203] = KO, [246] = KO,
+	[ 32] = KO, ['K'] = 10, ['v'] = 47, [161] = KO, [204] = KO, [247] = KO,
+	[ 33] = KO, ['L'] = 11, ['w'] = 48, [162] = KO, [205] = KO, [248] = KO,
+	[ 34] = KO, ['M'] = 12, ['x'] = 49, [163] = KO, [206] = KO, [249] = KO,
+	[ 35] = KO, ['N'] = 13, ['y'] = 50, [164] = KO, [207] = KO, [250] = KO,
+	[ 36] = KO, ['O'] = 14, ['z'] = 51, [165] = KO, [208] = KO, [251] = KO,
+	[ 37] = KO, ['P'] = 15, [123] = KO, [166] = KO, [209] = KO, [252] = KO,
+	[ 38] = KO, ['Q'] = 16, [124] = KO, [167] = KO, [210] = KO, [253] = KO,
+	[ 39] = KO, ['R'] = 17, [125] = KO, [168] = KO, [211] = KO, [254] = KO,
+	[ 40] = KO, ['S'] = 18, [126] = KO, [169] = KO, [212] = KO, [255] = KO,
+	[ 41] = KO, ['T'] = 19, [127] = KO, [170] = KO, [213] = KO,
+	[ 42] = KO, ['U'] = 20, [128] = KO, [171] = KO, [214] = KO,
+};
+
+int32_t base64_encode(const uint8_t  *in,
+                      const uint32_t in_len,
+                      uint8_t        *out,
+                      const uint32_t out_len)
+{
+	// Checking inputs.
+	if (in == NULL || out == NULL) {
+		return KNOT_EINVAL;
+	}
+	if (in_len > MAX_BIN_DATA_LEN || out_len < ((in_len + 2) / 3) * 4) {
+		return KNOT_ERANGE;
+	}
+
+	uint8_t		rest_len = in_len % 3;
+	const uint8_t	*stop = in + in_len - rest_len;
+	uint8_t		*text = out;
+
+	// Encoding loop takes 3 bytes and creates 4 characters.
+	while (in < stop) {
+		text[0] = base64_enc[in[0] >> 2];
+		text[1] = base64_enc[(in[0] & 0x03) << 4 | in[1] >> 4];
+		text[2] = base64_enc[(in[1] & 0x0F) << 2 | in[2] >> 6];
+		text[3] = base64_enc[in[2] & 0x3F];
+		text += 4;
+		in += 3;
+	}
+
+	// Processing of padding, if any.
+	switch (rest_len) {
+	case 2:
+		text[0] = base64_enc[in[0] >> 2];
+		text[1] = base64_enc[(in[0] & 0x03) << 4 | in[1] >> 4];
+		text[2] = base64_enc[(in[1] & 0x0F) << 2];
+		text[3] = base64_pad;
+		text += 4;
+		break;
+	case 1:
+		text[0] = base64_enc[in[0] >> 2];
+		text[1] = base64_enc[(in[0] & 0x03) << 4];
+		text[2] = base64_pad;
+		text[3] = base64_pad;
+		text += 4;
+		break;
+	}
+
+	return (text - out);
+}
+
+int32_t base64_encode_alloc(const uint8_t  *in,
+                            const uint32_t in_len,
+                            uint8_t        **out)
+{
+	// Checking inputs.
+	if (out == NULL) {
+		return KNOT_EINVAL;
+	}
+	if (in_len > MAX_BIN_DATA_LEN) {
+		return KNOT_ERANGE;
+	}
+
+	// Compute output buffer length.
+	uint32_t out_len = ((in_len + 2) / 3) * 4;
+
+	// Allocate output buffer.
+	*out = malloc(out_len);
+	if (*out == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	// Encode data.
+	int32_t ret = base64_encode(in, in_len, *out, out_len);
+	if (ret < 0) {
+		free(*out);
+	}
+
+	return ret;
+}
+
+int32_t base64_decode(const uint8_t  *in,
+                      const uint32_t in_len,
+                      uint8_t        *out,
+                      const uint32_t out_len)
+{
+	// Checking inputs.
+	if (in == NULL || out == NULL) {
+		return KNOT_EINVAL;
+	}
+	if (in_len > INT32_MAX || out_len < ((in_len + 3) / 4) * 3) {
+		return KNOT_ERANGE;
+	}
+	if ((in_len % 4) != 0) {
+		return KNOT_BASE64_ESIZE;
+	}
+
+	const uint8_t	*stop = in + in_len;
+	uint8_t		*bin = out;
+	uint8_t		pad_len = 0;
+	uint8_t		c1, c2, c3, c4;
+
+	// Decoding loop takes 4 characters and creates 3 bytes.
+	while (in < stop) {
+		// Filling and transforming 4 Base64 chars.
+		c1 = base64_dec[in[0]];
+		c2 = base64_dec[in[1]];
+		c3 = base64_dec[in[2]];
+		c4 = base64_dec[in[3]];
+
+		// Check 4. char if is bad or padding.
+		if (c4 >= PD) {
+			if (c4 == PD && pad_len == 0) {
+				pad_len = 1;
+			} else {
+				return KNOT_BASE64_ECHAR;
+			}
+		}
+
+		// Check 3. char if is bad or padding.
+		if (c3 >= PD) {
+			if (c3 == PD && pad_len == 1) {
+				pad_len = 2;
+			} else {
+				return KNOT_BASE64_ECHAR;
+			}
+		}
+
+		// Check 1. and 2. chars if are not padding.
+		if (c2 >= PD || c1 >= PD) {
+			return KNOT_BASE64_ECHAR;
+		}
+
+		// Computing of output data based on padding length.
+		switch (pad_len) {
+		case 0:
+			bin[2] = (c3 << 6) + c4;
+		case 1:
+			bin[1] = (c2 << 4) + (c3 >> 2);
+		case 2:
+			bin[0] = (c1 << 2) + (c2 >> 4);
+		}
+
+		// Update output end.
+		switch (pad_len) {
+		case 0:
+			bin += 3;
+			break;
+		case 1:
+			bin += 2;
+			break;
+		case 2:
+			bin += 1;
+			break;
+		}
+
+		in += 4;
+	}
+
+	return (bin - out);
+}
+
+int32_t base64_decode_alloc(const uint8_t  *in,
+                            const uint32_t in_len,
+                            uint8_t        **out)
+{
+	// Checking inputs.
+	if (out == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	// Compute output buffer length.
+	uint32_t out_len = ((in_len + 3) / 4) * 3;
+
+	// Allocate output buffer.
+	*out = malloc(out_len);
+	if (*out == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	// Decode data.
+	int32_t ret = base64_decode(in, in_len, *out, out_len);
+	if (ret < 0) {
+		free(*out);
+	}
+
+	return ret;
+}
diff --git a/contrib/base64.h b/contrib/base64.h
new file mode 100644
index 000000000..ebe89a6ba
--- /dev/null
+++ b/contrib/base64.h
@@ -0,0 +1,107 @@
+/*  Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    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 <http://www.gnu.org/licenses/>.
+ */
+/*!
+ * \file
+ *
+ * \brief Base64 implementation (RFC 4648).
+ *
+ * \addtogroup contrib
+ * @{
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+/*!
+ * \brief Encodes binary data using Base64.
+ *
+ * \note Output data buffer contains Base64 text string which isn't
+ *       terminated with '\0'!
+ *
+ * \param in		Input binary data.
+ * \param in_len	Length of input data.
+ * \param out		Output data buffer.
+ * \param out_len	Size of output buffer.
+ *
+ * \retval >=0		length of output string.
+ * \retval KNOT_E*	if error.
+ */
+int32_t base64_encode(const uint8_t  *in,
+                      const uint32_t in_len,
+                      uint8_t        *out,
+                      const uint32_t out_len);
+
+/*!
+ * \brief Encodes binary data using Base64 and output stores to own buffer.
+ *
+ * \note Output data buffer contains Base64 text string which isn't
+ *       terminated with '\0'!
+ *
+ * \note Output buffer should be deallocated after use.
+ *
+ * \param in		Input binary data.
+ * \param in_len	Length of input data.
+ * \param out		Output data buffer.
+ *
+ * \retval >=0		length of output string.
+ * \retval KNOT_E*	if error.
+ */
+int32_t base64_encode_alloc(const uint8_t  *in,
+                            const uint32_t in_len,
+                            uint8_t        **out);
+
+/*!
+ * \brief Decodes text data using Base64.
+ *
+ * \note Input data needn't be terminated with '\0'.
+ *
+ * \note Input data must be continuous Base64 string!
+ *
+ * \param in		Input text data.
+ * \param in_len	Length of input string.
+ * \param out		Output data buffer.
+ * \param out_len	Size of output buffer.
+ *
+ * \retval >=0		length of output data.
+ * \retval KNOT_E*	if error.
+ */
+int32_t base64_decode(const uint8_t  *in,
+                      const uint32_t in_len,
+                      uint8_t        *out,
+                      const uint32_t out_len);
+
+/*!
+ * \brief Decodes text data using Base64 and output stores to own buffer.
+ *
+ * \note Input data needn't be terminated with '\0'.
+ *
+ * \note Input data must be continuous Base64 string!
+ *
+ * \note Output buffer should be deallocated after use.
+ *
+ * \param in		Input text data.
+ * \param in_len	Length of input string.
+ * \param out		Output data buffer.
+ *
+ * \retval >=0		length of output data.
+ * \retval KNOT_E*	if error.
+ */
+int32_t base64_decode_alloc(const uint8_t  *in,
+                            const uint32_t in_len,
+                            uint8_t        **out);
+
+/*! @} */
diff --git a/contrib/contrib.mk b/contrib/contrib.mk
index b99c9ff88..1753b530f 100644
--- a/contrib/contrib.mk
+++ b/contrib/contrib.mk
@@ -5,7 +5,8 @@ contrib_SOURCES := \
 	contrib/ccan/json/json.c \
 	contrib/ucw/mempool.c \
 	contrib/murmurhash3/murmurhash3.c \
-	contrib/base32hex.c
+	contrib/base32hex.c \
+	contrib/base64.c
 contrib_CFLAGS := -fPIC
 contrib_TARGET := $(abspath contrib)/contrib$(AREXT)
 
@@ -17,4 +18,4 @@ contrib_CFLAGS  += -pthread
 contrib_LIBS    += -pthread
 endif
 
-$(eval $(call make_static,contrib,contrib))
\ No newline at end of file
+$(eval $(call make_static,contrib,contrib))
diff --git a/daemon/tls.c b/daemon/tls.c
index 4a39135ad..31cafefa3 100644
--- a/daemon/tls.c
+++ b/daemon/tls.c
@@ -17,6 +17,10 @@
  * this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <gnutls/gnutls.h>
+#include <gnutls/x509.h>
+#include <gnutls/abstract.h>
+#include <gnutls/crypto.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <assert.h>
@@ -24,6 +28,7 @@
 #include <uv.h>
 
 #include <contrib/ucw/lib.h>
+#include "contrib/base64.h"
 #include "daemon/worker.h"
 #include "daemon/tls.h"
 #include "daemon/io.h"
@@ -260,6 +265,75 @@ int tls_process(struct worker_ctx *worker, uv_stream_t *handle, const uint8_t *b
 	return submitted;
 }
 
+/*
+  DNS-over-TLS Out of band key-pinned authentication profile uses the
+  same form of pins as HPKP:
+  
+  e.g.  pin-sha256="FHkyLhvI0n70E47cJlRTamTrnYVcsYdjUGbr79CfAVI="
+  
+  DNS-over-TLS OOB key-pins: https://tools.ietf.org/html/rfc7858#appendix-A
+  HPKP pin reference:        https://tools.ietf.org/html/rfc7469#appendix-A
+*/
+#define PINLEN  (((32) * 8 + 4)/6) + 3 + 1
+
+/* out must be at least PINLEN octets long */
+static int get_oob_key_pin(gnutls_x509_crt_t crt, char *outchar, ssize_t outchar_len)
+{
+	int err;
+	gnutls_pubkey_t key;
+	gnutls_datum_t datum = { .size = 0 };
+
+	if ((err = gnutls_pubkey_init(&key)) < 0) {
+		return err;
+	}
+
+	if ((err = gnutls_pubkey_import_x509(key, crt, 0)) != GNUTLS_E_SUCCESS) {
+		goto leave;
+	} else {
+		if ((err = gnutls_pubkey_export2(key, GNUTLS_X509_FMT_DER, &datum)) != GNUTLS_E_SUCCESS) {
+			goto leave;
+		} else {
+			uint8_t raw_pin[32];
+			if ((err = gnutls_hash_fast(GNUTLS_DIG_SHA256, datum.data, datum.size, raw_pin)) != GNUTLS_E_SUCCESS) {
+				goto leave;
+			} else {
+				base64_encode(raw_pin, sizeof(raw_pin), (uint8_t *)outchar, outchar_len);
+			}
+		}
+	}
+leave:
+	gnutls_free(datum.data);
+	gnutls_pubkey_deinit(key);
+	return err;
+}
+
+void tls_credentials_log_pins(struct tls_credentials *tls_credentials)
+{
+	for (int index = 0;; index++) {
+		int err;
+		gnutls_x509_crt_t *certs = NULL;
+		unsigned int cert_count = 0;
+
+		if ((err = gnutls_certificate_get_x509_crt(tls_credentials->credentials, index, &certs, &cert_count)) != GNUTLS_E_SUCCESS) {
+			if (err != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
+				kr_log_error("[tls] could not get x509 certificates (%d) %s\n", err, gnutls_strerror_name(err));
+			}
+			return;
+		}
+
+		for (int i = 0; i < cert_count; i++) {
+			char pin[PINLEN] = { 0 };
+			if ((err = get_oob_key_pin(certs[i], pin, sizeof(pin))) != GNUTLS_E_SUCCESS) {
+				kr_log_error("[tls] could not calculate RFC 7858 OOB key-pin from cert %d (%d) %s\n", i, err, gnutls_strerror_name(err));
+			} else {
+				kr_log_info("[tls] RFC 7858 OOB key-pin (%d): pin-sha256=\"%s\"\n", i, pin);
+			}
+			gnutls_x509_crt_deinit(certs[i]);
+		}
+		gnutls_free(certs);
+	}
+}
+
 static int str_replace(char **where_ptr, const char *with)
 {
 	char *copy = with ? strdup(with) : NULL;
@@ -314,9 +388,10 @@ int tls_certificate_set(struct network *net, const char *tls_cert, const char *t
 	}
 	// Exchange the x509 credentials
 	struct tls_credentials *old_credentials = net->tls_credentials;
-
+	
 	// Start using the new x509_credentials
 	net->tls_credentials = tls_credentials;
+	tls_credentials_log_pins(net->tls_credentials);
 
 	if (old_credentials) {
 		err = tls_credentials_release(old_credentials);
diff --git a/daemon/tls.h b/daemon/tls.h
index 8243bc88c..3838fe37a 100644
--- a/daemon/tls.h
+++ b/daemon/tls.h
@@ -41,3 +41,6 @@ int tls_certificate_set(struct network *net, const char *tls_cert, const char *t
 int tls_credentials_release(struct tls_credentials *tls_credentials);
 void tls_credentials_free(struct tls_credentials *tls_credentials);
 struct tls_credentials *tls_credentials_reserve(struct tls_credentials *worker);
+/* 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);
-- 
GitLab