systemd-resolve: easy querying of TLSA records
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Thu, 18 Feb 2016 02:08:57 +0000 (21:08 -0500)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Thu, 18 Feb 2016 16:41:40 +0000 (11:41 -0500)
$ systemd-resolve --tlsa fedoraproject.org
_443._tcp.fedoraproject.org IN TLSA 0 0 1 GUAL5bejH7czkXcAeJ0vCiRxwMnVBsDlBMBsFtfLF8A=
        -- Cert. usage: CA constraint
        -- Selector: Full Certificate
        -- Matching type: SHA-256

$ systemd-resolve --tlsa=tcp fedoraproject.org:443
_443._tcp.fedoraproject.org IN TLSA 0 0 1 GUAL5bejH7czkXcAeJ0vCiRxwMnVBsDlBMBsFtfLF8A=
        ...

$ systemd-resolve --tlsa=udp fedoraproject.org
_443._udp.fedoraproject.org: resolve call failed: '_443._udp.fedoraproject.org' not found

v2:
- use uint16_t
- refuse port 0

man/systemd-resolve.xml
src/resolve/resolve-tool.c

index c288fd9..320663c 100644 (file)
     <cmdsynopsis>
       <command>systemd-resolve</command>
       <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <command> --tlsa</command>
+      <arg choice="plain"><replaceable>DOMAIN<optional>:PORT</optional></replaceable></arg>
+    </cmdsynopsis>
+
+    <cmdsynopsis>
+      <command>systemd-resolve</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
       <command> --statistics</command>
     </cmdsynopsis>
 
     is assumed to be a domain name, that is already prefixed with an SRV type, and an SRV lookup is done (no
     TXT).</para>
 
-    <para>The <option>--openpgp</option> switch may be use to query PGP keys stored as the
+    <para>The <option>--openpgp</option> switch may be used to query PGP keys stored as
     <ulink url="https://tools.ietf.org/html/draft-wouters-dane-openpgp-02">OPENPGPKEY</ulink> resource records.
     When this option is specified one or more e-mail address must be specified.</para>
 
+    <para>The <option>--tlsa</option> switch maybe be used to query TLS public
+    keys stored as
+    <ulink url="https://tools.ietf.org/html/rfc6698">TLSA</ulink> resource records.
+    When this option is specified one or more domain names must be specified.</para>
+
     <para>The <option>--statistics</option> switch may be used to show resolver statistics, including information about
     the number of successful and failed DNSSEC validations.</para>
 
       </varlistentry>
 
       <varlistentry>
+        <term><option>--tlsa</option></term>
+
+        <listitem><para>Enables TLSA resource record resolution (see above).
+        A query will be performed for each of the specified names prefixed with
+        the port and family
+        (<literal>_<replaceable>port</replaceable>._<replaceable>family</replaceable>.<replaceable>domain</replaceable></literal>).
+        The port number may be specified after a colon
+        (<literal>:</literal>), otherwise <constant>443</constant> will be used
+        by default. The family may be specified as an argument after
+        <option>--tlsa</option>, otherwise <constant>tcp</constant> will be
+        used.</para></listitem>
+      </varlistentry>
+
+      <varlistentry>
         <term><option>--cname=</option><replaceable>BOOL</replaceable></term>
 
         <listitem><para>Takes a boolean parameter. If true (the default), DNS CNAME or DNAME redirections are
@@ -325,6 +351,18 @@ d08ee310438ca124a6149ea5cc21b6313b390dce485576eff96f8722._openpgpkey.fedoraproje
         ...
 </programlisting>
     </example>
+
+    <example>
+      <title>Retrieve a TLS key (<literal>=tcp</literal> and
+      <literal>:443</literal> could be skipped)</title>
+
+      <programlisting>$ systemd-resolve --tlsa=tcp fedoraproject.org:443
+_443._tcp.fedoraproject.org IN TLSA 0 0 1 GUAL5bejH7czkXcAeJ0vCiRxwMnVBsDlBMBsFtfLF8A=
+        -- Cert. usage: CA constraint
+        -- Selector: Full Certificate
+        -- Matching type: SHA-256
+</programlisting>
+    </example>
   </refsect1>
 
   <refsect1>
index a519074..484fbb4 100644 (file)
@@ -44,12 +44,19 @@ static uint16_t arg_class = 0;
 static bool arg_legend = true;
 static uint64_t arg_flags = 0;
 
+typedef enum ServiceFamily {
+        SERVICE_FAMILY_TCP,
+        SERVICE_FAMILY_UDP,
+        SERVICE_FAMILY_SCTP,
+        _SERVICE_FAMILY_INVALID = -1,
+} ServiceFamily;
+static ServiceFamily arg_service_family = SERVICE_FAMILY_TCP;
+
 typedef enum RawType {
         RAW_NONE,
         RAW_PAYLOAD,
         RAW_PACKET,
 } RawType;
-
 static RawType arg_raw = RAW_NONE;
 
 static enum {
@@ -57,10 +64,34 @@ static enum {
         MODE_RESOLVE_RECORD,
         MODE_RESOLVE_SERVICE,
         MODE_RESOLVE_OPENPGP,
+        MODE_RESOLVE_TLSA,
         MODE_STATISTICS,
         MODE_RESET_STATISTICS,
 } arg_mode = MODE_RESOLVE_HOST;
 
+static ServiceFamily service_family_from_string(const char *s) {
+        if (s == NULL || streq(s, "tcp"))
+                return SERVICE_FAMILY_TCP;
+        if (streq(s, "udp"))
+                return SERVICE_FAMILY_UDP;
+        if (streq(s, "sctp"))
+                return SERVICE_FAMILY_SCTP;
+        return _SERVICE_FAMILY_INVALID;
+}
+
+static const char* service_family_to_string(ServiceFamily service) {
+        switch(service) {
+        case SERVICE_FAMILY_TCP:
+                return "_tcp";
+        case SERVICE_FAMILY_UDP:
+                return "_udp";
+        case SERVICE_FAMILY_SCTP:
+                return "_sctp";
+        default:
+                assert_not_reached("invalid service");
+        }
+}
+
 static void print_source(uint64_t flags, usec_t rtt) {
         char rtt_str[FORMAT_TIMESTAMP_MAX];
 
@@ -844,6 +875,38 @@ static int resolve_openpgp(sd_bus *bus, const char *address) {
                               arg_type ?: DNS_TYPE_OPENPGPKEY);
 }
 
+static int resolve_tlsa(sd_bus *bus, const char *address) {
+        const char *port;
+        uint16_t port_num = 443;
+        _cleanup_free_ char *full = NULL;
+        int r;
+
+        assert(bus);
+        assert(address);
+
+        port = strrchr(address, ':');
+        if (port) {
+                r = safe_atou16(port + 1, &port_num);
+                if (r < 0 || port_num == 0)
+                        return log_error_errno(r, "Invalid port \"%s\".", port + 1);
+
+                address = strndupa(address, port - address);
+        }
+
+        r = asprintf(&full, "_%u.%s.%s",
+                     port_num,
+                     service_family_to_string(arg_service_family),
+                     address);
+        if (r < 0)
+                return log_oom();
+
+        log_debug("Looking up \"%s\".", full);
+
+        return resolve_record(bus, full,
+                              arg_class ?: DNS_CLASS_IN,
+                              arg_type ?: DNS_TYPE_TLSA);
+}
+
 static int show_statistics(sd_bus *bus) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
@@ -1031,6 +1094,7 @@ static void help(void) {
                "     --service-address=BOOL Resolve address for services (default: yes)\n"
                "     --service-txt=BOOL     Resolve TXT records for services (default: yes)\n"
                "     --openpgp              Query OpenPGP public key\n"
+               "     --tlsa                 Query TLS public key\n"
                "     --cname=BOOL           Follow CNAME redirects (default: yes)\n"
                "     --search=BOOL          Use search domains for single-label names\n"
                "                                                              (default: yes)\n"
@@ -1050,6 +1114,7 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_SERVICE_ADDRESS,
                 ARG_SERVICE_TXT,
                 ARG_OPENPGP,
+                ARG_TLSA,
                 ARG_RAW,
                 ARG_SEARCH,
                 ARG_STATISTICS,
@@ -1069,6 +1134,7 @@ static int parse_argv(int argc, char *argv[]) {
                 { "service-address",  required_argument, NULL, ARG_SERVICE_ADDRESS  },
                 { "service-txt",      required_argument, NULL, ARG_SERVICE_TXT      },
                 { "openpgp",          no_argument,       NULL, ARG_OPENPGP          },
+                { "tlsa",             optional_argument, NULL, ARG_TLSA             },
                 { "raw",              optional_argument, NULL, ARG_RAW              },
                 { "search",           required_argument, NULL, ARG_SEARCH           },
                 { "statistics",       no_argument,       NULL, ARG_STATISTICS,      },
@@ -1183,6 +1249,15 @@ static int parse_argv(int argc, char *argv[]) {
                         arg_mode = MODE_RESOLVE_OPENPGP;
                         break;
 
+                case ARG_TLSA:
+                        arg_mode = MODE_RESOLVE_TLSA;
+                        arg_service_family = service_family_from_string(optarg);
+                        if (arg_service_family < 0) {
+                                log_error("Unknown service family \"%s\".", optarg);
+                                return -EINVAL;
+                        }
+                        break;
+
                 case ARG_RAW:
                         if (on_tty()) {
                                 log_error("Refusing to write binary data to tty.");
@@ -1261,7 +1336,7 @@ static int parse_argv(int argc, char *argv[]) {
                 return -EINVAL;
         }
 
-        if (arg_type != 0 && arg_mode != MODE_RESOLVE_RECORD) {
+        if (arg_type != 0 && arg_mode == MODE_RESOLVE_SERVICE) {
                 log_error("--service and --type= may not be combined.");
                 return -EINVAL;
         }
@@ -1378,6 +1453,24 @@ int main(int argc, char **argv) {
                 }
                 break;
 
+        case MODE_RESOLVE_TLSA:
+                if (argc < optind + 1) {
+                        log_error("Domain name required.");
+                        r = -EINVAL;
+                        goto finish;
+
+                }
+
+                r = 0;
+                while (optind < argc) {
+                        int k;
+
+                        k = resolve_tlsa(bus, argv[optind++]);
+                        if (k < 0)
+                                r = k;
+                }
+                break;
+
         case MODE_STATISTICS:
                 if (argc > optind) {
                         log_error("Too many arguments.");