From: Jan Doskočil Date: Wed, 14 May 2025 11:03:39 +0000 (+0200) Subject: conf: implemented certificate hostname validation X-Git-Tag: v3.5.0~60^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=261c378b74d6ae3e7b799e5dc96157c0d7d8809a;p=thirdparty%2Fknot-dns.git conf: implemented certificate hostname validation --- diff --git a/distro/pkg/deb/libknot15.symbols b/distro/pkg/deb/libknot15.symbols index ee8452a438..0c7250b550 100644 --- a/distro/pkg/deb/libknot15.symbols +++ b/distro/pkg/deb/libknot15.symbols @@ -193,6 +193,8 @@ libknot.so.15 libknot15 #MINVER# knot_tcp_sweep@Base 3.4.0 knot_tcp_table_free@Base 3.4.0 knot_tcp_table_new@Base 3.4.0 + knot_tls_cert_check@Base 3.5.0 + knot_tls_cert_check_hostnames@Base 3.5.0 knot_tls_conn_block@Base 3.4.0 knot_tls_conn_del@Base 3.4.0 knot_tls_conn_new@Base 3.4.0 diff --git a/doc/configuration.rst b/doc/configuration.rst index 24d8d1ebeb..5cd51faee9 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -981,6 +981,8 @@ Strict authentication: Note that the automatic ACL doesn't work in this case due to asymmetrical configuration. The secondary can authenticate using TSIG. +With PIN checks: + .. panels:: Primary: @@ -1042,6 +1044,72 @@ configuration. The secondary can authenticate using TSIG. master: primary acl: primary_notify +With CA and hostname checks: + +.. panels:: + + Primary + + .. code-block:: console + + server: + listen-quic: ::1 + cert-file: primary-cert.pem + key-file: primary-key.pem + + key: + - id: secondary_key + algorithm: hmac-sha256 + secret: S059OFJv1SCDdR2P6JKENgWaM409iq2X44igcJdERhc= + + remote: + - id: secondary + address: ::2 + quic: on + + acl: + - id: secondary_xfr + address: ::2 + key: secondary_key # TSIG for secondary authentication + action: transfer + + zone: + - domain: example.com + notify: secondary + acl: secondary_xfr + + --- + + Secondary: + + .. code-block:: console + + server: + listen-quic: ::2 + ca-file: ca-cert.pem + + key: + - id: secondary_key + algorithm: hmac-sha256 + secret: S059OFJv1SCDdR2P6JKENgWaM409iq2X44igcJdERhc= + + remote: + - id: primary + address: ::1 + key: secondary_key # TSIG for secondary authentication + quic: on + + acl: + - id: primary_notify + address: ::1 + cert-hostname: "Primary Knot" + action: notify + + zone: + - domain: example.com + master: primary + acl: primary_notify + Mutual authentication: ...................... @@ -1049,6 +1117,8 @@ The :rfc:`mutual authentication<9103#section-9.3.3>` guarantees authentication for both the primary and the secondary. In this case, TSIG would be redundant. This mode is recommended if possible. +With PIN checks: + .. panels:: Primary: @@ -1089,11 +1159,102 @@ This mode is recommended if possible. - domain: example.com master: primary +With CA and hostname checks: + +.. panels:: + + Primary: + + .. code-block:: console + + server: + listen-quic: ::1 + ca-file: ca-cert.pem + cert-file: primary-cert.pem + key-file: primary-key.pem + automatic-acl: on + + remote: + - id: secondary + address: ::2 + quic: on + cert-hostname: "Secondary Knot" + + zone: + - domain: example.com + notify: secondary + + --- + + Secondary: + + .. code-block:: console + + server: + listen-quic: ::2 + ca-file: ca-cert.pem + cert-file: secondary-cert.pem + key-file: secondary-key.pem + automatic-acl: on + + remote: + - id: primary + address: ::1 + quic: on + cert-hostname: "Primary Knot" + + zone: + - domain: example.com + master: primary + +.. TIP:: + + Using GnuTLS certtool you can generate a CA certificate with its private key: + + .. code-block:: console + + $ certtool --generate-privkey --key-type ed25519 --outfile ca-key.pem + $ echo -e "cn = \"My Example CA\"\nca\ncert_signing_key\nexpiration_days = 3650" >ca-template.info + $ certtool --generate-self-signed --load-privkey ca-key.pem \ + --template ca-template.info --outfile ca-cert.pem + + Then create certificates signed with this CA like so: + + .. code-block:: console + + $ CERT_NAME="primary" + $ certtool --generate-privkey --key-type ed25519 --outfile ${CERT_NAME}-key.pem + $ echo -e "dns_name = \"${CERT_NAME} server\"\nexpiration_days = 365" >${CERT_NAME}-template.info + $ certtool --generate-certificate --load-privkey ${CERT_NAME}-key.pem \ + --load-ca-certificate ca-cert.pem --load-ca-privkey ca-key.pem \ + --template ${CERT_NAME}-template.info --outfile ${CERT_NAME}-cert.pem + + If you want to use a wildcard DNSName in your certificate, beware that + GnuTLS, which is the TLS backend for Knot DNS, **will not verify** wildcard + names directly under TLDs (like ``*.example``). + + To see a server's TLS hostnames: + + .. code-block:: console + + $ kdig @1.1.1.1 +tls -dd + ;; DEBUG: Querying for owner(.), class(1), type(2), server(1.1.1.1), port(853), protocol(TCP) + ;; DEBUG: TLS, received certificate hierarchy: + ;; DEBUG: #1, CN=cloudflare-dns.com,O=Cloudflare\, Inc.,L=San Francisco,ST=California,C=US + ;; DEBUG: Subject Alternative Name: + ;; DEBUG: DNSname: cloudflare-dns.com + ;; DEBUG: DNSname: *.cloudflare-dns.com + ;; DEBUG: DNSname: one.one.one.one + [...] + + Knot DNS will only verify hostnames under the *Subject Alternative Name* + extension in compliance with :rfc:`8310#section-8.1`. + .. NOTE:: - Instead of certificate verification with specified authentication domain name, - Knot DNS uses certificate public key pinning. This approach has much lower - overhead and in most cases simplifies configuration and certificate management. + Certificate validation by CA and hostname is more computationally expensive + than by PIN, but PIN checking has the disadvantage of relying on constantness + of the public key. .. _DNS_over_TLS: diff --git a/doc/reference.rst b/doc/reference.rst index bf57d88f7e..c22b69acc7 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -210,6 +210,7 @@ General options related to the server. udp-max-payload-ipv6: SIZE key-file: STR cert-file: STR + ca-file: STR ... edns-client-subnet: BOOL answer-rotation: BOOL automatic-acl: BOOL @@ -582,6 +583,17 @@ A non-absolute path is relative to the :file:`@config_dir@` directory. *Default:* one-time in-memory certificate +.. _server_ca-file: + +ca-file +------- + +Specifies one or more paths to load trusted Certificate Authorities (CAs) from. +An empty string ("") means the system’s default trusted CAs. The loaded CAs are used +for remote certificate validation (:ref:`acl_cert-hostname` and :ref:`remote_cert-hostname`). + +*Default:* not set + .. _server_edns-client-subnet: edns-client-subnet @@ -1457,6 +1469,7 @@ transfer, target for a notification, etc.). tls: BOOL key: key_id cert-key: BASE64 ... + cert-hostname: STR ... block-notify-after-transfer: BOOL no-edns: BOOL automatic-acl: BOOL @@ -1561,17 +1574,29 @@ the communication with the remote server. cert-key -------- -An ordered list of remote certificate public key PINs. If the list is non-empty, -communication with the remote is possible only via QUIC or TLS protocols and +An ordered list of up to 4 remote certificate public key PINs. If the list is non-empty, +communication with the remote is only possible via QUIC or TLS protocols, and a peer certificate is required. The peer certificate key must match one of the specified PINs. A PIN is a unique identifier that represents the public key of the peer certificate. -It's a base64-encoded SHA-256 hash of the public key. This identifier +It's a base64-encoded SHA-256 hash of the public key. This identifier usually remains the same on a certificate renewal. *Default:* not set +.. _remote_cert-hostname: + +cert-hostname +------------- + +An ordered list of up to 4 hostnames to match against peer's certificate. At least +one must match for successful certificate validation (see :ref:`server_ca-file`). +If the list is non-empty, communication with the remote is only possible via +QUIC or TLS protocols, and a peer certificate is required. + +*Default:* not set + .. _remote_block-notify-after-transfer: block-notify-after-transfer @@ -1665,6 +1690,7 @@ which don't require authorization are always allowed. address: ADDR[/INT] | ADDR-ADDR | STR ... key: key_id ... cert-key: BASE64 ... + cert-hostname: STR ... remote: remote_id | remotes_id ... action: query | notify | transfer | update ... protocol: udp | tcp | tls | quic ... @@ -1709,16 +1735,28 @@ cert-key -------- An ordered list of remote certificate public key PINs. If the list is non-empty, -communication with the remote is possible only via QUIC or TLS protocols and +communication with the remote is only possible via QUIC or TLS protocols, and a peer certificate is required. The peer certificate key must match one of the specified PINs. A PIN is a unique identifier that represents the public key of the peer certificate. -It's a base64-encoded SHA-256 hash of the public key. This identifier +It's a base64-encoded SHA-256 hash of the public key. This identifier usually remains the same on a certificate renewal. *Default:* not set +.. _acl_cert-hostname: + +cert-hostname +------------- + +An ordered list of hostnames to match against peer's certificate. At least one +must match for successful certificate validation (see :ref:`server_ca-file`). +If the list is non-empty, communication with the remote is only possible via +QUIC or TLS protocols, and a peer certificate is required. + +*Default:* not set + .. _acl_remote: remote diff --git a/src/knot/conf/conf.c b/src/knot/conf/conf.c index b3f60d8d6e..c6e920b6d9 100644 --- a/src/knot/conf/conf.c +++ b/src/knot/conf/conf.c @@ -1380,6 +1380,10 @@ conf_remote_t conf_remote_txn( out.quic = conf_bool(&val); val = conf_id_get_txn(conf, txn, C_RMT, C_TLS, id); out.tls = conf_bool(&val); + val = conf_id_get_txn(conf, txn, C_RMT, C_NO_EDNS, id); + out.no_edns = conf_bool(&val); + val = conf_id_get_txn(conf, txn, C_RMT, C_BLOCK_NOTIFY_XFR, id); + out.block_notify_after_xfr = conf_bool(&val); conf_val_t rundir_val = conf_get_txn(conf, txn, C_SRV, C_RUNDIR); char *rundir = conf_abs_path(&rundir_val, NULL); @@ -1415,8 +1419,7 @@ conf_remote_t conf_remote_txn( conf_val_next(&val); } - val = conf_id_get_txn(conf, txn, C_RMT, C_CERT_KEY, id); - out.pin = (uint8_t *)conf_bin(&val, &out.pin_len); + free(rundir); // Get TSIG key (optional). conf_val_t key_id = conf_id_get_txn(conf, txn, C_RMT, C_KEY, id); @@ -1430,13 +1433,21 @@ conf_remote_t conf_remote_txn( out.key.secret.data = (uint8_t *)conf_bin(&val, &out.key.secret.size); } - free(rundir); - - val = conf_id_get_txn(conf, txn, C_RMT, C_BLOCK_NOTIFY_XFR, id); - out.block_notify_after_xfr = conf_bool(&val); + memset(out.hostnames, 0, sizeof(out.hostnames)); + val = conf_id_get_txn(conf, txn, C_RMT, C_CERT_HOSTNAME, id); + for (size_t i = 0; val.code == KNOT_EOK; i++) { + out.hostnames[i] = conf_str(&val); + conf_val_next(&val); + } - val = conf_id_get_txn(conf, txn, C_RMT, C_NO_EDNS, id); - out.no_edns = conf_bool(&val); + memset(out.pins, 0, sizeof(out.pins)); + val = conf_id_get_txn(conf, txn, C_RMT, C_CERT_KEY, id); + for (size_t i = 0; val.code == KNOT_EOK; i++) { + size_t len; + out.pins[i] = (uint8_t *)conf_bin(&val, &len); + assert(len == KNOT_TLS_PIN_LEN); + conf_val_next(&val); + } return out; } diff --git a/src/knot/conf/conf.h b/src/knot/conf/conf.h index 6a6c1650bd..a253ba171b 100644 --- a/src/knot/conf/conf.h +++ b/src/knot/conf/conf.h @@ -9,6 +9,7 @@ #include "knot/conf/base.h" #include "knot/conf/schema.h" +#include "libknot/quic/tls_common.h" /*! Configuration schema additional flags. */ #define CONF_IO_FACTIVE YP_FUSR1 /*!< Active confio transaction indicator. */ @@ -25,6 +26,8 @@ #define CONF_IO_FRLD_ALL (CONF_IO_FRLD_SRV | CONF_IO_FRLD_LOG | \ CONF_IO_FRLD_MOD | CONF_IO_FRLD_ZONES) +#define RMT_MAX_PINS KNOT_TLS_MAX_PINS + /*! Configuration remote getter output. */ typedef struct { /*! Target socket address. */ @@ -35,16 +38,16 @@ typedef struct { bool quic; /*! TLS context. */ bool tls; - /*! TSIG key. */ - knot_tsig_key_t key; - /*! Suppress sending NOTIFY after zone transfer from this master. */ - bool block_notify_after_xfr; /*! Disable EDNS on XFR queries. */ bool no_edns; - /*! Possible remote certificate PIN. */ - const uint8_t *pin; - /*! Length of the remote certificate PIN. Zero if PIN not specified. */ - size_t pin_len; + /*! Suppress sending NOTIFY after zone transfer from this master. */ + bool block_notify_after_xfr; + /*! TSIG key. */ + knot_tsig_key_t key; + /*! Remote certificate permittable hostnames. */ + const char *hostnames[RMT_MAX_PINS]; + /*! Remote certificate permittable PINs. */ + const uint8_t *pins[RMT_MAX_PINS]; } conf_remote_t; /*! Configuration section iterator. */ diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c index 514444db3c..fe1eee5cdf 100644 --- a/src/knot/conf/schema.c +++ b/src/knot/conf/schema.c @@ -236,6 +236,7 @@ static const yp_item_t desc_server[] = { 1232, YP_SSIZE } }, { C_CERT_FILE, YP_TSTR, YP_VNONE, YP_FNONE }, { C_KEY_FILE, YP_TSTR, YP_VNONE, YP_FNONE }, + { C_CA_FILE, YP_TSTR, YP_VNONE, YP_FMULTI }, { C_ECS, YP_TBOOL, YP_VNONE }, { C_ANS_ROTATION, YP_TBOOL, YP_VNONE }, { C_AUTO_ACL, YP_TBOOL, YP_VNONE }, @@ -341,6 +342,7 @@ static const yp_item_t desc_remote[] = { { C_TLS, YP_TBOOL, YP_VNONE }, { C_KEY, YP_TREF, YP_VREF = { C_KEY }, YP_FNONE, { check_ref } }, { C_CERT_KEY, YP_TB64, YP_VNONE, YP_FMULTI, { check_cert_pin } }, + { C_CERT_HOSTNAME, YP_TSTR, YP_VNONE, YP_FMULTI }, { C_BLOCK_NOTIFY_XFR, YP_TBOOL, YP_VNONE }, { C_NO_EDNS, YP_TBOOL, YP_VNONE }, { C_AUTO_ACL, YP_TBOOL, YP_VBOOL = { true } }, @@ -370,6 +372,7 @@ static const yp_item_t desc_acl[] = { { C_UPDATE_OWNER_NAME, YP_TDATA, YP_VDATA = { 0, NULL, rdname_to_bin, rdname_to_txt }, YP_FMULTI, }, { C_CERT_KEY, YP_TB64, YP_VNONE, YP_FMULTI, { check_cert_pin } }, + { C_CERT_HOSTNAME, YP_TSTR, YP_VNONE, YP_FMULTI }, { C_COMMENT, YP_TSTR, YP_VNONE }, { NULL } }; diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h index cd64dc2ce0..87984df91a 100644 --- a/src/knot/conf/schema.h +++ b/src/knot/conf/schema.h @@ -30,9 +30,11 @@ #define C_CATALOG_ROLE "\x0C""catalog-role" #define C_CATALOG_TPL "\x10""catalog-template" #define C_CATALOG_ZONE "\x0C""catalog-zone" +#define C_CA_FILE "\x07""ca-file" #define C_CDS_CDNSKEY "\x13""cds-cdnskey-publish" #define C_CDS_DIGESTTYPE "\x0F""cds-digest-type" #define C_CERT_FILE "\x09""cert-file" +#define C_CERT_HOSTNAME "\x0D""cert-hostname" #define C_CERT_KEY "\x08""cert-key" #define C_CHK_INTERVAL "\x0E""check-interval" #define C_CLEAR "\x05""clear" diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c index b31190670c..7e0b079403 100644 --- a/src/knot/conf/tools.c +++ b/src/knot/conf/tools.c @@ -873,11 +873,14 @@ int check_acl( C_KEY, args->id, args->id_len); conf_val_t proto = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_ACL, C_PROTOCOL, args->id, args->id_len); + conf_val_t hostname = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_ACL, + C_CERT_HOSTNAME, args->id, args->id_len); conf_val_t remote = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_ACL, C_RMT, args->id, args->id_len); if (remote.code != KNOT_ENOENT && - (addr.code != KNOT_ENOENT || key.code != KNOT_ENOENT || proto.code != KNOT_ENOENT)) { - args->err_str = "specified ACL/remote together with address, key, or protocol"; + (addr.code != KNOT_ENOENT || key.code != KNOT_ENOENT || + proto.code != KNOT_ENOENT || hostname.code != KNOT_ENOENT)) { + args->err_str = "specified ACL/remote together with address, key, protocol, or hostname"; return KNOT_EINVAL; } @@ -928,6 +931,19 @@ int check_remote( #endif } + conf_val_t pin = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_RMT, + C_CERT_KEY, args->id, args->id_len); + if (conf_val_count(&pin) > RMT_MAX_PINS) { + args->err_str = "too many cert-keys"; + return KNOT_EINVAL; + } + conf_val_t cert_host = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_RMT, + C_CERT_HOSTNAME, args->id, args->id_len); + if (conf_val_count(&cert_host) > RMT_MAX_PINS) { + args->err_str = "too many cert-hosts"; + return KNOT_EINVAL; + } + return KNOT_EOK; } diff --git a/src/knot/modules/dnsproxy/dnsproxy.c b/src/knot/modules/dnsproxy/dnsproxy.c index 695cd04142..2e310d1167 100644 --- a/src/knot/modules/dnsproxy/dnsproxy.c +++ b/src/knot/modules/dnsproxy/dnsproxy.c @@ -103,8 +103,8 @@ static int fwd(dnsproxy_t *proxy, knot_pkt_t *pkt, knotd_qdata_t *qdata, int add if (addr_pos < proxy->via.count) { // Simplified via address selection! src = &proxy->via.multi[addr_pos].addr; } - knot_request_t *req = knot_request_make_generic(re.mm, dst, src, query, - NULL, NULL, NULL, NULL, 0, flags); + knot_request_t *req = knot_request_make_generic(re.mm, dst, src, query, NULL, + NULL, NULL, NULL, NULL, flags); if (req == NULL) { knot_requestor_clear(&re); knot_pkt_free(query); diff --git a/src/knot/query/quic-requestor.c b/src/knot/query/quic-requestor.c index cef7cce4ab..cfeec47954 100644 --- a/src/knot/query/quic-requestor.c +++ b/src/knot/query/quic-requestor.c @@ -151,8 +151,8 @@ int knot_qreq_connect(struct knot_quic_reply **out, struct sockaddr_storage *remote, struct sockaddr_storage *local, const struct knot_creds *local_creds, - const uint8_t *peer_pin, - uint8_t peer_pin_len, + const char *const peer_hostnames[RMT_MAX_PINS], + const uint8_t *const peer_pins[RMT_MAX_PINS], bool *reused_fd, int timeout_ms) { @@ -173,7 +173,7 @@ int knot_qreq_connect(struct knot_quic_reply **out, r->send_reply = qr_send_reply; r->free_reply = qr_free_reply; - struct knot_creds *creds = knot_creds_init_peer(local_creds, peer_pin, peer_pin_len); + struct knot_creds *creds = knot_creds_init_peer(local_creds, peer_hostnames, peer_pins); if (creds == NULL) { free(r); return KNOT_ENOMEM; diff --git a/src/knot/query/quic-requestor.h b/src/knot/query/quic-requestor.h index 3c01bac6e9..757e07c39c 100644 --- a/src/knot/query/quic-requestor.h +++ b/src/knot/query/quic-requestor.h @@ -6,6 +6,7 @@ #pragma once #include "contrib/sockaddr.h" +#include "knot/conf/conf.h" #include "libknot/quic/quic.h" int knot_qreq_connect(struct knot_quic_reply **out, @@ -13,8 +14,8 @@ int knot_qreq_connect(struct knot_quic_reply **out, struct sockaddr_storage *remote, struct sockaddr_storage *local, const struct knot_creds *local_creds, - const uint8_t *peer_pin, - uint8_t peer_pin_len, + const char *const peer_hostnames[RMT_MAX_PINS], + const uint8_t *const peer_pins[RMT_MAX_PINS], bool *reused_fd, int timeout_ms); diff --git a/src/knot/query/requestor.c b/src/knot/query/requestor.c index cb0b45af4a..433fafd81f 100644 --- a/src/knot/query/requestor.c +++ b/src/knot/query/requestor.c @@ -84,11 +84,10 @@ static int request_ensure_connected(knot_request_t *request, bool *reused_fd, in &local_len); } #ifdef ENABLE_QUIC - int ret = knot_qreq_connect(&request->quic_ctx, - request->fd, &request->remote, - &request->source, request->creds, - request->pin, request->pin_len, - reused_fd, timeout_ms); + int ret = knot_qreq_connect(&request->quic_ctx, request->fd, + &request->remote, &request->source, + request->creds, request->hostnames, + request->pins, reused_fd, timeout_ms); if (ret != KNOT_EOK) { close(request->fd); request->fd = -1; @@ -104,8 +103,8 @@ static int request_ensure_connected(knot_request_t *request, bool *reused_fd, in int ret = knot_tls_req_ctx_init(&request->tls_req_ctx, request->fd, &request->remote, &request->source, - request->creds, request->pin, - request->pin_len, reused_fd, timeout_ms); + request->creds, request->hostnames, + request->pins, reused_fd, timeout_ms); if (ret != KNOT_EOK) { close(request->fd); request->fd = -1; @@ -210,15 +209,15 @@ knot_request_t *knot_request_make_generic(knot_mm_t *mm, const struct knot_creds *creds, const query_edns_data_t *edns, const knot_tsig_key_t *tsig_key, - const uint8_t *pin, - size_t pin_len, + const char *const hostnames[RMT_MAX_PINS], + const uint8_t *const pins[RMT_MAX_PINS], knot_request_flag_t flags) { if (remote == NULL || query == NULL) { return NULL; } - knot_request_t *request = mm_calloc(mm, 1, sizeof(*request) + pin_len); + knot_request_t *request = mm_calloc(mm, 1, sizeof(*request)); if (request == NULL) { return NULL; } @@ -247,9 +246,13 @@ knot_request_t *knot_request_make_generic(knot_mm_t *mm, request->edns = edns; request->creds = creds; - if ((flags & (KNOT_REQUEST_QUIC | KNOT_REQUEST_TLS)) && pin_len > 0) { - request->pin_len = pin_len; - memcpy(request->pin, pin, pin_len); + if (flags & (KNOT_REQUEST_QUIC | KNOT_REQUEST_TLS)) { + if (pins != NULL) { + memcpy(request->pins, pins, sizeof(request->pins)); + } + if (hostnames != NULL) { + memcpy(request->hostnames, hostnames, sizeof(request->hostnames)); + } } return request; @@ -269,9 +272,9 @@ knot_request_t *knot_request_make(knot_mm_t *mm, flags |= KNOT_REQUEST_TLS; } - return knot_request_make_generic(mm, &remote->addr, &remote->via, - query, creds, edns, &remote->key, remote->pin, - remote->pin_len, flags); + return knot_request_make_generic(mm, &remote->addr, &remote->via, query, + creds, edns, &remote->key, remote->hostnames, + remote->pins, flags); } void knot_request_free(knot_request_t *request, knot_mm_t *mm) diff --git a/src/knot/query/requestor.h b/src/knot/query/requestor.h index e3bd416343..0adef1d974 100644 --- a/src/knot/query/requestor.h +++ b/src/knot/query/requestor.h @@ -64,8 +64,9 @@ typedef struct knot_request { knot_sign_context_t sign; /*!< Required for async. DDNS processing. */ const struct knot_creds *creds; - size_t pin_len; - uint8_t pin[]; + + const char *hostnames[RMT_MAX_PINS]; + const uint8_t *pins[RMT_MAX_PINS]; } knot_request_t; static inline knotd_query_proto_t flags2proto(unsigned layer_flags) @@ -89,8 +90,8 @@ static inline knotd_query_proto_t flags2proto(unsigned layer_flags) * \param creds Local (server) credentials. * \param edns EDNS parameters. * \param tsig_key TSIG key for authentication. - * \param pin Possible remote certificate PIN. - * \param pin_len Length of the remote certificate PIN. + * \param hostnames Permittable remote certificate hostnames. + * \param pins Permittable remote certificate PINs. * \param flags Request flags. * * \return Prepared request or NULL in case of error. @@ -102,8 +103,8 @@ knot_request_t *knot_request_make_generic(knot_mm_t *mm, const struct knot_creds *creds, const query_edns_data_t *edns, const knot_tsig_key_t *tsig_key, - const uint8_t *pin, - size_t pin_len, + const char *const hostnames[RMT_MAX_PINS], + const uint8_t *const pins[RMT_MAX_PINS], knot_request_flag_t flags); /*! diff --git a/src/knot/query/tls-requestor.c b/src/knot/query/tls-requestor.c index 04159e48ea..935ac60d36 100644 --- a/src/knot/query/tls-requestor.c +++ b/src/knot/query/tls-requestor.c @@ -11,14 +11,17 @@ #include "libknot/quic/tls.h" #include "contrib/conn_pool.h" -int knot_tls_req_ctx_init(knot_tls_req_ctx_t *ctx, int fd, +int knot_tls_req_ctx_init(knot_tls_req_ctx_t *ctx, + int fd, const struct sockaddr_storage *remote, const struct sockaddr_storage *local, const struct knot_creds *local_creds, - const uint8_t *peer_pin, uint8_t peer_pin_len, - bool *reused_fd, int io_timeout_ms) + const char *const peer_hostnames[RMT_MAX_PINS], + const uint8_t *const peer_pins[RMT_MAX_PINS], + bool *reused_fd, + int io_timeout_ms) { - struct knot_creds *creds = knot_creds_init_peer(local_creds, peer_pin, peer_pin_len); + struct knot_creds *creds = knot_creds_init_peer(local_creds, peer_hostnames, peer_pins); if (creds == NULL) { return KNOT_ENOMEM; } diff --git a/src/knot/query/tls-requestor.h b/src/knot/query/tls-requestor.h index 6843d71aa5..63e1c5247e 100644 --- a/src/knot/query/tls-requestor.h +++ b/src/knot/query/tls-requestor.h @@ -8,6 +8,7 @@ #include #include +#include "knot/conf/conf.h" #include "libknot/quic/tls_common.h" struct knot_request; @@ -25,19 +26,25 @@ typedef struct knot_tls_req_ctx { * * \param ctx Context structure to be initialized. * \param fd Opened TCP connection file descriptor. + * \param remote Remote address for purposes of TLS session resumption. + * \param local Local address for purposes of TLS session resumption. * \param local_creds Local TLS credentials. - * \param peer_pin TLS peer pin. - * \param peer_pin_len TLS peer pin length. + * \param peer_hostnames Permittable TLS peer hostnames. + * \param peer_pins Permittable TLS peer PINs. + * \param reused_fd[out] Indicates successful TLS session resumption. * \param io_timeout_ms Configured io-timeout for TLS connection. * * \return KNOT_E* */ -int knot_tls_req_ctx_init(knot_tls_req_ctx_t *ctx, int fd, +int knot_tls_req_ctx_init(knot_tls_req_ctx_t *ctx, + int fd, const struct sockaddr_storage *remote, const struct sockaddr_storage *local, const struct knot_creds *local_creds, - const uint8_t *peer_pin, uint8_t peer_pin_len, - bool *reused_fd, int io_timeout_ms); + const char *const peer_hostnames[RMT_MAX_PINS], + const uint8_t *const peer_pins[RMT_MAX_PINS], + bool *reused_fd, + int io_timeout_ms); /*! * \brief Maintain the TLS requestor context (update session ticket). diff --git a/src/knot/server/server.c b/src/knot/server/server.c index 3500593a94..6b77777f71 100644 --- a/src/knot/server/server.c +++ b/src/knot/server/server.c @@ -551,7 +551,7 @@ static void log_sock_conf(conf_t *conf) } } -static int check_file(char *path, char *role) +static int check_file(const char *path, char *role) { if (path == NULL) { return KNOT_EOK; @@ -579,6 +579,10 @@ static int init_creds(conf_t *conf, server_t *server) { char *cert_file = conf_tls(conf, C_CERT_FILE); char *key_file = conf_tls(conf, C_KEY_FILE); + conf_val_t cafiles_val = conf_get(conf, C_SERVER, C_CA_FILE); + size_t nfiles = conf_val_count(&cafiles_val); + const char *ca_files[nfiles + 1]; + bool system_ca = false; int ret = check_file(cert_file, "certificate"); if (ret != KNOT_EOK) { @@ -615,16 +619,32 @@ static int init_creds(conf_t *conf, server_t *server) goto failed; } + memset(ca_files, 0, sizeof(ca_files)); + for (size_t i = 0; cafiles_val.code == KNOT_EOK; conf_val_next(&cafiles_val)) { + const char *file = conf_str(&cafiles_val); + if (*file == '\0') { + system_ca = true; + } else if ((ret = check_file(file, "ca")) != KNOT_EOK) { + goto failed; + } else { + ca_files[i++] = file; + } + } + if (server->quic_creds == NULL) { - server->quic_creds = knot_creds_init(key_file, cert_file, uid, gid); - if (server->quic_creds == NULL) { - log_error(QUIC_LOG "failed to initialize server credentials"); - ret = KNOT_ERROR; + ret = knot_creds_init(&server->quic_creds, key_file, cert_file, ca_files, + system_ca, uid, gid); + if (ret != KNOT_EOK) { + log_error(QUIC_LOG "failed to initialize credentials or to load certificates (%s)", + knot_strerror(ret)); goto failed; } } else { - ret = knot_creds_update(server->quic_creds, key_file, cert_file, uid, gid); + ret = knot_creds_update(server->quic_creds, key_file, cert_file, ca_files, + system_ca, uid, gid); if (ret != KNOT_EOK) { + log_error(QUIC_LOG "failed to initialize credentials or to load certificates (%s)", + knot_strerror(ret)); goto failed; } } diff --git a/src/knot/updates/acl.c b/src/knot/updates/acl.c index 613aa14376..ebcd3f69c7 100644 --- a/src/knot/updates/acl.c +++ b/src/knot/updates/acl.c @@ -30,6 +30,25 @@ static bool cert_pin_check(const uint8_t *session_pin, size_t session_pin_size, return false; } +static bool cert_check(struct gnutls_session_int *tls_session, conf_val_t *hostname_val) +{ + if (hostname_val->code == KNOT_ENOENT) { // No hostname authentication required. + return true; + } + + size_t count = conf_val_count(hostname_val); + const char *hostnames[count + 1]; + + const char **hostname = hostnames; + while (hostname_val->code == KNOT_EOK) { + *hostname++ = conf_str(hostname_val); + conf_val_next(hostname_val); + } + *hostname = NULL; + + return knot_tls_cert_check_hostnames(tls_session, hostnames) == KNOT_EOK; +} + static bool match_type(uint16_t type, conf_val_t *types) { if (types == NULL) { @@ -342,18 +361,19 @@ bool acl_allowed(conf_t *conf, conf_val_t *acl, acl_action_t action, conf_val_t deny_val = conf_id_get(conf, C_ACL, C_DENY, acl); bool deny = conf_bool(&deny_val); - /* Check if a remote matches given address and key. */ - conf_val_t addr_val, key_val, pin_val; + /* Check if a remote matches given params. */ + conf_val_t addr_val, key_val, pin_val, hostname_val; conf_mix_iter_t iter; conf_mix_iter_init(conf, &rmt_val, &iter); while (iter.id->code == KNOT_EOK) { addr_val = conf_id_get(conf, C_RMT, C_ADDR, iter.id); key_val = conf_id_get(conf, C_RMT, C_KEY, iter.id); pin_val = conf_id_get(conf, C_RMT, C_CERT_KEY, iter.id); - if (check_addr_key(conf, &addr_val, &key_val, remote, addr, - tsig, &pin_val, session_pin, session_pin_size, - deny, forward) - && check_proto_rmt(conf, proto, iter.id)) { + hostname_val = conf_id_get(conf, C_RMT, C_CERT_HOSTNAME, iter.id); + if (check_addr_key(conf, &addr_val, &key_val, remote, addr, tsig, &pin_val, + session_pin, session_pin_size, deny, forward) + && check_proto_rmt(conf, proto, iter.id) + && cert_check(tls_session, &hostname_val)) { break; } conf_mix_iter_next(&iter); @@ -361,14 +381,15 @@ bool acl_allowed(conf_t *conf, conf_val_t *acl, acl_action_t action, if (iter.id->code == KNOT_EOF) { goto next_acl; } - /* Or check if acl address/key matches given address and key. */ + /* Or check if acl matches given params. */ if (!remote) { addr_val = conf_id_get(conf, C_ACL, C_ADDR, acl); key_val = conf_id_get(conf, C_ACL, C_KEY, acl); pin_val = conf_id_get(conf, C_ACL, C_CERT_KEY, acl); - if (!check_addr_key(conf, &addr_val, &key_val, remote, addr, - tsig, &pin_val, session_pin, session_pin_size, - deny, forward)) { + hostname_val = conf_id_get(conf, C_ACL, C_CERT_HOSTNAME, acl); + if (!check_addr_key(conf, &addr_val, &key_val, remote, addr, tsig, &pin_val, + session_pin, session_pin_size, deny, forward) + || !cert_check(tls_session, &hostname_val)) { goto next_acl; } @@ -449,8 +470,13 @@ bool rmt_allowed(conf_t *conf, conf_val_t *rmts, const struct sockaddr_storage * goto next_remote; } - conf_val_t pin_val = conf_id_get(conf, C_RMT, C_CERT_KEY, iter.id); - if (!cert_pin_check(session_pin, session_pin_size, &pin_val)) { + val = conf_id_get(conf, C_RMT, C_CERT_KEY, iter.id); + if (!cert_pin_check(session_pin, session_pin_size, &val)) { + goto next_remote; + } + + val = conf_id_get(conf, C_RMT, C_CERT_HOSTNAME, iter.id); + if (!cert_check(tls_session, &val)) { goto next_remote; } diff --git a/src/libknot/errcode.h b/src/libknot/errcode.h index d87c4138f2..c9b0719892 100644 --- a/src/libknot/errcode.h +++ b/src/libknot/errcode.h @@ -95,7 +95,7 @@ enum knot_error { KNOT_EEMPTYZONE, KNOT_ENODB, KNOT_EUNREACH, - KNOT_EBADCERTKEY, + KNOT_EBADCERT, KNOT_EFACCES, KNOT_EBACKUPDATA, KNOT_ECPUCOMPAT, diff --git a/src/libknot/error.c b/src/libknot/error.c index 5e00b72564..f1eb1f0087 100644 --- a/src/libknot/error.c +++ b/src/libknot/error.c @@ -94,7 +94,7 @@ static const struct error errors[] = { { KNOT_EEMPTYZONE, "zone is empty" }, { KNOT_ENODB, "database does not exist" }, { KNOT_EUNREACH, "remote known to be unreachable" }, - { KNOT_EBADCERTKEY, "unknown certificate key" }, + { KNOT_EBADCERT, "invalid certificate" }, { KNOT_EFACCES, "file permission denied" }, { KNOT_EBACKUPDATA, "requested data not in backup" }, { KNOT_ECPUCOMPAT, "incompatible CPU architecture" }, diff --git a/src/libknot/quic/quic.c b/src/libknot/quic/quic.c index 9bdc4e8eb1..e19cb1f40c 100644 --- a/src/libknot/quic/quic.c +++ b/src/libknot/quic/quic.c @@ -289,8 +289,10 @@ static int handshake_completed_cb(ngtcp2_conn *conn, void *user_data) ctx->flags |= KNOT_QUIC_CONN_HANDSHAKE_DONE; if (!ngtcp2_conn_is_server(conn)) { - return knot_tls_pin_check(ctx->tls_session, ctx->quic_table->creds) - == KNOT_EOK ? 0 : NGTCP2_ERR_CALLBACK_FAILURE; + return knot_tls_pin_check(ctx->tls_session, ctx->quic_table->creds) == KNOT_EOK + && knot_tls_cert_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) { @@ -723,7 +725,7 @@ int knot_quic_handle(knot_quic_table_t *table, knot_quic_reply_t *reply, goto finish; } else if (ngtcp2_err_is_fatal(ret)) { // connection doomed if (ret == NGTCP2_ERR_CALLBACK_FAILURE) { - ret = KNOT_EBADCERTKEY; + ret = KNOT_EBADCERT; } else { ret = KNOT_ECONN; } diff --git a/src/libknot/quic/tls.c b/src/libknot/quic/tls.c index 5edaaf88a0..89942c733f 100644 --- a/src/libknot/quic/tls.c +++ b/src/libknot/quic/tls.c @@ -170,7 +170,10 @@ int knot_tls_handshake(knot_tls_conn_t *conn, bool oneshot) switch (ret) { case GNUTLS_E_SUCCESS: conn->flags |= KNOT_TLS_CONN_HANDSHAKE_DONE; - return knot_tls_pin_check(conn->session, conn->ctx->creds); + return knot_tls_pin_check(conn->session, conn->ctx->creds) == KNOT_EOK + && knot_tls_cert_check(conn->session, conn->ctx->creds) == KNOT_EOK + ? KNOT_EOK + : KNOT_EBADCERT; case GNUTLS_E_TIMEDOUT: return KNOT_NET_ETIMEOUT; default: diff --git a/src/libknot/quic/tls_common.c b/src/libknot/quic/tls_common.c index 5df73b8b3a..8bf9872e46 100644 --- a/src/libknot/quic/tls_common.c +++ b/src/libknot/quic/tls_common.c @@ -3,6 +3,7 @@ * For more information, see */ +#include #include #include #include @@ -10,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +19,7 @@ #include "libknot/quic/tls_common.h" #include "contrib/atomic.h" +#include "contrib/openbsd/siphash.h" #include "contrib/sockaddr.h" #include "contrib/string.h" #include "libknot/attribute.h" @@ -24,12 +27,13 @@ typedef struct knot_creds { knot_atomic_ptr_t cert_creds; // Current credentials. + uint64_t creds_hash; // Hashed creds sources to detect changes. 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[]; + const char *peer_hostnames[KNOT_TLS_MAX_PINS + 1]; + const uint8_t *peer_pins[KNOT_TLS_MAX_PINS + 1]; } knot_creds_t; _public_ @@ -175,43 +179,53 @@ finish: } _public_ -struct knot_creds *knot_creds_init(const char *key_file, const char *cert_file, - int uid, int gid) +int knot_creds_init(struct knot_creds **out, + const char *key_file, + const char *cert_file, + const char **ca_files, + bool system_ca, + int uid, + int gid) { + if (out == NULL) { + return KNOT_EINVAL; + } + knot_creds_t *creds = calloc(1, sizeof(*creds)); if (creds == NULL) { - return NULL; + return KNOT_ENOMEM; } - int ret = knot_creds_update(creds, key_file, cert_file, uid, gid); + int ret = knot_creds_update(creds, key_file, cert_file, ca_files, system_ca, uid, gid); if (ret != KNOT_EOK) { goto fail; } - ret = gnutls_anti_replay_init(&creds->tls_anti_replay); - if (ret != GNUTLS_E_SUCCESS) { + if (gnutls_anti_replay_init(&creds->tls_anti_replay) != GNUTLS_E_SUCCESS) { + ret = KNOT_ENOMEM; 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) { + if (gnutls_session_ticket_key_generate(&creds->tls_ticket_key) != GNUTLS_E_SUCCESS) { + ret = KNOT_ENOMEM; goto fail; } - return creds; + *out = creds; + return KNOT_EOK; fail: knot_creds_free(creds); - return NULL; + return ret; } _public_ struct knot_creds *knot_creds_init_peer(const struct knot_creds *local_creds, - const uint8_t *peer_pin, - uint8_t peer_pin_len) + const char *const peer_hostnames[KNOT_TLS_MAX_PINS], + const uint8_t *const peer_pins[KNOT_TLS_MAX_PINS]) { - knot_creds_t *creds = calloc(1, sizeof(*creds) + peer_pin_len); + knot_creds_t *creds = calloc(1, sizeof(*creds)); if (creds == NULL) { return NULL; } @@ -229,9 +243,13 @@ struct knot_creds *knot_creds_init_peer(const struct knot_creds *local_creds, ATOMIC_INIT(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; + if (peer_pins != NULL) { + memcpy(creds->peer_pins, peer_pins, + sizeof(peer_pins[0]) * KNOT_TLS_MAX_PINS); + } + if (peer_hostnames != NULL) { + memcpy(creds->peer_hostnames, peer_hostnames, + sizeof(peer_hostnames[0]) * KNOT_TLS_MAX_PINS); } return creds; @@ -255,63 +273,71 @@ static int creds_cert(gnutls_certificate_credentials_t creds, return KNOT_ERROR; } -static int creds_changed(gnutls_certificate_credentials_t creds, - gnutls_certificate_credentials_t prev, - bool self_cert, bool *changed) +static void hash_file(SIPHASH_CTX *ctx, const char *file_name) { - if (creds == NULL || prev == NULL) { - *changed = true; - return KNOT_EOK; + assert(ctx); + assert(file_name); + + char *data; + struct stat file_stat; + int fd = open(file_name, O_RDONLY); + if (fd == -1 || + fstat(fd, &file_stat) == -1 || + (data = mmap(0, file_stat.st_size, PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) { + close(fd); + return; } - gnutls_x509_crt_t cert = NULL, cert_prev = NULL; + SipHash24_Update(ctx, data, file_stat.st_size); - int ret = creds_cert(creds, &cert); - if (ret != KNOT_EOK) { - goto failed; - } - ret = creds_cert(prev, &cert_prev); - if (ret != KNOT_EOK) { - goto failed; - } + munmap(data, file_stat.st_size); + close(fd); +} - 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); +static uint64_t creds_hash(const char *key_file, + const char *cert_file, + const char **ca_files, + bool system_ca) +{ + SIPHASH_CTX ctx; + SIPHASH_KEY key = { 0 }; + SipHash24_Init(&ctx, &key); - 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; + assert(key_file); + hash_file(&ctx, key_file); + if (cert_file != NULL) { + hash_file(&ctx, cert_file); + } + if (ca_files != NULL) { + for (const char **file = ca_files; *file != NULL; file++) { + hash_file(&ctx, *file); } - - *changed = (pin_size != pin_prev_size) || - memcmp(pin, pin_prev, pin_size) != 0; - } else { - *changed = (gnutls_x509_crt_equals(cert, cert_prev) == 0); + } + if (system_ca) { + SipHash24_Update(&ctx, "\x01", 1); } - ret = KNOT_EOK; -failed: - gnutls_x509_crt_deinit(cert); - gnutls_x509_crt_deinit(cert_prev); - - return ret; + return SipHash24_End(&ctx); } _public_ -int knot_creds_update(struct knot_creds *creds, const char *key_file, const char *cert_file, - int uid, int gid) +int knot_creds_update(struct knot_creds *creds, + const char *key_file, + const char *cert_file, + const char **ca_files, + bool system_ca, + int uid, + int gid) { if (creds == NULL || key_file == NULL) { return KNOT_EINVAL; } + uint64_t new_hash = creds_hash(key_file, cert_file, ca_files, system_ca); + if (creds->creds_hash == new_hash) { + return KNOT_EOK; + } + gnutls_certificate_credentials_t new_creds; int ret = gnutls_certificate_allocate_credentials(&new_creds); if (ret != GNUTLS_E_SUCCESS) { @@ -330,23 +356,28 @@ int knot_creds_update(struct knot_creds *creds, const char *key_file, const char 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 (system_ca) { + if (gnutls_certificate_set_x509_system_trust(new_creds) < 0) { + gnutls_certificate_free_credentials(new_creds); + return KNOT_EBADCERT; + } } - - if (changed) { - if (creds->cert_creds_prev != NULL) { - gnutls_certificate_free_credentials(creds->cert_creds_prev); + if (ca_files != NULL) { + for (const char **file = ca_files; *file != NULL; file++) { + if (gnutls_certificate_set_x509_trust_file(new_creds, *file, + GNUTLS_X509_FMT_PEM) < 0) { + gnutls_certificate_free_credentials(new_creds); + return KNOT_EBADCERT; + } } - creds->cert_creds_prev = ATOMIC_XCHG(creds->cert_creds, new_creds); - } else { - gnutls_certificate_free_credentials(new_creds); } + 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); + creds->creds_hash = new_hash; + return KNOT_EOK; } @@ -493,17 +524,82 @@ _public_ int knot_tls_pin_check(struct gnutls_session_int *session, struct knot_creds *creds) { - if (creds->peer_pin_len == 0) { + // if no pin set -> opportunistic mode + if (creds->peer_pins[0] == NULL) { 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; + if (pin_size != KNOT_TLS_PIN_LEN) { + return KNOT_EBADCERT; } - return KNOT_EOK; + for (const uint8_t **it = creds->peer_pins; *it != NULL; it++) { + if (const_time_memcmp(pin, *it, KNOT_TLS_PIN_LEN) == 0) { + return KNOT_EOK; + } + } + + return KNOT_EBADCERT; +} + +_public_ +int knot_tls_cert_check_hostnames(struct gnutls_session_int *session, + const char *hostnames[]) +{ + // if no hostname set -> opportunistic mode + if (hostnames == NULL || hostnames[0] == NULL) { + return KNOT_EOK; + } + + if (gnutls_certificate_type_get(session) != GNUTLS_CRT_X509) { + return KNOT_EBADCERT; + } + + unsigned status = 0; + int ret = gnutls_certificate_verify_peers2(session, &status); + if (ret != GNUTLS_E_SUCCESS || status != 0) { + return KNOT_EBADCERT; + } + + unsigned count = 0; + const gnutls_datum_t *cert_list = gnutls_certificate_get_peers(session, &count); + if (count == 0) { + return KNOT_EBADCERT; + } + + gnutls_x509_crt_t cert; + ret = gnutls_x509_crt_init(&cert); + if (ret != GNUTLS_E_SUCCESS) { + return KNOT_EBADCERT; + } + + // standard compliant servers send an ordered cert list, so the 0th cert is peer's + ret = gnutls_x509_crt_import(cert, &cert_list[0], GNUTLS_X509_FMT_DER); + if (ret != GNUTLS_E_SUCCESS) { + gnutls_x509_crt_deinit(cert); + return KNOT_EBADCERT; + } + + // using gnutls_x509_crt_check_hostname() to enforce SAN-only hostname checking + // see https://datatracker.ietf.org/doc/html/rfc8310#section-8.1 + for (const char **hostname = hostnames; *hostname != NULL; hostname++) { + if (gnutls_x509_crt_check_hostname(cert, *hostname)) { + gnutls_x509_crt_deinit(cert); + return KNOT_EOK; + } + } + + gnutls_x509_crt_deinit(cert); + + return KNOT_EBADCERT; +} + +_public_ +int knot_tls_cert_check(struct gnutls_session_int *session, + struct knot_creds *creds) +{ + return knot_tls_cert_check_hostnames(session, creds->peer_hostnames); } diff --git a/src/libknot/quic/tls_common.h b/src/libknot/quic/tls_common.h index b2e73f114f..77668d3032 100644 --- a/src/libknot/quic/tls_common.h +++ b/src/libknot/quic/tls_common.h @@ -19,6 +19,7 @@ #include #define KNOT_TLS_PIN_LEN 32 +#define KNOT_TLS_MAX_PINS 4 struct gnutls_priority_st; struct gnutls_session_int; @@ -44,44 +45,60 @@ typedef enum { const char *knot_tls_priority(bool tls12); /*! - * \brief Init server TLS key and certificate for DoQ. + * \brief Init server credentials. * + * \param out Server credentials to initialize. * \param key_file Key PEM file path/name. * \param cert_file X509 certificate PEM file path/name (NULL if auto-generated). + * \param ca_files Which additional certificate indicators to import. NULL terminated. + * \param system_ca Whether to import system certificate indicators. * \param uid Generated key file owner id. * \param gid Generated key file group id. * * \return Initialized creds. */ -struct knot_creds *knot_creds_init(const char *key_file, const char *cert_file, - int uid, int gid); +int knot_creds_init(struct knot_creds **out, + const char *key_file, + const char *cert_file, + const char **ca_files, + bool system_ca, + int uid, + int gid +); /*! - * \brief Init peer TLS key and certificate for DoQ. + * \brief Init peer credentials. * - * \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. + * \param local_creds Local credentials if server. + * \param peer_hostname Optional peer certificate hostnames to check. + * \param peer_pin Optional peer certificate PINs to check. * * \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); + const char *const peer_hostname[KNOT_TLS_MAX_PINS], + const uint8_t *const peer_pin[KNOT_TLS_MAX_PINS]); /*! - * \brief Load new server TLS key and certificate for DoQ. + * \brief Update server credentials. * * \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). + * \param ca_files Which additional certificate indicators to import. NULL terminated. + * \param system_ca Whether to import system certificate indicators. * \param uid Generated key file owner id. * \param gid Generated key file group id. * * \return KNOT_E* */ -int knot_creds_update(struct knot_creds *creds, const char *key_file, const char *cert_file, - int uid, int gid); +int knot_creds_update(struct knot_creds *creds, + const char *key_file, + const char *cert_file, + const char **ca_files, + bool system_ca, + int uid, + int gid); /*! * \brief Gets the certificate from credentials. @@ -94,7 +111,7 @@ int knot_creds_update(struct knot_creds *creds, const char *key_file, const char int knot_creds_cert(struct knot_creds *creds, struct gnutls_x509_crt_int **cert); /*! - * \brief Deinit server TLS certificate for DoQ. + * \brief Deinit credentials. */ void knot_creds_free(struct knot_creds *creds); @@ -132,9 +149,31 @@ void knot_tls_pin(struct gnutls_session_int *session, uint8_t *pin, * \param session TLS connection. * \param creds TLS credentials. * - * \return KNOT_EOK or KNOT_EBADCERTKEY + * \return KNOT_EOK or KNOT_EBADCERT */ int knot_tls_pin_check(struct gnutls_session_int *session, struct knot_creds *creds); +/*! + * \brief Checks remote certificate validity against hostname strings. + * + * \param session TLS connection. + * \param hostnames NULL terminated array of possible hostnames. + * + * \return KNOT_EOK or KNOT_EBADCERT + */ +int knot_tls_cert_check_hostnames(struct gnutls_session_int *session, + const char *hostnames[]); + +/*! + * \brief Checks remote certificate validity against credentials. + * + * \param session TLS connection. + * \param creds TLS credentials. + * + * \return KNOT_EOK or KNOT_EBADCERT + */ +int knot_tls_cert_check(struct gnutls_session_int *session, + struct knot_creds *creds); + /*! @} */ diff --git a/src/utils/kxdpgun/main.c b/src/utils/kxdpgun/main.c index b21c7ec0e0..459e2bcf51 100644 --- a/src/utils/kxdpgun/main.c +++ b/src/utils/kxdpgun/main.c @@ -349,7 +349,7 @@ void *xdp_gun_thread(void *_ctx) } if (ctx->quic) { #ifdef ENABLE_QUIC - quic_creds = knot_creds_init_peer(NULL, NULL, 0); + quic_creds = knot_creds_init_peer(NULL, NULL, NULL); if (quic_creds == NULL) { ERR2("failed to initialize QUIC context"); goto cleanup; diff --git a/tests-extra/tests/quic/xfr/test.py b/tests-extra/tests/quic/xfr/test.py index e29afbae40..dd35c2d56d 100644 --- a/tests-extra/tests/quic/xfr/test.py +++ b/tests-extra/tests/quic/xfr/test.py @@ -7,6 +7,8 @@ from dnstest.utils import * import random import subprocess +use_hostname = random.choice([True, False]) + t = Test(quic=True, tsig=True) # TSIG needed to skip weaker ACL rules master = t.server("knot") @@ -33,7 +35,7 @@ if slave.valgrind: MSG_DENIED_NOTIFY = "ACL, denied, action notify" MSG_DENIED_TRANSFER = "ACL, denied, action transfer" MSG_RMT_NOTAUTH = "server responded with error 'NOTAUTH'" -MSG_RMT_BADCERT = "failed (unknown certificate key)" +MSG_RMT_BADCERT = "failed (invalid certificate)" MSG_TSIG_ERROR = "failed (failed to verify TSIG)" def check_error(server, msg): @@ -53,6 +55,8 @@ def upd_check_zones(master, slave, zones, prev_serials): master.check_quic() +t.gen_ca() + t.start() tcpdump_pcap = t.out_dir + "/traffic.pcap" @@ -72,7 +76,10 @@ try: t.xfr_diff(master, slave, zones) # Check master not authenticated due to bad cert-key - master.cert_key = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=" + if use_hostname: + master.cert_hostname = ["unknown"] + else: + master.cert_key = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=" slave.gen_confile() slave.reload() master.ctl("zone-notify") @@ -82,13 +89,25 @@ try: check_error(slave, MSG_RMT_BADCERT) # Check IXFR with cert-key-based authenticated master - master.fill_cert_key() + if use_hostname: + master.gen_cert() + master.cert_hostname = [master.name, "bad2"] + slave.set_ca() + master.gen_confile() + master.reload() + else: + master.fill_cert_key() slave.gen_confile() - slave.reload() + #slave.reload() doesn't work for hostname, restart instead till fixed + slave.stop() + slave.start() serials = upd_check_zones(master, slave, rnd_zones, serials) # Check slave not authenticated due to bad cert-key - slave.cert_key = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=" + if use_hostname: + slave.cert_hostname = ["unknown"] + else: + slave.cert_key = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=" master.gen_confile() master.reload() master.ctl("zone-notify") @@ -98,9 +117,18 @@ try: check_error(master, MSG_DENIED_TRANSFER) # Check IXFR with cert-key-based authenticated slave - slave.fill_cert_key() + if use_hostname: + slave.gen_cert() + slave.cert_hostname = ["bad1", "bad2", "bad3", slave.name] + master.set_ca() + slave.gen_confile() + slave.reload() + else: + slave.fill_cert_key() master.gen_confile() - master.reload() + #master.reload() doesn't work for hostname, restart instead till fixed + master.stop() + master.start() serials = upd_check_zones(master, slave, rnd_zones, serials) finally: diff --git a/tests-extra/tests/tls/xfr/test.py b/tests-extra/tests/tls/xfr/test.py index ba986587fd..92292b242c 100644 --- a/tests-extra/tests/tls/xfr/test.py +++ b/tests-extra/tests/tls/xfr/test.py @@ -7,6 +7,8 @@ from dnstest.utils import * import random import subprocess +use_hostname = random.choice([True, False]) + t = Test(tls=True, tsig=True, # TSIG needed to skip weaker ACL rules quic=random.choice([False, True])) # QUIC should have no effect @@ -34,7 +36,7 @@ if slave.valgrind: MSG_DENIED_NOTIFY = "ACL, denied, action notify" MSG_DENIED_TRANSFER = "ACL, denied, action transfer" MSG_RMT_NOTAUTH = "server responded with error 'NOTAUTH'" -MSG_RMT_BADCERT = "failed (unknown certificate key)" +MSG_RMT_BADCERT = "failed (invalid certificate)" MSG_TSIG_ERROR = "failed (failed to verify TSIG)" def check_error(server, msg): @@ -54,6 +56,8 @@ def upd_check_zones(master, slave, zones, prev_serials): master.check_quic() +t.gen_ca() + t.start() tcpdump_pcap = t.out_dir + "/traffic.pcap" @@ -73,7 +77,10 @@ try: t.xfr_diff(master, slave, zones) # Check master not authenticated due to bad cert-key - master.cert_key = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=" + if use_hostname: + master.cert_hostname = ["unknown"] + else: + master.cert_key = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=" slave.gen_confile() slave.reload() master.ctl("zone-notify") @@ -83,13 +90,25 @@ try: check_error(slave, MSG_RMT_BADCERT) # Check IXFR with cert-key-based authenticated master - master.fill_cert_key() + if use_hostname: + master.gen_cert() + master.cert_hostname = [master.name, "bad2"] + slave.set_ca() + master.gen_confile() + master.reload() + else: + master.fill_cert_key() slave.gen_confile() - slave.reload() + #slave.reload() doesn't work for hostname, restart instead till fixed + slave.stop() + slave.start() serials = upd_check_zones(master, slave, rnd_zones, serials) # Check slave not authenticated due to bad cert-key - slave.cert_key = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=" + if use_hostname: + slave.cert_hostname = ["unknown"] + else: + slave.cert_key = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=" master.gen_confile() master.reload() master.ctl("zone-notify") @@ -99,9 +118,18 @@ try: check_error(master, MSG_DENIED_TRANSFER) # Check IXFR with cert-key-based authenticated slave - slave.fill_cert_key() + if use_hostname: + slave.gen_cert() + slave.cert_hostname = ["bad1", "bad2", "bad3", slave.name] + master.set_ca() + slave.gen_confile() + slave.reload() + else: + slave.fill_cert_key() master.gen_confile() - master.reload() + #master.reload() doesn't work for hostname, restart instead till fixed + master.stop() + master.start() serials = upd_check_zones(master, slave, rnd_zones, serials) finally: diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py index 47dce78876..8a8bde2da5 100644 --- a/tests-extra/tools/dnstest/server.py +++ b/tests-extra/tools/dnstest/server.py @@ -168,6 +168,8 @@ class Server(object): self.quic_port = None self.tls_port = None self.cert_key = str() + self.cert_hostname = list() + self.ca_file = str() self.cert_key_file = None # quadruple (key_file, cert_file, hostname, pin) self.udp_workers = None self.tcp_workers = None @@ -617,6 +619,28 @@ class Server(object): hostname3 = socket.gethostname() return ("", certfile, hostname1 or hostname2 or hostname3, ssearch(gcli_s, r'pin-sha256:([^\n]*)')) + def gen_cert(self): + try: + ca_key = os.path.join(Context().test.out_dir, "ca-key.pem") + ca_cert = os.path.join(Context().test.out_dir, "ca-cert.pem") + srv_key = os.path.join(self.dir, f"{self.name}-key.pem") + srv_cert = os.path.join(self.dir, f"{self.name}-cert.pem") + srv_tpl = os.path.join(self.dir, f"{self.name}-tpl.info") + check_call(["certtool", "--generate-privkey", "--key-type", "ed25519", + "--outfile", srv_key], stdout=DEVNULL, stderr=DEVNULL) + with open(srv_tpl, "w") as tpl: + print(f"dns_name = \"{self.name}\"\nexpiration_days = 365", file=tpl) + check_call(["certtool", "--generate-certificate", "--load-privkey", srv_key, + "--load-ca-certificate", ca_cert, "--load-ca-privkey", ca_key, + "--template", srv_tpl, "--outfile", srv_cert], + stdout=DEVNULL, stderr=DEVNULL) + self.cert_key_file = (srv_key, srv_cert, self.name, fsearch(srv_key, r'pin-sha256:([^\n]*)')) + except CalledProcessError as e: + raise Failed("Failed to generate server certificate") + + def set_ca(self): + self.ca_file = os.path.join(Context().test.out_dir, "ca-cert.pem") + def kdig(self, rname, rtype, rclass="IN", dnssec=None, validate=None, msgdelay=None): cmd = [ params.kdig_bin, "@" + self.addr, "-p", str(self.port), "-q", rname, "-t", rtype, "-c", rclass ] if dnssec: @@ -1267,9 +1291,8 @@ class Bind(Server): % (slave.addr, slave.port, self.tsig.name) else: slaves += "%s port %s" % (slave.addr, slave.port) - #if slave.tls_port: - # slaves += " tls %s" % (slave.name if slave.cert_key_file else "ephemeral") - # TODO Bind9 fails to send NOTIFYoverTLS, until fixed https://gitlab.isc.org/isc-projects/bind9/-/issues/4821 + if slave.tls_port: + slaves += " tls %s" % (slave.name if slave.cert_key_file else "ephemeral") slaves += "; " if slaves: s.item("also-notify", "{ %s}" % slaves) @@ -1522,6 +1545,8 @@ class Knot(Server): if self.cert_key_file: s.item_str("key-file", self.cert_key_file[0]) s.item_str("cert-file", self.cert_key_file[1]) + if self.ca_file: + s.item_str("ca-file", self.ca_file) s.end() if self.xdp_port is not None and self.xdp_port > 0: @@ -1569,6 +1594,8 @@ class Knot(Server): s.item_str("tls" if master.tls_port else "quic", "on") if master.cert_key: s.item_str("cert-key", master.cert_key) + elif master.cert_hostname: + s.item_list("cert-hostname", master.cert_hostname) elif master.cert_key_file: s.item_str("cert-key", master.cert_key_file[3]) else: @@ -1594,6 +1621,8 @@ class Knot(Server): s.item_str("tls" if slave.tls_port else "quic", "on") if slave.cert_key: s.item_str("cert-key", slave.cert_key) + elif slave.cert_hostname: + s.item_list("cert-hostname", slave.cert_hostname) elif slave.cert_key_file: s.item_str("cert-key", slave.cert_key_file[3]) else: @@ -1630,6 +1659,8 @@ class Knot(Server): s.item_str("tls" if remote.tls_port else "quic", "on") if remote.cert_key: s.item_str("cert-key", remote.cert_key) + elif remote.cert_hostname: + s.item_list("cert-hostname", remote.cert_hostname) elif remote.cert_key_file: s.item_str("cert-key", remote.cert_key_file[3]) else: @@ -1665,8 +1696,10 @@ class Knot(Server): s.item_str("address", master.addr) if master.tsig: s.item_str("key", master.tsig.name) - if master.cert_key and not isinstance(master, Bind): # TODO until fixed https://gitlab.isc.org/isc-projects/bind9/-/issues/4821 + if master.cert_key: s.item_str("cert-key", master.cert_key) + if master.cert_hostname: + s.item_list("cert-hostname", master.cert_hostname) s.item("action", "notify") servers.add(master.name) for slave in z.slaves: @@ -1681,6 +1714,8 @@ class Knot(Server): s.item_str("key", slave.tsig.name) if slave.cert_key: s.item_str("cert-key", slave.cert_key) + if slave.cert_hostname: + s.item_list("cert-hostname", slave.cert_hostname) s.item("action", "[transfer" + (", update" if z.ddns else "") + "]") servers.add(slave.name) for remote in z.dnssec.dnskey_sync if z.dnssec.dnskey_sync else []: diff --git a/tests-extra/tools/dnstest/test.py b/tests-extra/tools/dnstest/test.py index 883948684b..53bc43e558 100644 --- a/tests-extra/tools/dnstest/test.py +++ b/tests-extra/tools/dnstest/test.py @@ -11,6 +11,7 @@ import time import dns.name import dns.zone import zone_generate +from subprocess import Popen, PIPE, check_call, CalledProcessError, check_output, run, DEVNULL from dnstest.utils import * from dnstest.context import Context import dnstest.params as params @@ -628,3 +629,17 @@ class Test(object): resp_ixfr.check_axfr_style_ixfr(resp_axfr) + def gen_ca(self): + try: + ca_key = os.path.join(self.out_dir, "ca-key.pem") + ca_cert = os.path.join(self.out_dir, "ca-cert.pem") + ca_tpl = os.path.join(self.out_dir, "ca-tpl.info") + check_call(["certtool", "--generate-privkey", "--key-type", "ed25519", + "--outfile", ca_key], stdout=DEVNULL, stderr=DEVNULL) + with open(ca_tpl, "w") as tpl: + print(f"cn = \"CA\"\nca\ncert_signing_key\nexpiration_days = 3650", file=tpl) + check_call(["certtool", "--generate-self-signed", "--load-privkey", + ca_key, "--template", ca_tpl, "--outfile", ca_cert], + stdout=DEVNULL, stderr=DEVNULL) + except CalledProcessError as e: + raise Failed("Failed to generate CA") diff --git a/tests/knot/test_requestor.c b/tests/knot/test_requestor.c index 1001d89f74..6199e71292 100644 --- a/tests/knot/test_requestor.c +++ b/tests/knot/test_requestor.c @@ -82,7 +82,7 @@ static knot_request_t *make_query(knot_requestor_t *requestor, knot_request_flag_t flags = TFO ? KNOT_REQUEST_TFO: KNOT_REQUEST_NONE; return knot_request_make_generic(requestor->mm, dst, src, pkt, NULL, - NULL, NULL, NULL, 0, flags); + NULL, NULL, NULL, NULL, flags); } static void test_disconnected(knot_requestor_t *requestor,