Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dns: add TLSA record query support #52983

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions doc/api/dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ The following methods from the `node:dns` module are available:
* [`resolver.resolvePtr()`][`dns.resolvePtr()`]
* [`resolver.resolveSoa()`][`dns.resolveSoa()`]
* [`resolver.resolveSrv()`][`dns.resolveSrv()`]
* [`resolver.resolveTlsa()`][`dns.resolveTlsa()`]
* [`resolver.resolveTxt()`][`dns.resolveTxt()`]
* [`resolver.reverse()`][`dns.reverse()`]
* [`resolver.setServers()`][`dns.setServers()`]
Expand Down Expand Up @@ -444,6 +445,7 @@ records. The type and structure of individual results varies based on `rrtype`:
| `'PTR'` | pointer records | {string} | [`dns.resolvePtr()`][] |
| `'SOA'` | start of authority records | {Object} | [`dns.resolveSoa()`][] |
| `'SRV'` | service records | {Object} | [`dns.resolveSrv()`][] |
| `'TLSA'` | certificate associations | {Object} | [`dns.resolveTlsa()`][] |
| `'TXT'` | text records | {string\[]} | [`dns.resolveTxt()`][] |

On error, `err` is an [`Error`][] object, where `err.code` is one of the
Expand Down Expand Up @@ -543,6 +545,7 @@ will be present on the object:
| `'PTR'` | `value` |
| `'SOA'` | Refer to [`dns.resolveSoa()`][] |
| `'SRV'` | Refer to [`dns.resolveSrv()`][] |
| `'TLSA'` | Refer to [`dns.resolveTlsa()`][] |
| `'TXT'` | This type of record contains an array property called `entries` which refers to [`dns.resolveTxt()`][], e.g. `{ entries: ['...'], type: 'TXT' }` |

Here is an example of the `ret` object passed to the callback:
Expand Down Expand Up @@ -802,6 +805,41 @@ be an array of objects with the following properties:
}
```

## `dns.resolveTlsa(hostname, callback)`

<!-- YAML
added: REPLACEME
-->

<!--lint disable no-undefined-references list-item-bullet-indent-->

* `hostname` {string}
* `callback` {Function}
* `err` {Error}
* `records` {Object\[]}

<!--lint enable no-undefined-references list-item-bullet-indent-->

Uses the DNS protocol to resolve certificate associations (`TLSA` records) for
the `hostname`. The `records` argument passed to the `callback` function is an
array of objects with these properties:

* `certUsage`
* `selector`
* `match`
* `data`

<!-- eslint-skip -->

```js
{
certUsage: 3,
selector: 1,
match: 1,
data: [ArrayBuffer]
}
```

## `dns.resolveTxt(hostname, callback)`

<!-- YAML
Expand Down Expand Up @@ -1008,6 +1046,7 @@ The following methods from the `dnsPromises` API are available:
* [`resolver.resolvePtr()`][`dnsPromises.resolvePtr()`]
* [`resolver.resolveSoa()`][`dnsPromises.resolveSoa()`]
* [`resolver.resolveSrv()`][`dnsPromises.resolveSrv()`]
* [`resolver.resolveTlsa()`][`dnsPromises.resolveTlsa()`]
* [`resolver.resolveTxt()`][`dnsPromises.resolveTxt()`]
* [`resolver.reverse()`][`dnsPromises.reverse()`]
* [`resolver.setServers()`][`dnsPromises.setServers()`]
Expand Down Expand Up @@ -1212,6 +1251,7 @@ based on `rrtype`:
| `'PTR'` | pointer records | {string} | [`dnsPromises.resolvePtr()`][] |
| `'SOA'` | start of authority records | {Object} | [`dnsPromises.resolveSoa()`][] |
| `'SRV'` | service records | {Object} | [`dnsPromises.resolveSrv()`][] |
| `'TLSA'` | certificate associations | {Object} | [`dnsPromises.resolveTlsa()`][] |
| `'TXT'` | text records | {string\[]} | [`dnsPromises.resolveTxt()`][] |

On error, the `Promise` is rejected with an [`Error`][] object, where `err.code`
Expand Down Expand Up @@ -1276,6 +1316,7 @@ present on the object:
| `'PTR'` | `value` |
| `'SOA'` | Refer to [`dnsPromises.resolveSoa()`][] |
| `'SRV'` | Refer to [`dnsPromises.resolveSrv()`][] |
| `'TLSA'` | Refer to [`dnsPromises.resolveTlsa()`][] |
| `'TXT'` | This type of record contains an array property called `entries` which refers to [`dnsPromises.resolveTxt()`][], e.g. `{ entries: ['...'], type: 'TXT' }` |

Here is an example of the result object:
Expand Down Expand Up @@ -1458,6 +1499,34 @@ the following properties:
}
```

### `dnsPromises.resolveTlsa(hostname)`

<!-- YAML
added: REPLACEME
-->

* `hostname` {string}

Uses the DNS protocol to resolve certificate associations (`TLSA` records) for
the `hostname`. On success, the `Promise` is resolved with an array of objects
with these properties:

* `certUsage`
* `selector`
* `match`
* `data`

<!-- eslint-skip -->

```js
{
certUsage: 3,
selector: 1,
match: 1,
data: [ArrayBuffer]
}
```

### `dnsPromises.resolveTxt(hostname)`

<!-- YAML
Expand Down Expand Up @@ -1658,6 +1727,7 @@ uses. For instance, they do not use the configuration from `/etc/hosts`.
[`dns.resolvePtr()`]: #dnsresolveptrhostname-callback
[`dns.resolveSoa()`]: #dnsresolvesoahostname-callback
[`dns.resolveSrv()`]: #dnsresolvesrvhostname-callback
[`dns.resolveTlsa()`]: #dnsresolvetlsahostname-callback
[`dns.resolveTxt()`]: #dnsresolvetxthostname-callback
[`dns.reverse()`]: #dnsreverseip-callback
[`dns.setDefaultResultOrder()`]: #dnssetdefaultresultorderorder
Expand All @@ -1676,6 +1746,7 @@ uses. For instance, they do not use the configuration from `/etc/hosts`.
[`dnsPromises.resolvePtr()`]: #dnspromisesresolveptrhostname
[`dnsPromises.resolveSoa()`]: #dnspromisesresolvesoahostname
[`dnsPromises.resolveSrv()`]: #dnspromisesresolvesrvhostname
[`dnsPromises.resolveTlsa()`]: #dnspromisesresolvetlsahostname
[`dnsPromises.resolveTxt()`]: #dnspromisesresolvetxthostname
[`dnsPromises.reverse()`]: #dnspromisesreverseip
[`dnsPromises.setDefaultResultOrder()`]: #dnspromisessetdefaultresultorderorder
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/dns/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ const resolverKeys = [
'resolvePtr',
'resolveSoa',
'resolveSrv',
'resolveTlsa',
'resolveTxt',
'reverse',
];
Expand Down Expand Up @@ -300,6 +301,7 @@ function createResolverClass(resolver) {
Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname');
Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx');
Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs');
Resolver.prototype.resolveTlsa = resolveMap.TLSA = resolver('queryTlsa');
Resolver.prototype.resolveTxt = resolveMap.TXT = resolver('queryTxt');
Resolver.prototype.resolveSrv = resolveMap.SRV = resolver('querySrv');
Resolver.prototype.resolvePtr = resolveMap.PTR = resolver('queryPtr');
Expand Down
96 changes: 96 additions & 0 deletions src/cares_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
#include <vector>
#include <unordered_set>

#ifndef T_TLSA
#define T_TLSA 52 /* TLSA certificate association */
#endif

#ifndef T_CAA
# define T_CAA 257 /* Certification Authority Authorization */
#endif
Expand All @@ -57,6 +61,7 @@ namespace node {
namespace cares_wrap {

using v8::Array;
using v8::ArrayBuffer;
using v8::Context;
using v8::EscapableHandleScope;
using v8::Exception;
Expand Down Expand Up @@ -352,6 +357,65 @@ int ParseCaaReply(
return ARES_SUCCESS;
}

int ParseTlsaReply(Environment* env,
unsigned char* buf,
int len,
Local<Array> ret) {
EscapableHandleScope handle_scope(env->isolate());

ares_dns_record_t* dnsrec = nullptr;

int status = ares_dns_parse(buf, len, 0, &dnsrec);
if (status != ARES_SUCCESS) {
ares_dns_record_destroy(dnsrec);
return status;
}

uint32_t offset = ret->Length();
size_t rr_count = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ANSWER);

for (size_t i = 0; i < rr_count; i++) {
const ares_dns_rr_t* rr =
ares_dns_record_rr_get(dnsrec, ARES_SECTION_ANSWER, i);

if (ares_dns_rr_get_type(rr) != ARES_REC_TYPE_TLSA) continue;

unsigned char certusage = ares_dns_rr_get_u8(rr, ARES_RR_TLSA_CERT_USAGE);
unsigned char selector = ares_dns_rr_get_u8(rr, ARES_RR_TLSA_SELECTOR);
unsigned char match = ares_dns_rr_get_u8(rr, ARES_RR_TLSA_MATCH);
size_t data_len;
const unsigned char* data =
ares_dns_rr_get_bin(rr, ARES_RR_TLSA_DATA, &data_len);
if (!data || data_len == 0) continue;

Trott marked this conversation as resolved.
Show resolved Hide resolved
Local<ArrayBuffer> data_ab = ArrayBuffer::New(env->isolate(), data_len);
memcpy(data_ab->Data(), data, data_len);

Local<Object> tlsa_rec = Object::New(env->isolate());
tlsa_rec
->Set(env->context(),
env->cert_usage_string(),
Integer::NewFromUnsigned(env->isolate(), certusage))
.Check();
tlsa_rec
->Set(env->context(),
env->selector_string(),
Integer::NewFromUnsigned(env->isolate(), selector))
.Check();
tlsa_rec
->Set(env->context(),
env->match_string(),
Integer::NewFromUnsigned(env->isolate(), match))
.Check();
tlsa_rec->Set(env->context(), env->data_string(), data_ab).Check();

ret->Set(env->context(), offset + i, tlsa_rec).Check();
}

ares_dns_record_destroy(dnsrec);
return ARES_SUCCESS;
}

int ParseTxtReply(
Environment* env,
const unsigned char* buf,
Expand Down Expand Up @@ -861,6 +925,11 @@ int NsTraits::Send(QueryWrap<NsTraits>* wrap, const char* name) {
return ARES_SUCCESS;
}

int TlsaTraits::Send(QueryWrap<TlsaTraits>* wrap, const char* name) {
wrap->AresQuery(name, ARES_CLASS_IN, ARES_REC_TYPE_TLSA);
return ARES_SUCCESS;
}

int TxtTraits::Send(QueryWrap<TxtTraits>* wrap, const char* name) {
wrap->AresQuery(name, ARES_CLASS_IN, ARES_REC_TYPE_TXT);
return ARES_SUCCESS;
Expand Down Expand Up @@ -1045,6 +1114,10 @@ int AnyTraits::Parse(
if (!soa_record.IsEmpty())
ret->Set(env->context(), ret->Length(), soa_record).Check();

/* Parse TLSA records */
status = ParseTlsaReply(env, buf, len, ret);
if (status != ARES_SUCCESS && status != ARES_ENODATA) return status;

/* Parse CAA records */
status = ParseCaaReply(env, buf, len, ret, true);
if (status != ARES_SUCCESS && status != ARES_ENODATA)
Expand Down Expand Up @@ -1219,6 +1292,27 @@ int NsTraits::Parse(
return ARES_SUCCESS;
}

int TlsaTraits::Parse(QueryTlsaWrap* wrap,
const std::unique_ptr<ResponseData>& response) {
if (response->is_host) [[unlikely]] {
return ARES_EBADRESP;
}

unsigned char* buf = response->buf.data;
int len = response->buf.size;

Environment* env = wrap->env();
HandleScope handle_scope(env->isolate());
Context::Scope context_scope(env->context());

Local<Array> tlsa_records = Array::New(env->isolate());
int status = ParseTlsaReply(env, buf, len, tlsa_records);
if (status != ARES_SUCCESS) return status;

wrap->CallOnComplete(tlsa_records);
return ARES_SUCCESS;
}

int TxtTraits::Parse(
QueryTxtWrap* wrap,
const std::unique_ptr<ResponseData>& response) {
Expand Down Expand Up @@ -1998,6 +2092,7 @@ void Initialize(Local<Object> target,
SetProtoMethod(isolate, channel_wrap, "queryCname", Query<QueryCnameWrap>);
SetProtoMethod(isolate, channel_wrap, "queryMx", Query<QueryMxWrap>);
SetProtoMethod(isolate, channel_wrap, "queryNs", Query<QueryNsWrap>);
SetProtoMethod(isolate, channel_wrap, "queryTlsa", Query<QueryTlsaWrap>);
SetProtoMethod(isolate, channel_wrap, "queryTxt", Query<QueryTxtWrap>);
SetProtoMethod(isolate, channel_wrap, "querySrv", Query<QuerySrvWrap>);
SetProtoMethod(isolate, channel_wrap, "queryPtr", Query<QueryPtrWrap>);
Expand Down Expand Up @@ -2029,6 +2124,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(Query<QueryCnameWrap>);
registry->Register(Query<QueryMxWrap>);
registry->Register(Query<QueryNsWrap>);
registry->Register(Query<QueryTlsaWrap>);
registry->Register(Query<QueryTxtWrap>);
registry->Register(Query<QuerySrvWrap>);
registry->Register(Query<QueryPtrWrap>);
Expand Down
8 changes: 8 additions & 0 deletions src/cares_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,13 @@ struct NsTraits final {
const std::unique_ptr<ResponseData>& response);
};

struct TlsaTraits final {
static constexpr const char* name = "resolveTlsa";
static int Send(QueryWrap<TlsaTraits>* wrap, const char* name);
static int Parse(QueryWrap<TlsaTraits>* wrap,
const std::unique_ptr<ResponseData>& response);
};

struct TxtTraits final {
static constexpr const char* name = "resolveTxt";
static int Send(QueryWrap<TxtTraits>* wrap, const char* name);
Expand Down Expand Up @@ -515,6 +522,7 @@ using QueryCaaWrap = QueryWrap<CaaTraits>;
using QueryCnameWrap = QueryWrap<CnameTraits>;
using QueryMxWrap = QueryWrap<MxTraits>;
using QueryNsWrap = QueryWrap<NsTraits>;
using QueryTlsaWrap = QueryWrap<TlsaTraits>;
using QueryTxtWrap = QueryWrap<TxtTraits>;
using QuerySrvWrap = QueryWrap<SrvTraits>;
using QueryPtrWrap = QueryWrap<PtrTraits>;
Expand Down
4 changes: 4 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
V(cached_data_rejected_string, "cachedDataRejected") \
V(cached_data_string, "cachedData") \
V(cache_key_string, "cacheKey") \
V(cert_usage_string, "certUsage") \
V(change_string, "change") \
V(changes_string, "changes") \
V(channel_string, "channel") \
Expand Down Expand Up @@ -135,6 +136,7 @@
V(dns_ptr_string, "PTR") \
V(dns_soa_string, "SOA") \
V(dns_srv_string, "SRV") \
V(dns_tlsa_string, "TLSA") \
V(dns_txt_string, "TXT") \
V(done_string, "done") \
V(duration_string, "duration") \
Expand Down Expand Up @@ -237,6 +239,7 @@
V(line_number_string, "lineNumber") \
V(loop_count, "loopCount") \
V(mac_string, "mac") \
V(match_string, "match") \
V(max_buffer_string, "maxBuffer") \
V(max_concurrent_streams_string, "maxConcurrentStreams") \
V(message_port_constructor_string, "MessagePort") \
Expand Down Expand Up @@ -336,6 +339,7 @@
V(script_id_string, "scriptId") \
V(script_name_string, "scriptName") \
V(search_string, "search") \
V(selector_string, "selector") \
V(serial_number_string, "serialNumber") \
V(serial_string, "serial") \
V(servername_string, "servername") \
Expand Down
2 changes: 2 additions & 0 deletions test/common/internet.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const addresses = {
CNAME_HOST: 'blog.nodejs.org',
// A host with NS records registered
NS_HOST: 'nodejs.org',
// A host with TLSA records registered
TLSA_HOST: '_443._tcp.fedoraproject.org',
// A host with TXT records registered
TXT_HOST: 'nodejs.org',
// An accessible IPv4 DNS server
Expand Down
1 change: 1 addition & 0 deletions test/internet/test-dns-cares-domains.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const methods = [
'resolveCname',
'resolveMx',
'resolveNs',
'resolveTlsa',
'resolveTxt',
'resolveSrv',
'resolvePtr',
Expand Down
Loading
Loading