From 1f60b32a512c70455fdb69d6aa4ba87faf73f723 Mon Sep 17 00:00:00 2001 From: Artem Boldariev Date: Wed, 19 Jan 2022 13:10:08 +0200 Subject: [PATCH] Add support for Strict/Mutual TLS to dig This commit adds support for Strict/Mutual TLS to dig. The new command-line options and their behaviour are modelled after kdig (+tls-ca, +tls-hostname, +tls-certfile, +tls-keyfile) for compatibility reasons. That is, using +tls-* is sufficient to enable DoT in dig, implying +tls-ca If there is no other DNS transport specified via command-line, specifying any of +tls-* options makes dig use DoT. In this case, its behaviour is the same as if +tls-ca is specified: that is, the remote peer's certificate is verified using the platform-specific intermediate CA certificates store. This behaviour is introduced for compatibility with kdig. --- bin/dig/dig.c | 170 +++++++++++++++++++++++++++------ bin/dig/dig.rst | 21 +++++ bin/dig/dighost.c | 235 ++++++++++++++++++++++++++++++++++++++++++---- bin/dig/dighost.h | 15 ++- doc/man/dig.1in | 24 +++++ 5 files changed, 416 insertions(+), 49 deletions(-) diff --git a/bin/dig/dig.c b/bin/dig/dig.c index 4783b98ba2..9bced727d6 100644 --- a/bin/dig/dig.c +++ b/bin/dig/dig.c @@ -295,6 +295,14 @@ help(void) { " +[no]tcp (TCP mode (+[no]vc))\n" " +timeout=### (Set query timeout) [5]\n" " +[no]tls (DNS-over-TLS mode)\n" + " +[no]tls-ca[=file] (Enable remote server's " + "TLS certificate validation)\n" + " +[no]tls-hostname=hostname (Explicitly set " + "the expected TLS hostname)\n" + " +[no]tls-certfile=file (Load client TLS " + "certificate chain from file)\n" + " +[no]tls-keyfile=file (Load client TLS " + "private key from file)\n" " +[no]trace (Trace delegation down " "from root " "[+dnssec])\n" @@ -346,7 +354,7 @@ received(unsigned int bytes, isc_sockaddr_t *from, dig_query_t *query) { } else { printf(";; Query time: %ld msec\n", (long)diff / 1000); } - if (query->lookup->tls_mode) { + if (dig_lookup_is_tls(query->lookup)) { proto = "TLS"; } else if (query->lookup->https_mode) { if (query->lookup->http_plain) { @@ -1021,6 +1029,128 @@ printgreeting(int argc, char **argv, dig_lookup_t *lookup) { } } +#define FULLCHECK(A) \ + do { \ + size_t _l = strlen(cmd); \ + if (_l >= sizeof(A) || strncasecmp(cmd, A, _l) != 0) \ + goto invalid_option; \ + } while (0) +#define FULLCHECK2(A, B) \ + do { \ + size_t _l = strlen(cmd); \ + if ((_l >= sizeof(A) || strncasecmp(cmd, A, _l) != 0) && \ + (_l >= sizeof(B) || strncasecmp(cmd, B, _l) != 0)) \ + goto invalid_option; \ + } while (0) +#define FULLCHECK6(A, B, C, D, E, F) \ + do { \ + size_t _l = strlen(cmd); \ + if ((_l >= sizeof(A) || strncasecmp(cmd, A, _l) != 0) && \ + (_l >= sizeof(B) || strncasecmp(cmd, B, _l) != 0) && \ + (_l >= sizeof(C) || strncasecmp(cmd, C, _l) != 0) && \ + (_l >= sizeof(D) || strncasecmp(cmd, D, _l) != 0) && \ + (_l >= sizeof(E) || strncasecmp(cmd, E, _l) != 0) && \ + (_l >= sizeof(F) || strncasecmp(cmd, F, _l) != 0)) \ + goto invalid_option; \ + } while (0) + +static bool +plus_tls_options(const char *cmd, const char *value, const bool state, + dig_lookup_t *lookup) { + /* + * Using TLS implies "TCP-like" mode. + */ + if (!lookup->tcp_mode_set) { + lookup->tcp_mode = state; + } + switch (cmd[3]) { + case '-': + /* + * Assume that if any of the +tls-* options are set, then we + * need to verify the remote certificate (compatibility with + * kdig). + */ + if (state) { + lookup->tls_ca_set = state; + } + switch (cmd[4]) { + case 'c': + switch (cmd[5]) { + case 'a': + FULLCHECK("tls-ca"); + lookup->tls_ca_set = state; + if (state && value != NULL) { + lookup->tls_ca_file = + isc_mem_strdup(mctx, value); + } + break; + case 'e': + FULLCHECK("tls-certfile"); + lookup->tls_cert_file_set = state; + if (state) { + if (value != NULL && *value != '\0') { + lookup->tls_cert_file = + isc_mem_strdup(mctx, + value); + } else { + fprintf(stderr, + ";; TLS certificate " + "file is " + "not specified\n"); + goto invalid_option; + } + } + break; + default: + goto invalid_option; + } + break; + case 'h': + FULLCHECK("tls-hostname"); + lookup->tls_hostname_set = state; + if (state) { + if (value != NULL && *value != '\0') { + lookup->tls_hostname = + isc_mem_strdup(mctx, value); + } else { + fprintf(stderr, ";; TLS hostname is " + "not specified\n"); + goto invalid_option; + } + } + break; + case 'k': + FULLCHECK("tls-keyfile"); + lookup->tls_key_file_set = state; + if (state) { + if (value != NULL && *value != '\0') { + lookup->tls_key_file = + isc_mem_strdup(mctx, value); + } else { + fprintf(stderr, + ";; TLS private key file is " + "not specified\n"); + goto invalid_option; + } + } + break; + default: + goto invalid_option; + } + break; + case '\0': + FULLCHECK("tls"); + lookup->tls_mode = state; + break; + default: + goto invalid_option; + } + + return true; +invalid_option: + return false; +} + /*% * We're not using isc_commandline_parse() here since the command line * syntax of dig is quite a bit different from that which can be described @@ -1050,31 +1180,6 @@ plus_option(char *option, bool is_batchfile, bool *need_clone, /* parse the rest of the string */ value = strtok_r(NULL, "", &last); -#define FULLCHECK(A) \ - do { \ - size_t _l = strlen(cmd); \ - if (_l >= sizeof(A) || strncasecmp(cmd, A, _l) != 0) \ - goto invalid_option; \ - } while (0) -#define FULLCHECK2(A, B) \ - do { \ - size_t _l = strlen(cmd); \ - if ((_l >= sizeof(A) || strncasecmp(cmd, A, _l) != 0) && \ - (_l >= sizeof(B) || strncasecmp(cmd, B, _l) != 0)) \ - goto invalid_option; \ - } while (0) -#define FULLCHECK6(A, B, C, D, E, F) \ - do { \ - size_t _l = strlen(cmd); \ - if ((_l >= sizeof(A) || strncasecmp(cmd, A, _l) != 0) && \ - (_l >= sizeof(B) || strncasecmp(cmd, B, _l) != 0) && \ - (_l >= sizeof(C) || strncasecmp(cmd, C, _l) != 0) && \ - (_l >= sizeof(D) || strncasecmp(cmd, D, _l) != 0) && \ - (_l >= sizeof(E) || strncasecmp(cmd, E, _l) != 0) && \ - (_l >= sizeof(F) || strncasecmp(cmd, F, _l) != 0)) \ - goto invalid_option; \ - } while (0) - switch (cmd[0]) { case 'a': switch (cmd[1]) { @@ -1937,10 +2042,15 @@ plus_option(char *option, bool is_batchfile, bool *need_clone, } break; case 'l': - FULLCHECK("tls"); - lookup->tls_mode = state; - if (!lookup->tcp_mode_set) { - lookup->tcp_mode = state; + switch (cmd[2]) { + case 's': + if (!plus_tls_options(cmd, value, state, + lookup)) { + goto invalid_option; + } + break; + default: + goto invalid_option; } break; case 'o': diff --git a/bin/dig/dig.rst b/bin/dig/dig.rst index 006b9f8086..a5bfb86556 100644 --- a/bin/dig/dig.rst +++ b/bin/dig/dig.rst @@ -633,6 +633,27 @@ abbreviation is unambiguous; for example, :option:`+cd` is equivalent to name servers. When this option is in use, the port number defaults to 853. +.. option:: +tls-ca[=file-name], +notls-ca + + This option enables remote server TLS certificate validation for + DNS transports, relying on TLS. Certificate authorities + certificates are loaded from the specified PEM file + (``file-name``). If the file is not specified, the default + certificates from the global certificates store are used. + +.. option:: +tls-certfile=file-name, +tls-keyfile=file-name, +notls-certfile, +notls-keyfile + + These options set the state of certificate-based client + authentication for DNS transports, relying on TLS. Both certificate + chain file and private key file are expected to be in PEM format. + Both options must be specified at the same time. + +.. option:: +tls-hostname=hostname, +notls-hostname + + This option makes :program:`dig` use the provided hostname during remote + server TLS certificate verification. Otherwise, the DNS server name + is used. This option has no effect if :option:`+tls-ca` is not specified. + .. option:: +topdown, +notopdown This feature is related to :option:`dig +sigchase`, which is obsolete and diff --git a/bin/dig/dighost.c b/bin/dig/dighost.c index ef16a727d8..8eb5895f10 100644 --- a/bin/dig/dighost.c +++ b/bin/dig/dighost.c @@ -642,6 +642,8 @@ make_empty_lookup(void) { ISC_LIST_INIT(looknew->q); ISC_LIST_INIT(looknew->my_server_list); + looknew->tls_ctx_cache = isc_tlsctx_cache_new(mctx); + isc_refcount_init(&looknew->references, 1); looknew->magic = DIG_LOOKUP_MAGIC; @@ -732,6 +734,30 @@ clone_lookup(dig_lookup_t *lookold, bool servers) { looknew->https_get = lookold->https_get; looknew->http_plain = lookold->http_plain; + looknew->tls_ca_set = lookold->tls_ca_set; + if (lookold->tls_ca_file != NULL) { + looknew->tls_ca_file = isc_mem_strdup(mctx, + lookold->tls_ca_file); + }; + + looknew->tls_hostname_set = lookold->tls_hostname_set; + if (lookold->tls_hostname != NULL) { + looknew->tls_hostname = isc_mem_strdup(mctx, + lookold->tls_hostname); + } + + looknew->tls_key_file_set = lookold->tls_key_file_set; + if (lookold->tls_key_file != NULL) { + looknew->tls_key_file = isc_mem_strdup(mctx, + lookold->tls_key_file); + } + + looknew->tls_cert_file_set = lookold->tls_cert_file_set; + if (lookold->tls_cert_file != NULL) { + looknew->tls_cert_file = isc_mem_strdup(mctx, + lookold->tls_cert_file); + } + looknew->showbadcookie = lookold->showbadcookie; looknew->sendcookie = lookold->sendcookie; looknew->seenbadcookie = lookold->seenbadcookie; @@ -797,6 +823,11 @@ clone_lookup(dig_lookup_t *lookold, bool servers) { dns_fixedname_name(&looknew->fdomain)); if (servers) { + if (lookold->tls_ctx_cache != NULL) { + isc_tlsctx_cache_detach(&looknew->tls_ctx_cache); + isc_tlsctx_cache_attach(lookold->tls_ctx_cache, + &looknew->tls_ctx_cache); + } clone_server_list(lookold->my_server_list, &looknew->my_server_list); } @@ -1601,6 +1632,26 @@ _destroy_lookup(dig_lookup_t *lookup) { isc_mem_free(mctx, lookup->https_path); } + if (lookup->tls_ctx_cache != NULL) { + isc_tlsctx_cache_detach(&lookup->tls_ctx_cache); + } + + if (lookup->tls_ca_file != NULL) { + isc_mem_free(mctx, lookup->tls_ca_file); + } + + if (lookup->tls_hostname != NULL) { + isc_mem_free(mctx, lookup->tls_hostname); + } + + if (lookup->tls_key_file != NULL) { + isc_mem_free(mctx, lookup->tls_key_file); + } + + if (lookup->tls_cert_file != NULL) { + isc_mem_free(mctx, lookup->tls_cert_file); + } + isc_mem_free(mctx, lookup); } @@ -2720,6 +2771,106 @@ _cancel_lookup(dig_lookup_t *lookup, const char *file, unsigned int line) { check_if_done(); } +static isc_tlsctx_t * +get_create_tls_context(dig_query_t *query, const bool is_https) { + isc_result_t result; + isc_tlsctx_t *ctx = NULL, *found_ctx = NULL; + isc_tls_cert_store_t *store = NULL, *found_store = NULL; + char tlsctxname[ISC_SOCKADDR_FORMATSIZE]; + const uint16_t family = isc_sockaddr_pf(&query->sockaddr) == PF_INET6 + ? AF_INET6 + : AF_INET; + isc_tlsctx_cache_transport_t transport = + is_https ? isc_tlsctx_cache_https : isc_tlsctx_cache_tls; + const bool hostname_ignore_subject = !is_https; + + if (query->lookup->tls_key_file_set != query->lookup->tls_cert_file_set) + { + return (NULL); + } + + isc_sockaddr_format(&query->sockaddr, tlsctxname, sizeof(tlsctxname)); + + result = isc_tlsctx_cache_find(query->lookup->tls_ctx_cache, tlsctxname, + transport, family, &found_ctx, + &found_store); + if (result != ISC_R_SUCCESS) { + if (query->lookup->tls_ca_set) { + if (found_store == NULL) { + result = isc_tls_cert_store_create( + query->lookup->tls_ca_file, &store); + + if (result != ISC_R_SUCCESS) { + goto failure; + } + } else { + store = found_store; + } + } + + result = isc_tlsctx_createclient(&ctx); + if (result != ISC_R_SUCCESS) { + goto failure; + } + + if (store != NULL) { + const char *hostname = + query->lookup->tls_hostname_set + ? query->lookup->tls_hostname + : query->userarg; + /* + * According to RFC 8310, Subject field MUST NOT be + * inspected when verifying hostname for DoT. Only + * SubjectAltName must be checked. That is NOT the case + * for HTTPS. + */ + result = isc_tlsctx_enable_peer_verification( + ctx, false, store, hostname, + hostname_ignore_subject); + if (result != ISC_R_SUCCESS) { + goto failure; + } + } + + if (query->lookup->tls_key_file_set && + query->lookup->tls_cert_file_set) { + result = isc_tlsctx_load_certificate( + ctx, query->lookup->tls_key_file, + query->lookup->tls_cert_file); + if (result != ISC_R_SUCCESS) { + goto failure; + } + } + + if (!is_https) { + isc_tlsctx_enable_dot_client_alpn(ctx); + } + +#if HAVE_LIBNGHTTP2 + if (is_https) { + isc_tlsctx_enable_http2client_alpn(ctx); + } +#endif /* HAVE_LIBNGHTTP2 */ + + result = isc_tlsctx_cache_add(query->lookup->tls_ctx_cache, + tlsctxname, transport, family, + ctx, store, NULL, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + return (ctx); + } + + INSIST(!query->lookup->tls_ca_set || found_store != NULL); + return (found_ctx); +failure: + if (ctx != NULL && found_ctx != ctx) { + isc_tlsctx_free(&ctx); + } + if (store != NULL && store != found_store) { + isc_tls_cert_store_free(&store); + } + return (NULL); +} + static void tcp_connected(isc_nmhandle_t *handle, isc_result_t eresult, void *arg); @@ -2733,18 +2884,22 @@ start_tcp(dig_query_t *query) { isc_result_t result; dig_query_t *next = NULL; dig_query_t *connectquery = NULL; + isc_tlsctx_t *tlsctx = NULL; + bool tls_mode = false; REQUIRE(DIG_VALID_QUERY(query)); debug("start_tcp(%p)", query); query_attach(query, &query->lookup->current_query); + tls_mode = dig_lookup_is_tls(query->lookup); + /* * For TLS connections, we want to override the default * port number. */ if (!port_set) { - if (query->lookup->tls_mode) { + if (tls_mode) { port = 853; } else if (query->lookup->https_mode && !query->lookup->http_plain) { @@ -2824,14 +2979,15 @@ start_tcp(dig_query_t *query) { query_attach(query, &connectquery); - if (query->lookup->tls_mode) { - result = isc_tlsctx_createclient(&query->tlsctx); - RUNTIME_CHECK(result == ISC_R_SUCCESS); - isc_tlsctx_enable_dot_client_alpn(query->tlsctx); + if (tls_mode) { + tlsctx = get_create_tls_context(connectquery, false); + if (tlsctx == NULL) { + goto failure_tls; + } isc_nm_tlsdnsconnect(netmgr, &localaddr, &query->sockaddr, tcp_connected, connectquery, local_timeout, 0, - query->tlsctx); + tlsctx); #if HAVE_LIBNGHTTP2 } else if (query->lookup->https_mode) { char uri[4096] = { 0 }; @@ -2841,17 +2997,17 @@ start_tcp(dig_query_t *query) { uri, sizeof(uri)); if (!query->lookup->http_plain) { - result = - isc_tlsctx_createclient(&query->tlsctx); - RUNTIME_CHECK(result == ISC_R_SUCCESS); - isc_tlsctx_enable_http2client_alpn( - query->tlsctx); + tlsctx = get_create_tls_context(connectquery, + true); + if (tlsctx == NULL) { + goto failure_tls; + } } isc_nm_httpconnect(netmgr, &localaddr, &query->sockaddr, uri, !query->lookup->https_get, - tcp_connected, connectquery, - query->tlsctx, local_timeout, 0); + tcp_connected, connectquery, tlsctx, + local_timeout, 0); #endif } else { isc_nm_tcpdnsconnect(netmgr, &localaddr, @@ -2861,6 +3017,29 @@ start_tcp(dig_query_t *query) { /* XXX: set DSCP */ } + + return; +failure_tls: + if (query->lookup->tls_key_file_set != query->lookup->tls_cert_file_set) + { + dighost_warning( + "both TLS client certificate and key file must be " + "specified a the same time"); + } else { + dighost_warning("TLS context cannot be created"); + } + + if (ISC_LINK_LINKED(query, link)) { + next = ISC_LIST_NEXT(query, link); + } else { + next = NULL; + } + query_detach(&query); + if (next == NULL) { + clear_current_lookup(); + } else { + start_tcp(next); + } } static void @@ -3289,16 +3468,27 @@ tcp_connected(isc_nmhandle_t *handle, isc_result_t eresult, void *arg) { LOCK_LOOKUP; lookup_attach(query->lookup, &l); - if (query->tlsctx != NULL) { - isc_tlsctx_free(&query->tlsctx); - } - - if (eresult == ISC_R_CANCELED || query->canceled) { + if (eresult == ISC_R_CANCELED || eresult == ISC_R_TLSBADPEERCERT || + query->canceled) + { debug("in cancel handler"); isc_sockaddr_format(&query->sockaddr, sockstr, sizeof(sockstr)); + if (eresult == ISC_R_TLSBADPEERCERT) { + dighost_warning( + "TLS peer certificate verification for " + "%s failed: %s", + sockstr, + isc_nm_verify_tls_peer_result_string(handle)); + } else if (query->lookup->rdtype == dns_rdatatype_ixfr || + query->lookup->rdtype == dns_rdatatype_axfr) + { + puts("; Transfer failed."); + } + if (!query->canceled) { cancel_lookup(l); } + query_detach(&query); lookup_detach(&l); clear_current_lookup(); @@ -4633,3 +4823,12 @@ dig_idnsetup(dig_lookup_t *lookup, bool active) { return; #endif /* HAVE_LIBIDN2 */ } + +bool +dig_lookup_is_tls(const dig_lookup_t *lookup) { + if (lookup->tls_mode || (lookup->tls_ca_set && !lookup->https_mode)) { + return (true); + } + + return (false); +} diff --git a/bin/dig/dighost.h b/bin/dig/dighost.h index 43c72777de..d79cc6fc1a 100644 --- a/bin/dig/dighost.h +++ b/bin/dig/dighost.h @@ -177,6 +177,17 @@ struct dig_lookup { bool https_get; char *https_path; }; + struct { + bool tls_ca_set; + char *tls_ca_file; + bool tls_hostname_set; + char *tls_hostname; + bool tls_cert_file_set; + char *tls_cert_file; + bool tls_key_file_set; + char *tls_key_file; + isc_tlsctx_cache_t *tls_ctx_cache; + }; }; /*% The dig_query structure */ @@ -209,7 +220,6 @@ struct dig_query { isc_time_t time_recv; uint64_t byte_count; isc_timer_t *timer; - isc_tlsctx_t *tlsctx; }; struct dig_server { @@ -447,4 +457,7 @@ dig_idnsetup(dig_lookup_t *lookup, bool active); void dig_shutdown(void); +bool +dig_lookup_is_tls(const dig_lookup_t *lookup); + ISC_LANG_ENDDECLS diff --git a/doc/man/dig.1in b/doc/man/dig.1in index bb3c265b29..d5f42ed852 100644 --- a/doc/man/dig.1in +++ b/doc/man/dig.1in @@ -735,6 +735,30 @@ to 853. .UNINDENT .INDENT 0.0 .TP +.B +tls\-ca[=file\-name], +notls\-ca +This option enables remote server TLS certificate validation for +DNS transports, relying on TLS. Certificate authorities +certificates are loaded from the specified PEM file +(\fBfile\-name\fP). If the file is not specified, the default +certificates from the global certificates store are used. +.UNINDENT +.INDENT 0.0 +.TP +.B +tls\-certfile=file\-name, +tls\-keyfile=file\-name, +notls\-certfile, +notls\-keyfile +These options set the state of certificate\-based client +authentication for DNS transports, relying on TLS. Both certificate +chain file and private key file are expected to be in PEM format. +Both options must be specified at the same time. +.UNINDENT +.INDENT 0.0 +.TP +.B +tls\-hostname=hostname, +notls\-hostname +This option makes \fBdig\fP use the provided hostname during remote +server TLS certificate verification. Otherwise, the DNS server name +is used. This option has no effect if \fI\%+tls\-ca\fP is not specified. +.UNINDENT +.INDENT 0.0 +.TP .B +topdown, +notopdown This feature is related to \fI\%dig +sigchase\fP, which is obsolete and has been removed. Use \fI\%delv\fP instead.