]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Add support for Strict/Mutual TLS to dig
authorArtem Boldariev <artem@boldariev.com>
Wed, 19 Jan 2022 11:10:08 +0000 (13:10 +0200)
committerArtem Boldariev <artem@boldariev.com>
Mon, 28 Mar 2022 13:22:53 +0000 (16:22 +0300)
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
bin/dig/dig.rst
bin/dig/dighost.c
bin/dig/dighost.h
doc/man/dig.1in

index 6ad7f29da3b303eb7dfe5d0a9048c3956a05a403..0f50739b92ec2b035ad10f7c07b95db3aab16652 100644 (file)
@@ -289,6 +289,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"
@@ -340,7 +348,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) {
@@ -1015,6 +1023,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
@@ -1044,31 +1174,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':
index 07505b18dc493c9f2a0d2fee7a86e13c9644e11c..ac56d82ab399e7008f48feb1dffa3d1aaa7c4f79 100644 (file)
@@ -632,6 +632,24 @@ abbreviation is unambiguous; for example, ``+cd`` is equivalent to
    name servers. When this option is in use, the port number defaults
    to 853.
 
+``+[no]tls-ca[=file-name]``
+   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.
+
+``+[no]tls-certfile=file-name`` and ``+[no]tls-keyfile=file-name``
+   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.
+
+``+[no]tls-hostname=hostname``
+   This option makes ``dig`` use the provided hostname during remote
+   server TLS certificate verification. Otherwise, the DNS server name
+   is used. This option has no effect if ``+tls-ca`` is not specified.
+
 .. option:: +[no]topdown
 
    This feature is related to ``dig +sigchase``, which is obsolete and
index ef47570f2d16c67d738226aa443c9d3bc5bcee8c..661ca6e8abf3b9df87cf9aa416d03a799951484f 100644 (file)
@@ -639,6 +639,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;
@@ -729,6 +731,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;
@@ -794,6 +820,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);
        }
@@ -1574,6 +1605,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);
 }
 
@@ -2688,6 +2739,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);
 
@@ -2701,18 +2852,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) {
@@ -2792,14 +2947,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,
-                                            query->tlsctx);
+                                            tlsctx);
 #if HAVE_LIBNGHTTP2
                } else if (query->lookup->https_mode) {
                        char uri[4096] = { 0 };
@@ -2809,17 +2965,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);
+                                          tcp_connected, connectquery, tlsctx,
+                                          local_timeout);
 #endif
                } else {
                        isc_nm_tcpdnsconnect(netmgr, &localaddr,
@@ -2846,6 +3002,29 @@ start_tcp(dig_query_t *query) {
                        start_tcp(next);
                }
        }
+
+       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
@@ -3250,16 +3429,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();
@@ -4571,3 +4761,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);
+}
index afefed40c27828564277400695982fc95b5b0ad9..3e3e0a0d8bf70c960e26a4a57cc394fe1fb01ffa 100644 (file)
@@ -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
index 670c72e9b57ca4ea92cc1a2161df740d1ce687c7..ae3aee6db22ce1ffbfc5cadae63fc792a1f68f6c 100644 (file)
@@ -734,6 +734,26 @@ to 853.
 .UNINDENT
 .INDENT 0.0
 .TP
+.B \fB+[no]tls\-ca[=file\-name]\fP
+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.
+.TP
+.B \fB+[no]tls\-certfile=file\-name\fP and \fB+[no]tls\-keyfile=file\-name\fP
+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.
+.TP
+.B \fB+[no]tls\-hostname=hostname\fP
+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 \fB+tls\-ca\fP is not specified.
+.UNINDENT
+.INDENT 0.0
+.TP
 .B +[no]topdown
 This feature is related to \fBdig +sigchase\fP, which is obsolete and
 has been removed. Use \fBdelv\fP instead.