resolvectl: support JSON output for openpgp and tlsa

The openpgp and tlsa verbs rejected --json= even though they
already determine the RR type they query. Allow JSON output for
these shortcut commands.

Accept matching explicit --type= values as well, but reject
mismatched types so openpgp only queries OPENPGPKEY records and
tlsa only queries TLSA records.

Refuse unauthenticated key records in JSON format so scripts do
not accidentally consume cryptographic DNS data that would only
produce a warning in human-readable output.

Fixes #43245
This commit is contained in:
dongshengyuan
2026-08-02 16:52:50 +08:00
committed by Yu Watanabe
parent 1652be7df8
commit 05ea7e87b6
6 changed files with 89 additions and 16 deletions

View File

@@ -103,6 +103,12 @@
are converted to the corresponding DNS domain name, and any <constant class='dns'>OPENPGPKEY</constant>
keys are printed.</para>
<para>The resource record type is implied by this command. If <option>--type=</option> is specified,
it must be <constant class='dns'>OPENPGPKEY</constant>.</para>
<para>If combined with <option>--json=</option>, outputs the resource record data in JSON format.
Unauthenticated key data is refused in JSON format.</para>
<xi:include href="version-info.xml" xpointer="v239"/></listitem>
</varlistentry>
@@ -119,6 +125,12 @@
<constant>443</constant> will be used by default. The family may be specified as the first argument,
otherwise <constant>tcp</constant> will be used.</para>
<para>The resource record type is implied by this command. If <option>--type=</option> is specified,
it must be <constant class='dns'>TLSA</constant>.</para>
<para>If combined with <option>--json=</option>, outputs the resource record data in JSON format.
Unauthenticated key data is refused in JSON format.</para>
<xi:include href="version-info.xml" xpointer="v239"/></listitem>
</varlistentry>

View File

@@ -567,6 +567,8 @@ static int output_rr_packet(DnsResourceRecord *rr, int ifindex) {
return 0;
}
static DEFINE_POINTER_ARRAY_FREE_FUNC(DnsResourceRecord*, dns_resource_record_unref);
static int idna_candidate(const char *name, char **ret) {
_cleanup_free_ char *idnafied = NULL;
int r;
@@ -668,32 +670,50 @@ static int resolve_record(const char *name, uint16_t class, uint16_t type, bool
if (r < 0)
return r;
if (reply.n_records == 0) {
if (warn_missing)
log_error("%s: no records found", name);
return -ESRCH;
}
DnsResourceRecord **rrs = new0(DnsResourceRecord*, reply.n_records);
size_t n_rrs = reply.n_records;
if (!rrs)
return log_oom();
CLEANUP_ARRAY(rrs, n_rrs, dns_resource_record_unref_array);
bool json = sd_json_format_enabled(arg_json_format_flags);
bool needs_authentication = false;
FOREACH_ARRAY(record, reply.records, reply.n_records) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
r = dns_resource_record_new_from_raw(&rr, record->raw.iov_base, record->raw.iov_len);
size_t i = record - reply.records;
r = dns_resource_record_new_from_raw(&rrs[i], record->raw.iov_base, record->raw.iov_len);
if (r < 0)
return log_error_errno(r, "Failed to parse RR: %m");
if (dns_type_needs_authentication(rrs[i]->key->type)) {
needs_authentication = true;
if (json && !FLAGS_SET(reply.flags, SD_RESOLVED_AUTHENTICATED))
return log_error_errno(SYNTHETIC_ERRNO(EKEYREJECTED),
"Refusing to output unauthenticated DNS records that require "
"authentication in JSON format.");
}
}
FOREACH_ARRAY(record, reply.records, reply.n_records) {
size_t i = record - reply.records;
if (arg_raw == RAW_PACKET) {
uint64_t u64 = htole64(record->raw.iov_len);
fwrite(&u64, sizeof(u64), 1, stdout);
fwrite(record->raw.iov_base, 1, record->raw.iov_len, stdout);
} else {
r = output_rr_packet(rr, record->ifindex);
r = output_rr_packet(rrs[i], record->ifindex);
if (r < 0)
return r;
}
if (dns_type_needs_authentication(rr->key->type))
needs_authentication = true;
}
if (reply.n_records == 0) {
if (warn_missing)
log_error("%s: no records found", name);
return -ESRCH;
}
print_source(reply.flags, ts);
@@ -1042,8 +1062,8 @@ static int verb_openpgp(int argc, char *argv[], uintptr_t _data, void *userdata)
#if HAVE_OPENSSL
int ret = 0;
if (sd_json_format_enabled(arg_json_format_flags))
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Use --json=pretty with --type= to acquire resource record information in JSON format.");
if (!IN_SET(arg_type, 0, DNS_TYPE_OPENPGPKEY))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The openpgp command may only be combined with --type=OPENPGPKEY.");
STRV_FOREACH(p, strv_skip(argv, 1))
RET_GATHER(ret, resolve_openpgp(*p));
@@ -1098,8 +1118,8 @@ static int verb_tlsa(int argc, char *argv[], uintptr_t _data, void *userdata) {
assert(argc >= 2);
if (sd_json_format_enabled(arg_json_format_flags))
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Use --json=pretty with --type= to acquire resource record information in JSON format.");
if (!IN_SET(arg_type, 0, DNS_TYPE_TLSA))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The tlsa command may only be combined with --type=TLSA.");
if (service_family_is_valid(argv[1])) {
family = argv[1];
@@ -3659,6 +3679,9 @@ static int native_parse_argv(int argc, char *argv[], char ***remaining_args) {
break;
}
if (arg_raw != RAW_NONE && sd_json_format_enabled(arg_json_format_flags))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--raw and --json= may not be combined.");
if (arg_type == 0 && arg_class != 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"--class= may only be used in conjunction with --type=.");

View File

@@ -2264,6 +2264,22 @@ TEST(dns_resource_record_to_json_soa) {
ASSERT_EQ(sd_json_variant_unsigned(sd_json_variant_by_key(j, "minimum")), UINT64_C(3600));
}
TEST(dns_resource_record_to_json_openpgpkey) {
static const uint8_t data[] = { 0, 1, 2, 3 };
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr =
ASSERT_NOT_NULL(dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_OPENPGPKEY, "www.example.com"));
ASSERT_NOT_NULL(rr->generic.data = memdup(data, sizeof(data)));
rr->generic.data_size = sizeof(data);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
ASSERT_OK(dns_resource_record_to_json(rr, &j));
sd_json_variant *key = ASSERT_NOT_NULL(sd_json_variant_by_key(j, "key"));
ASSERT_EQ(sd_json_variant_unsigned(sd_json_variant_by_key(key, "type")), (uint64_t) DNS_TYPE_OPENPGPKEY);
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(j, "data")), "AAECAw==");
}
TEST(dns_resource_record_to_string_ptr) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
const char *str;

View File

@@ -2487,6 +2487,12 @@ int dns_resource_record_to_json(DnsResourceRecord *rr, sd_json_variant **ret) {
SD_JSON_BUILD_PAIR_UNSIGNED("matchingType", rr->tlsa.matching_type),
SD_JSON_BUILD_PAIR_HEX("data", rr->tlsa.data, rr->tlsa.data_size));
case DNS_TYPE_OPENPGPKEY:
return sd_json_buildo(
ret,
SD_JSON_BUILD_PAIR_VARIANT("key", k),
SD_JSON_BUILD_PAIR_BASE64("data", rr->generic.data, rr->generic.data_size));
case DNS_TYPE_SVCB:
case DNS_TYPE_HTTPS: {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *p = NULL;

View File

@@ -24,3 +24,5 @@ mail A 10.0.0.122
myservice A 10.0.0.123
AAAA fd00:dead:beef:cafe::123
_mysvc._tcp SRV 10 5 1234 myservice
5a786cdc59c161cdafd818143705026636962198c66ed4c5b3da321e._openpgpkey OPENPGPKEY AAECAw==

View File

@@ -607,6 +607,20 @@ testcase_08_resolved() {
run resolvectl openpgp mr.smith@signed.test
grep -qF "5a786cdc59c161cdafd818143705026636962198c66ed4c5b3da321e._openpgpkey.signed.test" "$RUN_OUT"
grep -qF "authenticated: yes" "$RUN_OUT"
run resolvectl openpgp mr.smith@signed.test --json=short
grep -qF '"key":{"class":1,"type":61,"name":"5a786cdc59c161cdafd818143705026636962198c66ed4c5b3da321e._openpgpkey.signed.test"}' "$RUN_OUT"
grep -qF '"data":"' "$RUN_OUT"
(! run resolvectl openpgp mr.smith@untrusted.test --json=short)
grep -qF "Refusing to output unauthenticated DNS records that require authentication in JSON format." "$RUN_OUT"
run resolvectl openpgp mr.smith@signed.test --json=short --type=OPENPGPKEY
grep -qF '"key":{"class":1,"type":61,"name":"5a786cdc59c161cdafd818143705026636962198c66ed4c5b3da321e._openpgpkey.signed.test"}' "$RUN_OUT"
grep -qF '"data":"' "$RUN_OUT"
(! run resolvectl openpgp mr.smith@signed.test --json=short --type=A)
grep -qF -- "The openpgp command may only be combined with --type=OPENPGPKEY." "$RUN_OUT"
(! run resolvectl tlsa signed.test:invalid --json=short)
grep -qF 'Invalid port "invalid".' "$RUN_OUT"
(! run resolvectl tlsa signed.test --json=short --type=A)
grep -qF -- "The tlsa command may only be combined with --type=TLSA." "$RUN_OUT"
# Check zone transfers (AXFR/IXFR)
# Note: since resolved doesn't support zone transfers, let's just make sure it
# simply refuses such requests without choking on them