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.
This commit is contained in:
Artem Boldariev
2022-01-19 13:10:08 +02:00
parent 783663db80
commit fd38a4e1bf
5 changed files with 409 additions and 49 deletions

View 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':

View 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

View 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);
}

View 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

View 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.