summaryrefslogtreecommitdiffstats
path: root/src/libknot/quic
diff options
context:
space:
mode:
Diffstat (limited to 'src/libknot/quic')
-rw-r--r--src/libknot/quic/quic.c343
-rw-r--r--src/libknot/quic/quic.h59
-rw-r--r--src/libknot/quic/quic_conn.c35
-rw-r--r--src/libknot/quic/quic_conn.h20
-rw-r--r--src/libknot/quic/tls.c262
-rw-r--r--src/libknot/quic/tls.h135
-rw-r--r--src/libknot/quic/tls_common.c472
-rw-r--r--src/libknot/quic/tls_common.h134
8 files changed, 1071 insertions, 389 deletions
diff --git a/src/libknot/quic/quic.c b/src/libknot/quic/quic.c
index f9d1d1d..4eb84c3 100644
--- a/src/libknot/quic/quic.c
+++ b/src/libknot/quic/quic.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/* Copyright (C) 2024 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
@@ -33,7 +33,6 @@
#include "contrib/macros.h"
#include "contrib/sockaddr.h"
-#include "contrib/string.h"
#include "contrib/ucw/lists.h"
#include "libknot/endian.h"
#include "libdnssec/error.h"
@@ -58,19 +57,6 @@
#define TLS_CALLBACK_ERR (-1)
-const gnutls_datum_t doq_alpn = {
- (unsigned char *)"doq", 3
-};
-
-typedef struct knot_quic_creds {
- gnutls_certificate_credentials_t tls_cert;
- gnutls_anti_replay_t tls_anti_replay;
- gnutls_datum_t tls_ticket_key;
- bool peer;
- uint8_t peer_pin_len;
- uint8_t peer_pin[];
-} knot_quic_creds_t;
-
typedef struct knot_quic_session {
node_t n;
gnutls_datum_t tls_session;
@@ -153,223 +139,6 @@ session_free:
return ret;
}
-static int tls_anti_replay_db_add_func(void *dbf, time_t exp_time,
- const gnutls_datum_t *key,
- const gnutls_datum_t *data)
-{
- return 0;
-}
-
-static void tls_session_ticket_key_free(gnutls_datum_t *ticket)
-{
- gnutls_memset(ticket->data, 0, ticket->size);
- gnutls_free(ticket->data);
-}
-
-static int self_key(gnutls_x509_privkey_t *privkey, const char *key_file)
-{
- gnutls_datum_t data = { 0 };
-
- int ret = gnutls_x509_privkey_init(privkey);
- if (ret != GNUTLS_E_SUCCESS) {
- return ret;
- }
-
- int fd = open(key_file, O_RDONLY);
- if (fd != -1) {
- struct stat stat;
- if (fstat(fd, &stat) != 0 ||
- (data.data = gnutls_malloc(stat.st_size)) == NULL ||
- read(fd, data.data, stat.st_size) != stat.st_size) {
- ret = GNUTLS_E_KEYFILE_ERROR;
- goto finish;
- }
-
- data.size = stat.st_size;
- ret = gnutls_x509_privkey_import_pkcs8(*privkey, &data, GNUTLS_X509_FMT_PEM,
- NULL, GNUTLS_PKCS_PLAIN);
- if (ret != GNUTLS_E_SUCCESS) {
- goto finish;
- }
- } else {
- ret = gnutls_x509_privkey_generate(*privkey, GNUTLS_PK_EDDSA_ED25519,
- GNUTLS_CURVE_TO_BITS(GNUTLS_ECC_CURVE_ED25519), 0);
- if (ret != GNUTLS_E_SUCCESS) {
- goto finish;
- }
-
- ret = gnutls_x509_privkey_export2_pkcs8(*privkey, GNUTLS_X509_FMT_PEM, NULL,
- GNUTLS_PKCS_PLAIN, &data);
- if (ret != GNUTLS_E_SUCCESS ||
- (fd = open(key_file, O_WRONLY | O_CREAT, 0600)) == -1 ||
- write(fd, data.data, data.size) != data.size) {
- ret = GNUTLS_E_KEYFILE_ERROR;
- goto finish;
- }
- }
-
-finish:
- close(fd);
- gnutls_free(data.data);
- if (ret != GNUTLS_E_SUCCESS) {
- gnutls_x509_privkey_deinit(*privkey);
- *privkey = NULL;
- }
- return ret;
-}
-
-static int self_signed_cert(gnutls_certificate_credentials_t tls_cert,
- const char *key_file)
-{
- gnutls_x509_privkey_t privkey = NULL;
- gnutls_x509_crt_t cert = NULL;
-
- char *hostname = sockaddr_hostname();
- if (hostname == NULL) {
- return GNUTLS_E_MEMORY_ERROR;
- }
-
- int ret;
- uint8_t serial[16];
- gnutls_rnd(GNUTLS_RND_NONCE, serial, sizeof(serial));
- // Clear the left-most bit to be a positive number (two's complement form).
- serial[0] &= 0x7F;
-
-#define CHK(cmd) if ((ret = (cmd)) != GNUTLS_E_SUCCESS) { goto finish; }
-#define NOW_DAYS(days) (time(NULL) + 24 * 3600 * (days))
-
- CHK(self_key(&privkey, key_file));
-
- CHK(gnutls_x509_crt_init(&cert));
- CHK(gnutls_x509_crt_set_version(cert, 3));
- CHK(gnutls_x509_crt_set_serial(cert, serial, sizeof(serial)));
- CHK(gnutls_x509_crt_set_activation_time(cert, NOW_DAYS(-1)));
- CHK(gnutls_x509_crt_set_expiration_time(cert, NOW_DAYS(10 * 365)));
- CHK(gnutls_x509_crt_set_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0,
- hostname, strlen(hostname)));
- CHK(gnutls_x509_crt_set_key(cert, privkey));
- CHK(gnutls_x509_crt_sign2(cert, cert, privkey, GNUTLS_DIG_SHA512, 0));
-
- ret = gnutls_certificate_set_x509_key(tls_cert, &cert, 1, privkey);
-
-finish:
- free(hostname);
- gnutls_x509_crt_deinit(cert);
- gnutls_x509_privkey_deinit(privkey);
-
- return ret;
-}
-
-_public_
-struct knot_quic_creds *knot_quic_init_creds(const char *cert_file,
- const char *key_file)
-{
- knot_quic_creds_t *creds = calloc(1, sizeof(*creds));
- if (creds == NULL) {
- return NULL;
- }
-
- int ret = gnutls_certificate_allocate_credentials(&creds->tls_cert);
- if (ret != GNUTLS_E_SUCCESS) {
- goto fail;
- }
-
- ret = gnutls_anti_replay_init(&creds->tls_anti_replay);
- if (ret != GNUTLS_E_SUCCESS) {
- goto fail;
- }
- gnutls_anti_replay_set_add_function(creds->tls_anti_replay, tls_anti_replay_db_add_func);
- gnutls_anti_replay_set_ptr(creds->tls_anti_replay, NULL);
-
- if (cert_file != NULL) {
- ret = gnutls_certificate_set_x509_key_file(creds->tls_cert,
- cert_file, key_file,
- GNUTLS_X509_FMT_PEM);
- } else {
- ret = self_signed_cert(creds->tls_cert, key_file);
- }
- if (ret != GNUTLS_E_SUCCESS) {
- goto fail;
- }
-
- ret = gnutls_session_ticket_key_generate(&creds->tls_ticket_key);
- if (ret != GNUTLS_E_SUCCESS) {
- goto fail;
- }
-
- return creds;
-fail:
- knot_quic_free_creds(creds);
- return NULL;
-}
-
-_public_
-struct knot_quic_creds *knot_quic_init_creds_peer(const struct knot_quic_creds *local_creds,
- const uint8_t *peer_pin,
- uint8_t peer_pin_len)
-{
- knot_quic_creds_t *creds = calloc(1, sizeof(*creds) + peer_pin_len);
- if (creds == NULL) {
- return NULL;
- }
-
- if (local_creds != NULL) {
- creds->peer = true;
- creds->tls_cert = local_creds->tls_cert;
- } else {
- int ret = gnutls_certificate_allocate_credentials(&creds->tls_cert);
- if (ret != GNUTLS_E_SUCCESS) {
- free(creds);
- return NULL;
- }
- }
-
- if (peer_pin_len > 0 && peer_pin != NULL) {
- memcpy(creds->peer_pin, peer_pin, peer_pin_len);
- creds->peer_pin_len = peer_pin_len;
- }
-
- return creds;
-}
-
-_public_
-int knot_quic_creds_cert(struct knot_quic_creds *creds, struct gnutls_x509_crt_int **cert)
-{
- if (creds == NULL || cert == NULL) {
- return KNOT_EINVAL;
- }
-
- gnutls_x509_crt_t *certs;
- unsigned cert_count;
- int ret = gnutls_certificate_get_x509_crt(creds->tls_cert, 0, &certs, &cert_count);
- if (ret == GNUTLS_E_SUCCESS) {
- if (cert_count == 0) {
- gnutls_x509_crt_deinit(*certs);
- return KNOT_ENOENT;
- }
- *cert = *certs;
- free(certs);
- }
- return ret;
-}
-
-_public_
-void knot_quic_free_creds(struct knot_quic_creds *creds)
-{
- if (creds == NULL) {
- return;
- }
-
- if (!creds->peer && creds->tls_cert != NULL) {
- gnutls_certificate_free_credentials(creds->tls_cert);
- }
- gnutls_anti_replay_deinit(creds->tls_anti_replay);
- if (creds->tls_ticket_key.data != NULL) {
- tls_session_ticket_key_free(&creds->tls_ticket_key);
- }
- free(creds);
-}
-
static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref)
{
return ((knot_quic_conn_t *)conn_ref->user_data)->conn;
@@ -377,51 +146,31 @@ static ngtcp2_conn *get_conn(ngtcp2_crypto_conn_ref *conn_ref)
static int tls_init_conn_session(knot_quic_conn_t *conn, bool server)
{
- if (gnutls_init(&conn->tls_session, (server ? GNUTLS_SERVER : GNUTLS_CLIENT) |
- GNUTLS_ENABLE_EARLY_DATA | GNUTLS_NO_AUTO_SEND_TICKET |
- GNUTLS_NO_END_OF_EARLY_DATA) != GNUTLS_E_SUCCESS) {
- return TLS_CALLBACK_ERR;
- }
-
- gnutls_certificate_send_x509_rdn_sequence(conn->tls_session, 1);
- gnutls_certificate_server_set_request(conn->tls_session, GNUTLS_CERT_REQUEST);
-
- if (gnutls_priority_set_direct(conn->tls_session, QUIC_PRIORITIES,
- NULL) != GNUTLS_E_SUCCESS) {
+ int ret = knot_tls_session(&conn->tls_session, conn->quic_table->creds,
+ conn->quic_table->priority, "\x03""doq",
+ true, server);
+ if (ret != KNOT_EOK) {
return TLS_CALLBACK_ERR;
}
- if (server && gnutls_session_ticket_enable_server(conn->tls_session,
- &conn->quic_table->creds->tls_ticket_key) != GNUTLS_E_SUCCESS) {
- return TLS_CALLBACK_ERR;
+ if (server) {
+ ret = ngtcp2_crypto_gnutls_configure_server_session(conn->tls_session);
+ } else {
+ ret = ngtcp2_crypto_gnutls_configure_client_session(conn->tls_session);
}
-
- int ret = ngtcp2_crypto_gnutls_configure_server_session(conn->tls_session);
- if (ret != 0) {
+ if (ret != NGTCP2_NO_ERROR) {
return TLS_CALLBACK_ERR;
}
- gnutls_record_set_max_early_data_size(conn->tls_session, 0xffffffffu);
-
conn->conn_ref = (nc_conn_ref_placeholder_t) {
.get_conn = get_conn,
.user_data = conn
};
- _Static_assert(sizeof(nc_conn_ref_placeholder_t) == sizeof(ngtcp2_crypto_conn_ref), "invalid placeholder for conn_ref");
+ _Static_assert(sizeof(nc_conn_ref_placeholder_t) == sizeof(ngtcp2_crypto_conn_ref),
+ "invalid placeholder for conn_ref");
gnutls_session_set_ptr(conn->tls_session, &conn->conn_ref);
- if (server) {
- gnutls_anti_replay_enable(conn->tls_session, conn->quic_table->creds->tls_anti_replay);
-
- }
- if (gnutls_credentials_set(conn->tls_session, GNUTLS_CRD_CERTIFICATE,
- conn->quic_table->creds->tls_cert) != GNUTLS_E_SUCCESS) {
- return TLS_CALLBACK_ERR;
- }
-
- gnutls_alpn_set_protocols(conn->tls_session, &doq_alpn, 1, GNUTLS_ALPN_MANDATORY);
-
ngtcp2_conn_set_tls_native_handle(conn->conn, conn->tls_session);
return KNOT_EOK;
@@ -477,54 +226,6 @@ uint16_t knot_quic_conn_local_port(knot_quic_conn_t *conn)
return ((const struct sockaddr_in6 *)path->local.addr)->sin6_port;
}
-_public_
-void knot_quic_conn_pin(knot_quic_conn_t *conn, uint8_t *pin, size_t *pin_size, bool local)
-{
- if (conn == NULL) {
- goto error;
- }
-
- const gnutls_datum_t *data = NULL;
- if (local) {
- data = gnutls_certificate_get_ours(conn->tls_session);
- } else {
- unsigned count = 0;
- data = gnutls_certificate_get_peers(conn->tls_session, &count);
- if (count == 0) {
- goto error;
- }
- }
- if (data == NULL) {
- goto error;
- }
-
- gnutls_x509_crt_t cert;
- int ret = gnutls_x509_crt_init(&cert);
- if (ret != GNUTLS_E_SUCCESS) {
- goto error;
- }
-
- ret = gnutls_x509_crt_import(cert, data, GNUTLS_X509_FMT_DER);
- if (ret != GNUTLS_E_SUCCESS) {
- gnutls_x509_crt_deinit(cert);
- goto error;
- }
-
- ret = gnutls_x509_crt_get_key_id(cert, GNUTLS_KEYID_USE_SHA256, pin, pin_size);
- if (ret != GNUTLS_E_SUCCESS) {
- gnutls_x509_crt_deinit(cert);
- goto error;
- }
-
- gnutls_x509_crt_deinit(cert);
-
- return;
-error:
- if (pin_size != NULL) {
- *pin_size = 0;
- }
-}
-
static void knot_quic_rand_cb(uint8_t *dest, size_t destlen, const ngtcp2_rand_ctx *rand_ctx)
{
(void)rand_ctx;
@@ -602,18 +303,8 @@ static int handshake_completed_cb(ngtcp2_conn *conn, void *user_data)
ctx->flags |= KNOT_QUIC_CONN_HANDSHAKE_DONE;
if (!ngtcp2_conn_is_server(conn)) {
- knot_quic_creds_t *creds = ctx->quic_table->creds;
- if (creds->peer_pin_len == 0) {
- return 0;
- }
- uint8_t pin[KNOT_QUIC_PIN_LEN];
- size_t pin_size = sizeof(pin);
- knot_quic_conn_pin(ctx, pin, &pin_size, false);
- if (pin_size != creds->peer_pin_len ||
- const_time_memcmp(pin, creds->peer_pin, pin_size) != 0) {
- return NGTCP2_ERR_CALLBACK_FAILURE;
- }
- return 0;
+ return knot_tls_pin_check(ctx->tls_session, ctx->quic_table->creds)
+ == KNOT_EOK ? 0 : NGTCP2_ERR_CALLBACK_FAILURE;
}
if (gnutls_session_ticket_send(ctx->tls_session, 1, 0) != GNUTLS_E_SUCCESS) {
@@ -945,6 +636,10 @@ int knot_quic_handle(knot_quic_table_t *table, knot_quic_reply_t *reply,
goto finish;
}
+ if (conn != NULL && (conn->flags & KNOT_QUIC_CONN_BLOCKED)) {
+ return KNOT_EOK;
+ }
+
ngtcp2_path path;
path.remote.addr = (struct sockaddr *)reply->ip_rem;
path.remote.addrlen = addr_len((struct sockaddr_in6 *)reply->ip_rem);
@@ -1249,6 +944,8 @@ int knot_quic_send(knot_quic_table_t *quic_table, knot_quic_conn_t *conn,
return KNOT_EINVAL;
} else if (reply->handle_ret < 0) {
return reply->handle_ret;
+ } else if ((conn->flags & KNOT_QUIC_CONN_BLOCKED) && !(flags & KNOT_QUIC_SEND_IGNORE_BLOCKED)) {
+ return KNOT_EOK;
} else if (reply->handle_ret > 0) {
return send_special(quic_table, reply, conn);
} else if (conn == NULL) {
diff --git a/src/libknot/quic/quic.h b/src/libknot/quic/quic.h
index 29a02e0..b4acb33 100644
--- a/src/libknot/quic/quic.h
+++ b/src/libknot/quic/quic.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/* Copyright (C) 2024 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
@@ -29,20 +29,18 @@
#include <netinet/in.h>
#include "libknot/quic/quic_conn.h"
-
-#define KNOT_QUIC_PIN_LEN 32
+#include "libknot/quic/tls_common.h"
#define KNOT_QUIC_HANDLE_RET_CLOSE 2000
// RFC 9250
#define KNOT_QUIC_ERR_EXCESSIVE_LOAD 0x4
-struct gnutls_x509_crt_int;
-struct knot_quic_creds;
struct knot_quic_session;
typedef enum {
KNOT_QUIC_SEND_IGNORE_LASTBYTE = (1 << 0),
+ KNOT_QUIC_SEND_IGNORE_BLOCKED = (1 << 1),
} knot_quic_send_flag_t;
typedef struct knot_quic_reply {
@@ -87,45 +85,6 @@ struct knot_quic_session *knot_quic_session_save(knot_quic_conn_t *conn);
int knot_quic_session_load(knot_quic_conn_t *conn, struct knot_quic_session *session);
/*!
- * \brief Init server TLS certificate for DoQ.
- *
- * \param cert_file X509 certificate PEM file path/name (NULL if auto-generated).
- * \param key_file Key PEM file path/name.
- *
- * \return Initialized creds.
- */
-struct knot_quic_creds *knot_quic_init_creds(const char *cert_file,
- const char *key_file);
-
-/*!
- * \brief Init peer TLS certificate for DoQ.
- *
- * \param local_creds Local credentials if server.
- * \param peer_pin Optional peer certificate pin to check.
- * \param peer_pin_len Length of the peer pin. Set 0 if not specified.
- *
- * \return Initialized creds.
- */
-struct knot_quic_creds *knot_quic_init_creds_peer(const struct knot_quic_creds *local_creds,
- const uint8_t *peer_pin,
- uint8_t peer_pin_len);
-
-/*!
- * \brief Gets the certificate from credentials.
- *
- * \param creds TLS credentials.
- * \param cert Output certificate.
- *
- * \return KNOT_E*
- */
-int knot_quic_creds_cert(struct knot_quic_creds *creds, struct gnutls_x509_crt_int **cert);
-
-/*!
- * \brief Deinit server TLS certificate for DoQ.
- */
-void knot_quic_free_creds(struct knot_quic_creds *creds);
-
-/*!
* \brief Returns timeout value for the connection.
*/
uint64_t quic_conn_get_timeout(knot_quic_conn_t *conn);
@@ -156,18 +115,6 @@ uint32_t knot_quic_conn_rtt(knot_quic_conn_t *conn);
uint16_t knot_quic_conn_local_port(knot_quic_conn_t *conn);
/*!
- * \brief Gets local or remote certificate pin.
- *
- * \note Zero output pin_size value means no certificate available or error.
- *
- * \param conn QUIC connection.
- * \param pin Output certificate pin.
- * \param pin_size Input size of the storage / output size of the stored pin.
- * \param local Local or remote certificate indication.
- */
-void knot_quic_conn_pin(knot_quic_conn_t *conn, uint8_t *pin, size_t *pin_size, bool local);
-
-/*!
* \brief Create new outgoing QUIC connection.
*
* \param table QUIC connections table to be added to.
diff --git a/src/libknot/quic/quic_conn.c b/src/libknot/quic/quic_conn.c
index 6616573..1a3b9df 100644
--- a/src/libknot/quic/quic_conn.c
+++ b/src/libknot/quic/quic_conn.c
@@ -1,4 +1,4 @@
-/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/* Copyright (C) 2024 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
@@ -29,6 +29,7 @@
#include "libdnssec/random.h"
#include "libknot/attribute.h"
#include "libknot/error.h"
+#include "libknot/quic/tls_common.h"
#include "libknot/quic/quic.h"
#include "libknot/xdp/tcp_iobuf.h"
#include "libknot/wire.h"
@@ -45,7 +46,7 @@ static int cmp_expiry_heap_nodes(void *c1, void *c2)
_public_
knot_quic_table_t *knot_quic_table_new(size_t max_conns, size_t max_ibufs, size_t max_obufs,
- size_t udp_payload, struct knot_quic_creds *creds)
+ size_t udp_payload, struct knot_creds *creds)
{
size_t table_size = max_conns * BUCKETS_PER_CONNS;
@@ -61,9 +62,17 @@ knot_quic_table_t *knot_quic_table_new(size_t max_conns, size_t max_ibufs, size_
res->obufs_max = max_obufs;
res->udp_payload_limit = udp_payload;
+ int ret = gnutls_priority_init2(&res->priority, KNOT_TLS_PRIORITIES, NULL,
+ GNUTLS_PRIORITY_INIT_DEF_APPEND);
+ if (ret != GNUTLS_E_SUCCESS) {
+ free(res);
+ return NULL;
+ }
+
res->expiry_heap = malloc(sizeof(struct heap));
if (res->expiry_heap == NULL || !heap_init(res->expiry_heap, cmp_expiry_heap_nodes, 0)) {
free(res->expiry_heap);
+ gnutls_priority_deinit(res->priority);
free(res);
return NULL;
}
@@ -92,6 +101,7 @@ void knot_quic_table_free(knot_quic_table_t *table)
assert(table->ibufs_size == 0);
assert(table->obufs_size == 0);
+ gnutls_priority_deinit(table->priority);
heap_deinit(table->expiry_heap);
free(table->expiry_heap);
free(table);
@@ -118,7 +128,9 @@ void knot_quic_table_sweep(knot_quic_table_t *table, struct knot_quic_reply *swe
while (!EMPTY_HEAP(table->expiry_heap)) {
knot_quic_conn_t *c = *(knot_quic_conn_t **)HHEAD(table->expiry_heap);
- if (table->usage > table->max_conns) {
+ if ((c->flags & KNOT_QUIC_CONN_BLOCKED)) {
+ break; // highly inprobable
+ } else if (table->usage > table->max_conns) {
knot_sweep_stats_incr(stats, KNOT_SWEEP_CTR_LIMIT_CONN);
send_excessive_load(c, sweep_reply, table);
knot_quic_table_rem(c, table);
@@ -476,7 +488,7 @@ uint8_t *knot_quic_stream_add_data(knot_quic_conn_t *conn, int64_t stream_id,
add_tail((list_t *)&s->outbufs, (node_t *)obuf);
s->obufs_size += obuf->len;
conn->obufs_size += obuf->len;
- conn->quic_table->obufs_size += obuf->len;
+ ATOMIC_ADD(conn->quic_table->obufs_size, obuf->len);
return obuf->buf + prefix;
}
@@ -497,7 +509,7 @@ void knot_quic_stream_ack_data(knot_quic_conn_t *conn, int64_t stream_id,
assert(HEAD(*obs) != first); // help CLANG analyzer understand what rem_node did and that further usage of HEAD(*obs) is safe
s->obufs_size -= first->len;
conn->obufs_size -= first->len;
- conn->quic_table->obufs_size -= first->len;
+ ATOMIC_SUB(conn->quic_table->obufs_size, first->len);
s->first_offset += first->len;
free(first);
if (s->unsent_obuf == first) {
@@ -556,6 +568,19 @@ void knot_quic_stream_mark_sent(knot_quic_conn_t *conn, int64_t stream_id,
}
_public_
+void knot_quic_conn_block(knot_quic_conn_t *conn, bool block)
+{
+ if (block) {
+ conn->flags |= KNOT_QUIC_CONN_BLOCKED;
+ conn->next_expiry = UINT64_MAX;
+ conn_heap_reschedule(conn, conn->quic_table);
+ } else {
+ conn->flags &= ~KNOT_QUIC_CONN_BLOCKED;
+ quic_conn_mark_used(conn, conn->quic_table);
+ }
+}
+
+_public_
void knot_quic_cleanup(knot_quic_conn_t *conns[], size_t n_conns)
{
for (size_t i = 0; i < n_conns; i++) {
diff --git a/src/libknot/quic/quic_conn.h b/src/libknot/quic/quic_conn.h
index 64ead51..49e0631 100644
--- a/src/libknot/quic/quic_conn.h
+++ b/src/libknot/quic/quic_conn.h
@@ -1,4 +1,4 @@
-/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/* Copyright (C) 2024 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
@@ -29,10 +29,13 @@
#include <stdint.h>
#include <sys/uio.h>
+#include "contrib/atomic.h"
+
#define MAX_STREAMS_PER_CONN 10 // this limits the number of un-finished streams per conn (i.e. if response has been recvd with FIN, it doesn't count)
+struct gnutls_priority_st;
struct ngtcp2_cid; // declaration taken from wherever in ngtcp2
-struct knot_quic_creds;
+struct knot_creds;
struct knot_quic_reply;
struct knot_sweep_stats;
@@ -70,6 +73,7 @@ typedef struct {
typedef enum {
KNOT_QUIC_CONN_HANDSHAKE_DONE = (1 << 0),
KNOT_QUIC_CONN_SESSION_TAKEN = (1 << 1),
+ KNOT_QUIC_CONN_BLOCKED = (1 << 2),
} knot_quic_conn_flag_t;
typedef struct knot_quic_conn {
@@ -111,12 +115,13 @@ typedef struct knot_quic_table {
size_t ibufs_max;
size_t obufs_max;
size_t ibufs_size;
- size_t obufs_size;
+ knot_atomic_size_t obufs_size;
size_t udp_payload_limit; // for simplicity not distinguishing IPv4/6
void (*log_cb)(const char *);
const char *qlog_dir;
uint64_t hash_secret[4];
- struct knot_quic_creds *creds;
+ struct knot_creds *creds;
+ struct gnutls_priority_st *priority;
struct heap *expiry_heap;
knot_quic_cid_t *conns[];
} knot_quic_table_t;
@@ -133,7 +138,7 @@ typedef struct knot_quic_table {
* \return Allocated table, or NULL.
*/
knot_quic_table_t *knot_quic_table_new(size_t max_conns, size_t max_ibufs, size_t max_obufs,
- size_t udp_payload, struct knot_quic_creds *creds);
+ size_t udp_payload, struct knot_creds *creds);
/*!
* \brief Free QUIC table including its contents.
@@ -306,6 +311,11 @@ void knot_quic_stream_mark_sent(knot_quic_conn_t *conn, int64_t stream_id,
size_t amount_sent);
/*!
+ * \brief (Un)block the connection for incoming/outgoing traffic and sweep.
+ */
+void knot_quic_conn_block(knot_quic_conn_t *conn, bool block);
+
+/*!
* \brief Free rest of resources of closed conns.
*
* \param conns Array with recently used conns (possibly NULLs).
diff --git a/src/libknot/quic/tls.c b/src/libknot/quic/tls.c
new file mode 100644
index 0000000..01172df
--- /dev/null
+++ b/src/libknot/quic/tls.c
@@ -0,0 +1,262 @@
+/* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <arpa/inet.h>
+#include <assert.h>
+#include <gnutls/crypto.h>
+#include <gnutls/gnutls.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include "libknot/quic/tls.h"
+
+#include "contrib/macros.h"
+#include "contrib/time.h"
+#include "libknot/attribute.h"
+#include "libknot/error.h"
+#include "libknot/quic/tls_common.h"
+
+_public_
+knot_tls_ctx_t *knot_tls_ctx_new(struct knot_creds *creds, unsigned io_timeout,
+ unsigned hs_timeout, bool server)
+{
+ knot_tls_ctx_t *res = calloc(1, sizeof(*res));
+ if (res == NULL) {
+ return NULL;
+ }
+
+ res->creds = creds;
+ res->handshake_timeout = hs_timeout;
+ res->io_timeout = io_timeout;
+ res->server = server;
+
+ int ret = gnutls_priority_init2(&res->priority, KNOT_TLS_PRIORITIES, NULL,
+ GNUTLS_PRIORITY_INIT_DEF_APPEND);
+ if (ret != GNUTLS_E_SUCCESS) {
+ free(res);
+ return NULL;
+ }
+
+ return res;
+}
+
+_public_
+void knot_tls_ctx_free(knot_tls_ctx_t *ctx)
+{
+ if (ctx != NULL) {
+ gnutls_priority_deinit(ctx->priority);
+ free(ctx);
+ }
+}
+
+_public_
+knot_tls_conn_t *knot_tls_conn_new(knot_tls_ctx_t *ctx, int sock_fd)
+{
+ knot_tls_conn_t *res = calloc(1, sizeof(*res));
+ if (res == NULL) {
+ return NULL;
+ }
+ res->ctx = ctx;
+ res->fd = sock_fd;
+
+ int ret = knot_tls_session(&res->session, ctx->creds, ctx->priority,
+ "\x03""dot", false, ctx->server);
+ if (ret != KNOT_EOK) {
+ goto fail;
+ }
+
+ gnutls_transport_set_int(res->session, sock_fd); // Use internal recv/send/poll.
+ gnutls_handshake_set_timeout(res->session, ctx->handshake_timeout);
+
+ return res;
+fail:
+ gnutls_deinit(res->session);
+ free(res);
+ return NULL;
+}
+
+_public_
+void knot_tls_conn_del(knot_tls_conn_t *conn)
+{
+ if (conn != NULL && conn->fd_clones_count-- < 1) {
+ gnutls_deinit(conn->session);
+ free(conn);
+ }
+}
+
+_public_
+int knot_tls_handshake(knot_tls_conn_t *conn, bool oneshot)
+{
+ if (conn->flags & (KNOT_TLS_CONN_HANDSHAKE_DONE | KNOT_TLS_CONN_BLOCKED)) {
+ return KNOT_EOK;
+ }
+
+ /* Check if NB socket is writeable. */
+ int opt;
+ socklen_t opt_len = sizeof(opt);
+ int ret = getsockopt(conn->fd, SOL_SOCKET, SO_ERROR, &opt, &opt_len);
+ if (ret < 0 || opt == ECONNREFUSED) {
+ return KNOT_NET_ECONNECT;
+ }
+
+ gnutls_record_set_timeout(conn->session, conn->ctx->io_timeout);
+ do {
+ ret = gnutls_handshake(conn->session);
+ } while (!oneshot && ret < 0 && gnutls_error_is_fatal(ret) == 0);
+
+ switch (ret) {
+ case GNUTLS_E_SUCCESS:
+ conn->flags |= KNOT_TLS_CONN_HANDSHAKE_DONE;
+ return knot_tls_pin_check(conn->session, conn->ctx->creds);
+ case GNUTLS_E_TIMEDOUT:
+ return KNOT_NET_ETIMEOUT;
+ default:
+ if (gnutls_error_is_fatal(ret) == 0) {
+ return KNOT_EAGAIN;
+ } else {
+ return KNOT_NET_EHSHAKE;
+ }
+ }
+}
+
+#define TIMEOUT_CTX_INIT \
+ struct timespec begin, end; \
+ if (*timeout_ptr > 0) { \
+ clock_gettime(CLOCK_MONOTONIC, &begin); \
+ }
+
+#define TIMEOUT_CTX_UPDATE \
+ if (*timeout_ptr > 0) { \
+ clock_gettime(CLOCK_MONOTONIC, &end); \
+ int running_ms = time_diff_ms(&begin, &end); \
+ *timeout_ptr = MAX(*timeout_ptr - running_ms, 0); \
+ }
+
+static ssize_t recv_data(knot_tls_conn_t *conn, void *data, size_t size, int *timeout_ptr)
+{
+ gnutls_record_set_timeout(conn->session, *timeout_ptr);
+
+ size_t total = 0;
+ ssize_t res;
+ while (total < size) {
+ TIMEOUT_CTX_INIT
+ res = gnutls_record_recv(conn->session, data + total, size - total);
+ if (res > 0) {
+ total += res;
+ } else if (res == 0) {
+ return KNOT_ECONNRESET;
+ } else if (gnutls_error_is_fatal(res) != 0) {
+ return KNOT_NET_ERECV;
+ }
+ TIMEOUT_CTX_UPDATE
+ gnutls_record_set_timeout(conn->session, *timeout_ptr);
+ }
+
+ assert(total == size);
+ return size;
+}
+
+_public_
+ssize_t knot_tls_recv_dns(knot_tls_conn_t *conn, void *data, size_t size)
+{
+ if (conn == NULL || data == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ if (conn->flags & KNOT_TLS_CONN_BLOCKED) {
+ return 0;
+ }
+
+ ssize_t ret = knot_tls_handshake(conn, false);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ int timeout = conn->ctx->io_timeout;
+
+ uint16_t msg_len;
+ ret = recv_data(conn, &msg_len, sizeof(msg_len), &timeout);
+ if (ret != sizeof(msg_len)) {
+ return ret;
+ }
+
+ msg_len = ntohs(msg_len);
+ if (size < msg_len) {
+ return KNOT_ESPACE;
+ }
+
+ ret = recv_data(conn, data, msg_len, &timeout);
+ if (ret != size) {
+ return ret;
+ }
+
+ return msg_len;
+}
+
+_public_
+ssize_t knot_tls_send_dns(knot_tls_conn_t *conn, void *data, size_t size)
+{
+ if (conn == NULL || data == NULL || size > UINT16_MAX) {
+ return KNOT_EINVAL;
+ }
+
+ ssize_t res = knot_tls_handshake(conn, false);
+ if (res != KNOT_EOK) {
+ return res;
+ }
+
+ // Enable data buffering.
+ gnutls_record_cork(conn->session);
+
+ uint16_t msg_len = htons(size);
+ res = gnutls_record_send(conn->session, &msg_len, sizeof(msg_len));
+ if (res != sizeof(msg_len)) {
+ return KNOT_NET_ESEND;
+ }
+
+ res = gnutls_record_send(conn->session, data, size);
+ if (res != size) {
+ return KNOT_NET_ESEND;
+ }
+
+ int timeout = conn->ctx->io_timeout, *timeout_ptr = &timeout;
+ gnutls_record_set_timeout(conn->session, timeout);
+
+ // Send the buffered data.
+ while (gnutls_record_check_corked(conn->session) > 0) {
+ TIMEOUT_CTX_INIT
+ int ret = gnutls_record_uncork(conn->session, 0);
+ if (ret < 0 && gnutls_error_is_fatal(ret) != 0) {
+ return ret == GNUTLS_E_TIMEDOUT ? KNOT_ETIMEOUT :
+ KNOT_NET_ESEND;
+ }
+ TIMEOUT_CTX_UPDATE
+ gnutls_record_set_timeout(conn->session, timeout);
+ }
+
+ return size;
+}
+
+_public_
+void knot_tls_conn_block(knot_tls_conn_t *conn, bool block)
+{
+ if (block) {
+ conn->flags |= KNOT_TLS_CONN_BLOCKED;
+ } else {
+ conn->flags &= ~KNOT_TLS_CONN_BLOCKED;
+ }
+}
diff --git a/src/libknot/quic/tls.h b/src/libknot/quic/tls.h
new file mode 100644
index 0000000..7801ca8
--- /dev/null
+++ b/src/libknot/quic/tls.h
@@ -0,0 +1,135 @@
+/* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
+ */
+
+/*!
+ * \file
+ *
+ * \brief Pure TLS functionality.
+ *
+ * \addtogroup quic
+ * @{
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+struct gnutls_priority_st;
+
+typedef enum {
+ KNOT_TLS_CONN_HANDSHAKE_DONE = (1 << 0),
+ KNOT_TLS_CONN_SESSION_TAKEN = (1 << 1), // unused, to be implemeted later
+ KNOT_TLS_CONN_BLOCKED = (1 << 2),
+} knot_tls_conn_flag_t;
+
+typedef struct knot_tls_ctx {
+ struct knot_creds *creds;
+ struct gnutls_priority_st *priority;
+ unsigned handshake_timeout;
+ unsigned io_timeout;
+ bool server;
+} knot_tls_ctx_t;
+
+typedef struct knot_tls_conn {
+ struct gnutls_session_int *session;
+ struct knot_tls_ctx *ctx;
+ int fd;
+ unsigned fd_clones_count;
+ knot_tls_conn_flag_t flags;
+} knot_tls_conn_t;
+
+/*!
+ * \brief Initialize DoT answering context.
+ *
+ * \param creds Certificate credentials.
+ * \param io_timeout Connections' IO-timeout (in milliseconds).
+ * \param hs_timeout Handshake timeout (in milliseconds).
+ * \param server Server context (otherwise client).
+ *
+ * \return Initialized context or NULL.
+ */
+knot_tls_ctx_t *knot_tls_ctx_new(struct knot_creds *creds, unsigned io_timeout,
+ unsigned hs_timeout, bool server);
+
+/*!
+ * \brief Free DoT answering context.
+ */
+void knot_tls_ctx_free(knot_tls_ctx_t *ctx);
+
+/*!
+ * \brief Initialize DoT connection.
+ *
+ * \param ctx DoT answering context.
+ * \param sock_fd Opened TCP connection socket.
+ *
+ * \return Connection struct or NULL.
+ */
+knot_tls_conn_t *knot_tls_conn_new(knot_tls_ctx_t *ctx, int sock_fd);
+
+/*!
+ * \brief Free DoT connection struct.
+ *
+ * \note Doesn't close the TCP connection socket.
+ */
+void knot_tls_conn_del(knot_tls_conn_t *conn);
+
+/*!
+ * \brief Perform the TLS handshake (via gnutls_handshake()).
+ *
+ * \note This is also done by the recv/send functions.
+ *
+ * \param conn DoT connection.
+ * \param oneshot If set, don't wait untill the handshake is finished.
+ *
+ * \retval KNOT_EOK Handshake successfully finished.
+ * \retval KNOT_EGAIN Handshake not finished, call me again.
+ * \retval KNOT_NET_EHSHAKE Handshake error.
+ * \retval KNOT_NET_ECONNECT Socket not connected.
+ */
+int knot_tls_handshake(knot_tls_conn_t *conn, bool oneshot);
+
+/*!
+ * \brief Receive a size-word-prefixed DNS message.
+ *
+ * \param conn DoT connection.
+ * \param data Destination buffer.
+ * \param size Maximum buffer size.
+ *
+ * \return Either the DNS message size received or negative error code.
+ *
+ * \note The two-byte-size-prefix is stripped upon reception, not stored to the buffer.
+ */
+ssize_t knot_tls_recv_dns(knot_tls_conn_t *conn, void *data, size_t size);
+
+/*!
+ * \brief Send a size-word-prefixed DNS message.
+ *
+ * \param conn DoT connection.
+ * \param data DNS payload.
+ * \param size Payload size.
+ *
+ * \return Either exactly 'size' or a negative error code.
+ */
+ssize_t knot_tls_send_dns(knot_tls_conn_t *conn, void *data, size_t size);
+
+/*!
+ * \brief Set or unset the conection's BLOCKED flag.
+ */
+void knot_tls_conn_block(knot_tls_conn_t *conn, bool block);
+
+/*! @} */
diff --git a/src/libknot/quic/tls_common.c b/src/libknot/quic/tls_common.c
new file mode 100644
index 0000000..d1647d8
--- /dev/null
+++ b/src/libknot/quic/tls_common.c
@@ -0,0 +1,472 @@
+/* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <fcntl.h>
+#include <gnutls/crypto.h>
+#include <gnutls/gnutls.h>
+#include <gnutls/x509.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "libknot/quic/tls_common.h"
+
+#include "contrib/atomic.h"
+#include "contrib/sockaddr.h"
+#include "contrib/string.h"
+#include "libknot/attribute.h"
+#include "libknot/error.h"
+
+typedef struct knot_creds {
+ knot_atomic_ptr_t cert_creds; // Current credentials.
+ gnutls_certificate_credentials_t cert_creds_prev; // Previous credentials (for pending connections).
+ gnutls_anti_replay_t tls_anti_replay;
+ gnutls_datum_t tls_ticket_key;
+ bool peer;
+ uint8_t peer_pin_len;
+ uint8_t peer_pin[];
+} knot_creds_t;
+
+static int tls_anti_replay_db_add_func(void *dbf, time_t exp_time,
+ const gnutls_datum_t *key,
+ const gnutls_datum_t *data)
+{
+ return 0;
+}
+
+static void tls_session_ticket_key_free(gnutls_datum_t *ticket)
+{
+ memzero(ticket->data, ticket->size);
+ gnutls_free(ticket->data);
+}
+
+static int self_key(gnutls_x509_privkey_t *privkey, const char *key_file)
+{
+ gnutls_datum_t data = { 0 };
+
+ int ret = gnutls_x509_privkey_init(privkey);
+ if (ret != GNUTLS_E_SUCCESS) {
+ return ret;
+ }
+
+ int fd = open(key_file, O_RDONLY);
+ if (fd != -1) {
+ struct stat stat;
+ if (fstat(fd, &stat) != 0 ||
+ (data.data = gnutls_malloc(stat.st_size)) == NULL ||
+ read(fd, data.data, stat.st_size) != stat.st_size) {
+ ret = GNUTLS_E_KEYFILE_ERROR;
+ goto finish;
+ }
+
+ data.size = stat.st_size;
+ ret = gnutls_x509_privkey_import_pkcs8(*privkey, &data, GNUTLS_X509_FMT_PEM,
+ NULL, GNUTLS_PKCS_PLAIN);
+ if (ret != GNUTLS_E_SUCCESS) {
+ goto finish;
+ }
+ } else {
+ ret = gnutls_x509_privkey_generate(*privkey, GNUTLS_PK_EDDSA_ED25519,
+ GNUTLS_CURVE_TO_BITS(GNUTLS_ECC_CURVE_ED25519), 0);
+ if (ret != GNUTLS_E_SUCCESS) {
+ goto finish;
+ }
+
+ ret = gnutls_x509_privkey_export2_pkcs8(*privkey, GNUTLS_X509_FMT_PEM, NULL,
+ GNUTLS_PKCS_PLAIN, &data);
+ if (ret != GNUTLS_E_SUCCESS ||
+ (fd = open(key_file, O_WRONLY | O_CREAT, 0600)) == -1 ||
+ write(fd, data.data, data.size) != data.size) {
+ ret = GNUTLS_E_KEYFILE_ERROR;
+ goto finish;
+ }
+ }
+
+finish:
+ if (fd > -1) {
+ close(fd);
+ }
+ gnutls_free(data.data);
+ if (ret != GNUTLS_E_SUCCESS) {
+ gnutls_x509_privkey_deinit(*privkey);
+ *privkey = NULL;
+ }
+ return ret;
+}
+
+static int self_signed_cert(gnutls_certificate_credentials_t tls_cert,
+ const char *key_file)
+{
+ gnutls_x509_privkey_t privkey = NULL;
+ gnutls_x509_crt_t cert = NULL;
+
+ char *hostname = sockaddr_hostname();
+ if (hostname == NULL) {
+ return GNUTLS_E_MEMORY_ERROR;
+ }
+
+ int ret;
+ uint8_t serial[16];
+ gnutls_rnd(GNUTLS_RND_NONCE, serial, sizeof(serial));
+ // Clear the left-most bit to be a positive number (two's complement form).
+ serial[0] &= 0x7F;
+
+#define CHK(cmd) if ((ret = (cmd)) != GNUTLS_E_SUCCESS) { goto finish; }
+#define NOW_DAYS(days) (time(NULL) + 24 * 3600 * (days))
+
+ CHK(self_key(&privkey, key_file));
+
+ CHK(gnutls_x509_crt_init(&cert));
+ CHK(gnutls_x509_crt_set_version(cert, 3));
+ CHK(gnutls_x509_crt_set_serial(cert, serial, sizeof(serial)));
+ CHK(gnutls_x509_crt_set_activation_time(cert, NOW_DAYS(-1)));
+ CHK(gnutls_x509_crt_set_expiration_time(cert, NOW_DAYS(10 * 365)));
+ CHK(gnutls_x509_crt_set_dn_by_oid(cert, GNUTLS_OID_X520_COMMON_NAME, 0,
+ hostname, strlen(hostname)));
+ CHK(gnutls_x509_crt_set_key(cert, privkey));
+ CHK(gnutls_x509_crt_sign2(cert, cert, privkey, GNUTLS_DIG_SHA512, 0));
+
+ ret = gnutls_certificate_set_x509_key(tls_cert, &cert, 1, privkey);
+
+finish:
+ free(hostname);
+ gnutls_x509_crt_deinit(cert);
+ gnutls_x509_privkey_deinit(privkey);
+
+ return ret;
+}
+
+_public_
+struct knot_creds *knot_creds_init(const char *key_file, const char *cert_file)
+{
+ knot_creds_t *creds = calloc(1, sizeof(*creds));
+ if (creds == NULL) {
+ return NULL;
+ }
+
+ int ret = knot_creds_update(creds, key_file, cert_file);
+ if (ret != KNOT_EOK) {
+ goto fail;
+ }
+
+ ret = gnutls_anti_replay_init(&creds->tls_anti_replay);
+ if (ret != GNUTLS_E_SUCCESS) {
+ goto fail;
+ }
+ gnutls_anti_replay_set_add_function(creds->tls_anti_replay, tls_anti_replay_db_add_func);
+ gnutls_anti_replay_set_ptr(creds->tls_anti_replay, NULL);
+
+ ret = gnutls_session_ticket_key_generate(&creds->tls_ticket_key);
+ if (ret != GNUTLS_E_SUCCESS) {
+ goto fail;
+ }
+
+ return creds;
+fail:
+ knot_creds_free(creds);
+ return NULL;
+}
+
+_public_
+struct knot_creds *knot_creds_init_peer(const struct knot_creds *local_creds,
+ const uint8_t *peer_pin,
+ uint8_t peer_pin_len)
+{
+ knot_creds_t *creds = calloc(1, sizeof(*creds) + peer_pin_len);
+ if (creds == NULL) {
+ return NULL;
+ }
+
+ if (local_creds != NULL) {
+ creds->peer = true;
+ creds->cert_creds = ATOMIC_GET(local_creds->cert_creds);
+ } else {
+ gnutls_certificate_credentials_t new_creds;
+ int ret = gnutls_certificate_allocate_credentials(&new_creds);
+ if (ret != GNUTLS_E_SUCCESS) {
+ free(creds);
+ return NULL;
+ }
+ creds->cert_creds = new_creds;
+ }
+
+ if (peer_pin_len > 0 && peer_pin != NULL) {
+ memcpy(creds->peer_pin, peer_pin, peer_pin_len);
+ creds->peer_pin_len = peer_pin_len;
+ }
+
+ return creds;
+}
+
+static int creds_cert(gnutls_certificate_credentials_t creds,
+ struct gnutls_x509_crt_int **cert)
+{
+ gnutls_x509_crt_t *certs;
+ unsigned cert_count;
+ int ret = gnutls_certificate_get_x509_crt(creds, 0, &certs, &cert_count);
+ if (ret == GNUTLS_E_SUCCESS) {
+ if (cert_count == 0) {
+ gnutls_x509_crt_deinit(*certs);
+ return KNOT_ENOENT;
+ }
+ *cert = *certs;
+ free(certs);
+ return KNOT_EOK;
+ }
+ return KNOT_ERROR;
+}
+
+static int creds_changed(gnutls_certificate_credentials_t creds,
+ gnutls_certificate_credentials_t prev,
+ bool self_cert, bool *changed)
+{
+ if (creds == NULL || prev == NULL) {
+ *changed = true;
+ return KNOT_EOK;
+ }
+
+ gnutls_x509_crt_t cert = NULL, cert_prev = NULL;
+
+ int ret = creds_cert(creds, &cert);
+ if (ret != KNOT_EOK) {
+ goto failed;
+ }
+ ret = creds_cert(prev, &cert_prev);
+ if (ret != KNOT_EOK) {
+ goto failed;
+ }
+
+ if (self_cert) {
+ uint8_t pin[KNOT_TLS_PIN_LEN], pin_prev[KNOT_TLS_PIN_LEN];
+ size_t pin_size = sizeof(pin), pin_prev_size = sizeof(pin_prev);
+
+ ret = gnutls_x509_crt_get_key_id(cert, GNUTLS_KEYID_USE_SHA256,
+ pin, &pin_size);
+ if (ret != KNOT_EOK) {
+ goto failed;
+ }
+ ret = gnutls_x509_crt_get_key_id(cert_prev, GNUTLS_KEYID_USE_SHA256,
+ pin_prev, &pin_prev_size);
+ if (ret != KNOT_EOK) {
+ goto failed;
+ }
+
+ *changed = (pin_size != pin_prev_size) ||
+ memcmp(pin, pin_prev, pin_size) != 0;
+ } else {
+ *changed = (gnutls_x509_crt_equals(cert, cert_prev) == 0);
+ }
+
+ ret = KNOT_EOK;
+failed:
+ gnutls_x509_crt_deinit(cert);
+ gnutls_x509_crt_deinit(cert_prev);
+
+ return ret;
+}
+
+_public_
+int knot_creds_update(struct knot_creds *creds, const char *key_file, const char *cert_file)
+{
+ if (creds == NULL || key_file == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ gnutls_certificate_credentials_t new_creds;
+ int ret = gnutls_certificate_allocate_credentials(&new_creds);
+ if (ret != GNUTLS_E_SUCCESS) {
+ return KNOT_ENOMEM;
+ }
+
+ if (cert_file != NULL) {
+ ret = gnutls_certificate_set_x509_key_file(new_creds,
+ cert_file, key_file,
+ GNUTLS_X509_FMT_PEM);
+ } else {
+ ret = self_signed_cert(new_creds, key_file);
+ }
+ if (ret != GNUTLS_E_SUCCESS) {
+ gnutls_certificate_free_credentials(new_creds);
+ return KNOT_EFILE;
+ }
+
+ bool changed = false;
+ ret = creds_changed(new_creds, ATOMIC_GET(creds->cert_creds),
+ cert_file == NULL, &changed);
+ if (ret != KNOT_EOK) {
+ gnutls_certificate_free_credentials(new_creds);
+ return ret;
+ }
+
+ if (changed) {
+ if (creds->cert_creds_prev != NULL) {
+ gnutls_certificate_free_credentials(creds->cert_creds_prev);
+ }
+ creds->cert_creds_prev = ATOMIC_XCHG(creds->cert_creds, new_creds);
+ } else {
+ gnutls_certificate_free_credentials(new_creds);
+ }
+
+ return KNOT_EOK;
+}
+
+_public_
+int knot_creds_cert(struct knot_creds *creds, struct gnutls_x509_crt_int **cert)
+{
+ if (creds == NULL || cert == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ return creds_cert(ATOMIC_GET(creds->cert_creds), cert);
+}
+
+_public_
+void knot_creds_free(struct knot_creds *creds)
+{
+ if (creds == NULL) {
+ return;
+ }
+
+ if (!creds->peer && creds->cert_creds != NULL) {
+ gnutls_certificate_free_credentials(creds->cert_creds);
+ if (creds->cert_creds_prev != NULL) {
+ gnutls_certificate_free_credentials(creds->cert_creds_prev);
+ }
+ }
+ gnutls_anti_replay_deinit(creds->tls_anti_replay);
+ if (creds->tls_ticket_key.data != NULL) {
+ tls_session_ticket_key_free(&creds->tls_ticket_key);
+ }
+ free(creds);
+}
+
+_public_
+int knot_tls_session(struct gnutls_session_int **session,
+ struct knot_creds *creds,
+ struct gnutls_priority_st *priority,
+ const char *alpn,
+ bool early_data,
+ bool server)
+{
+ if (session == NULL || creds == NULL || priority == NULL || alpn == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ gnutls_init_flags_t flags = GNUTLS_NO_SIGNAL;
+ if (early_data) {
+ flags |= GNUTLS_ENABLE_EARLY_DATA;
+#ifdef ENABLE_QUIC // Next flags aren't available in older GnuTLS versions.
+ flags |= GNUTLS_NO_AUTO_SEND_TICKET | GNUTLS_NO_END_OF_EARLY_DATA;
+#endif
+ }
+
+ int ret = gnutls_init(session, (server ? GNUTLS_SERVER : GNUTLS_CLIENT) | flags);
+ if (ret == GNUTLS_E_SUCCESS) {
+ gnutls_certificate_send_x509_rdn_sequence(*session, 1);
+ gnutls_certificate_server_set_request(*session, GNUTLS_CERT_REQUEST);
+ ret = gnutls_priority_set(*session, priority);
+ }
+ if (server && ret == GNUTLS_E_SUCCESS) {
+ ret = gnutls_session_ticket_enable_server(*session, &creds->tls_ticket_key);
+ }
+ if (ret == GNUTLS_E_SUCCESS) {
+ const gnutls_datum_t alpn_datum = { (void *)&alpn[1], alpn[0] };
+ gnutls_alpn_set_protocols(*session, &alpn_datum, 1, GNUTLS_ALPN_MANDATORY);
+ if (early_data) {
+ gnutls_record_set_max_early_data_size(*session, 0xffffffffu);
+ }
+ if (server) {
+ gnutls_anti_replay_enable(*session, creds->tls_anti_replay);
+ }
+ ret = gnutls_credentials_set(*session, GNUTLS_CRD_CERTIFICATE,
+ ATOMIC_GET(creds->cert_creds));
+ }
+ if (ret != GNUTLS_E_SUCCESS) {
+ gnutls_deinit(*session);
+ *session = NULL;
+ }
+ return ret == GNUTLS_E_SUCCESS ? KNOT_EOK : KNOT_ERROR;
+}
+
+_public_
+void knot_tls_pin(struct gnutls_session_int *session, uint8_t *pin,
+ size_t *pin_size, bool local)
+{
+ if (session == NULL) {
+ goto error;
+ }
+
+ const gnutls_datum_t *data = NULL;
+ if (local) {
+ data = gnutls_certificate_get_ours(session);
+ } else {
+ unsigned count = 0;
+ data = gnutls_certificate_get_peers(session, &count);
+ if (count == 0) {
+ goto error;
+ }
+ }
+ if (data == NULL) {
+ goto error;
+ }
+
+ gnutls_x509_crt_t cert;
+ int ret = gnutls_x509_crt_init(&cert);
+ if (ret != GNUTLS_E_SUCCESS) {
+ goto error;
+ }
+
+ ret = gnutls_x509_crt_import(cert, data, GNUTLS_X509_FMT_DER);
+ if (ret != GNUTLS_E_SUCCESS) {
+ gnutls_x509_crt_deinit(cert);
+ goto error;
+ }
+
+ ret = gnutls_x509_crt_get_key_id(cert, GNUTLS_KEYID_USE_SHA256, pin, pin_size);
+ if (ret != GNUTLS_E_SUCCESS) {
+ gnutls_x509_crt_deinit(cert);
+ goto error;
+ }
+
+ gnutls_x509_crt_deinit(cert);
+
+ return;
+error:
+ if (pin_size != NULL) {
+ *pin_size = 0;
+ }
+}
+
+_public_
+int knot_tls_pin_check(struct gnutls_session_int *session,
+ struct knot_creds *creds)
+{
+ if (creds->peer_pin_len == 0) {
+ return KNOT_EOK;
+ }
+
+ uint8_t pin[KNOT_TLS_PIN_LEN];
+ size_t pin_size = sizeof(pin);
+ knot_tls_pin(session, pin, &pin_size, false);
+ if (pin_size != creds->peer_pin_len ||
+ const_time_memcmp(pin, creds->peer_pin, pin_size) != 0) {
+ return KNOT_EBADCERTKEY;
+ }
+
+ return KNOT_EOK;
+}
diff --git a/src/libknot/quic/tls_common.h b/src/libknot/quic/tls_common.h
new file mode 100644
index 0000000..934f256
--- /dev/null
+++ b/src/libknot/quic/tls_common.h
@@ -0,0 +1,134 @@
+/* Copyright (C) 2024 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 <https://www.gnu.org/licenses/>.
+ */
+
+/*!
+ * \file
+ *
+ * \brief Credentials handling common to QUIC and TLS.
+ *
+ * \addtogroup quic
+ * @{
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#define KNOT_TLS_PIN_LEN 32
+#define KNOT_TLS_PRIORITIES "-VERS-ALL:+VERS-TLS1.3:" \
+ "-GROUP-ALL:+GROUP-X25519:+GROUP-SECP256R1:" \
+ "+GROUP-SECP384R1:+GROUP-SECP521R1"
+
+struct gnutls_priority_st;
+struct gnutls_session_int;
+struct gnutls_x509_crt_int;
+struct knot_creds;
+
+/*!
+ * \brief Init server TLS key and certificate for DoQ.
+ *
+ * \param key_file Key PEM file path/name.
+ * \param cert_file X509 certificate PEM file path/name (NULL if auto-generated).
+ *
+ * \return Initialized creds.
+ */
+struct knot_creds *knot_creds_init(const char *key_file, const char *cert_file);
+
+/*!
+ * \brief Init peer TLS key and certificate for DoQ.
+ *
+ * \param local_creds Local credentials if server.
+ * \param peer_pin Optional peer certificate pin to check.
+ * \param peer_pin_len Length of the peer pin. Set 0 if not specified.
+ *
+ * \return Initialized creds.
+ */
+struct knot_creds *knot_creds_init_peer(const struct knot_creds *local_creds,
+ const uint8_t *peer_pin,
+ uint8_t peer_pin_len);
+
+/*!
+ * \brief Load new server TLS key and certificate for DoQ.
+ *
+ * \param creds Server credentials where key/cert pair will be updated.
+ * \param key_file Key PEM file path/name.
+ * \param cert_file X509 certificate PEM file path/name (NULL if auto-generated).
+ *
+ * \return KNOT_E*
+ */
+int knot_creds_update(struct knot_creds *creds, const char *key_file, const char *cert_file);
+
+/*!
+ * \brief Gets the certificate from credentials.
+ *
+ * \param creds TLS credentials.
+ * \param cert Output certificate.
+ *
+ * \return KNOT_E*
+ */
+int knot_creds_cert(struct knot_creds *creds, struct gnutls_x509_crt_int **cert);
+
+/*!
+ * \brief Deinit server TLS certificate for DoQ.
+ */
+void knot_creds_free(struct knot_creds *creds);
+
+/*!
+ * \brief Initialize GnuTLS session with credentials, ALPN, etc.
+ *
+ * \param session Out: initialized GnuTLS session struct.
+ * \param creds Certificate credentials.
+ * \param priority Session priority configuration.
+ * \param alpn ALPN string, first byte is the string length.
+ * \param early_data Allow early data.
+ * \param server Should be server session (otherwise client).
+ *
+ * \return KNOT_E*
+ */
+int knot_tls_session(struct gnutls_session_int **session,
+ struct knot_creds *creds,
+ struct gnutls_priority_st *priority,
+ const char *alpn,
+ bool early_data,
+ bool server);
+
+/*!
+ * \brief Gets local or remote certificate pin.
+ *
+ * \note Zero output pin_size value means no certificate available or error.
+ *
+ * \param session TLS connection.
+ * \param pin Output certificate pin.
+ * \param pin_size Input size of the storage / output size of the stored pin.
+ * \param local Local or remote certificate indication.
+ */
+void knot_tls_pin(struct gnutls_session_int *session, uint8_t *pin,
+ size_t *pin_size, bool local);
+
+/*!
+ * \brief Checks remote certificate pin in the session against credentials.
+ *
+ * \param session TLS connection.
+ * \param creds TLS credentials.
+ *
+ * \return KNOT_EOK or KNOT_EBADCERTKEY
+ */
+int knot_tls_pin_check(struct gnutls_session_int *session,
+ struct knot_creds *creds);
+
+/*! @} */