diff --git a/contrib/base64.c b/contrib/base64.c
new file mode 100644
index 0000000000000000000000000000000000000000..58e1b31a71b367451f11c27c7e72aa80e1b9ee14
--- /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 0000000000000000000000000000000000000000..ebe89a6ba44e2a93643eab0648072fa15d7c6c97
--- /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 b99c9ff887d9164827e46c7e49fd6c22c5917e9b..1753b530fb1382739aa5c577f737886c71d0e3f8 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 4a39135ad91df0847806656d17bf7f454c6039c0..31cafefa36b9fb2a4ab6ac68921b3402ef9dca3a 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 8243bc88cac5681f8816226b6707ed775b618c1d..3838fe37a24e667880a7c3638e82d20fcad7d602 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);