Skip to content

Commit 1150546

Browse files
rithvikvibhuaduh95
authored andcommitted
dns: add TLSA record query and parsing
PR-URL: #52983 Refs: #39569 Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
1 parent 4f36fb9 commit 1150546

File tree

9 files changed

+226
-0
lines changed

9 files changed

+226
-0
lines changed

Diff for: doc/api/dns.md

+71
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ The following methods from the `node:dns` module are available:
129129
* [`resolver.resolvePtr()`][`dns.resolvePtr()`]
130130
* [`resolver.resolveSoa()`][`dns.resolveSoa()`]
131131
* [`resolver.resolveSrv()`][`dns.resolveSrv()`]
132+
* [`resolver.resolveTlsa()`][`dns.resolveTlsa()`]
132133
* [`resolver.resolveTxt()`][`dns.resolveTxt()`]
133134
* [`resolver.reverse()`][`dns.reverse()`]
134135
* [`resolver.setServers()`][`dns.setServers()`]
@@ -442,6 +443,7 @@ records. The type and structure of individual results varies based on `rrtype`:
442443
| `'PTR'` | pointer records | {string} | [`dns.resolvePtr()`][] |
443444
| `'SOA'` | start of authority records | {Object} | [`dns.resolveSoa()`][] |
444445
| `'SRV'` | service records | {Object} | [`dns.resolveSrv()`][] |
446+
| `'TLSA'` | certificate associations | {Object} | [`dns.resolveTlsa()`][] |
445447
| `'TXT'` | text records | {string\[]} | [`dns.resolveTxt()`][] |
446448

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

546549
Here is an example of the `ret` object passed to the callback:
@@ -800,6 +803,41 @@ be an array of objects with the following properties:
800803
}
801804
```
802805

806+
## `dns.resolveTlsa(hostname, callback)`
807+
808+
<!-- YAML
809+
added: REPLACEME
810+
-->
811+
812+
<!--lint disable no-undefined-references list-item-bullet-indent-->
813+
814+
* `hostname` {string}
815+
* `callback` {Function}
816+
* `err` {Error}
817+
* `records` {Object\[]}
818+
819+
<!--lint enable no-undefined-references list-item-bullet-indent-->
820+
821+
Uses the DNS protocol to resolve certificate associations (`TLSA` records) for
822+
the `hostname`. The `records` argument passed to the `callback` function is an
823+
array of objects with these properties:
824+
825+
* `certUsage`
826+
* `selector`
827+
* `match`
828+
* `data`
829+
830+
<!-- eslint-skip -->
831+
832+
```js
833+
{
834+
certUsage: 3,
835+
selector: 1,
836+
match: 1,
837+
data: [ArrayBuffer]
838+
}
839+
```
840+
803841
## `dns.resolveTxt(hostname, callback)`
804842

805843
<!-- YAML
@@ -1002,6 +1040,7 @@ The following methods from the `dnsPromises` API are available:
10021040
* [`resolver.resolvePtr()`][`dnsPromises.resolvePtr()`]
10031041
* [`resolver.resolveSoa()`][`dnsPromises.resolveSoa()`]
10041042
* [`resolver.resolveSrv()`][`dnsPromises.resolveSrv()`]
1043+
* [`resolver.resolveTlsa()`][`dnsPromises.resolveTlsa()`]
10051044
* [`resolver.resolveTxt()`][`dnsPromises.resolveTxt()`]
10061045
* [`resolver.reverse()`][`dnsPromises.reverse()`]
10071046
* [`resolver.setServers()`][`dnsPromises.setServers()`]
@@ -1204,6 +1243,7 @@ based on `rrtype`:
12041243
| `'PTR'` | pointer records | {string} | [`dnsPromises.resolvePtr()`][] |
12051244
| `'SOA'` | start of authority records | {Object} | [`dnsPromises.resolveSoa()`][] |
12061245
| `'SRV'` | service records | {Object} | [`dnsPromises.resolveSrv()`][] |
1246+
| `'TLSA'` | certificate associations | {Object} | [`dnsPromises.resolveTlsa()`][] |
12071247
| `'TXT'` | text records | {string\[]} | [`dnsPromises.resolveTxt()`][] |
12081248

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

12731314
Here is an example of the result object:
@@ -1450,6 +1491,34 @@ the following properties:
14501491
}
14511492
```
14521493

1494+
### `dnsPromises.resolveTlsa(hostname)`
1495+
1496+
<!-- YAML
1497+
added: REPLACEME
1498+
-->
1499+
1500+
* `hostname` {string}
1501+
1502+
Uses the DNS protocol to resolve certificate associations (`TLSA` records) for
1503+
the `hostname`. On success, the `Promise` is resolved with an array of objects
1504+
with these properties:
1505+
1506+
* `certUsage`
1507+
* `selector`
1508+
* `match`
1509+
* `data`
1510+
1511+
<!-- eslint-skip -->
1512+
1513+
```js
1514+
{
1515+
certUsage: 3,
1516+
selector: 1,
1517+
match: 1,
1518+
data: [ArrayBuffer]
1519+
}
1520+
```
1521+
14531522
### `dnsPromises.resolveTxt(hostname)`
14541523

14551524
<!-- YAML
@@ -1648,6 +1717,7 @@ uses. For instance, they do not use the configuration from `/etc/hosts`.
16481717
[`dns.resolvePtr()`]: #dnsresolveptrhostname-callback
16491718
[`dns.resolveSoa()`]: #dnsresolvesoahostname-callback
16501719
[`dns.resolveSrv()`]: #dnsresolvesrvhostname-callback
1720+
[`dns.resolveTlsa()`]: #dnsresolvetlsahostname-callback
16511721
[`dns.resolveTxt()`]: #dnsresolvetxthostname-callback
16521722
[`dns.reverse()`]: #dnsreverseip-callback
16531723
[`dns.setDefaultResultOrder()`]: #dnssetdefaultresultorderorder
@@ -1666,6 +1736,7 @@ uses. For instance, they do not use the configuration from `/etc/hosts`.
16661736
[`dnsPromises.resolvePtr()`]: #dnspromisesresolveptrhostname
16671737
[`dnsPromises.resolveSoa()`]: #dnspromisesresolvesoahostname
16681738
[`dnsPromises.resolveSrv()`]: #dnspromisesresolvesrvhostname
1739+
[`dnsPromises.resolveTlsa()`]: #dnspromisesresolvetlsahostname
16691740
[`dnsPromises.resolveTxt()`]: #dnspromisesresolvetxthostname
16701741
[`dnsPromises.reverse()`]: #dnspromisesreverseip
16711742
[`dnsPromises.setDefaultResultOrder()`]: #dnspromisessetdefaultresultorderorder

Diff for: lib/internal/dns/utils.js

+2
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ const resolverKeys = [
235235
'resolvePtr',
236236
'resolveSoa',
237237
'resolveSrv',
238+
'resolveTlsa',
238239
'resolveTxt',
239240
'reverse',
240241
];
@@ -300,6 +301,7 @@ function createResolverClass(resolver) {
300301
Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname');
301302
Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx');
302303
Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs');
304+
Resolver.prototype.resolveTlsa = resolveMap.TLSA = resolver('queryTlsa');
303305
Resolver.prototype.resolveTxt = resolveMap.TXT = resolver('queryTxt');
304306
Resolver.prototype.resolveSrv = resolveMap.SRV = resolver('querySrv');
305307
Resolver.prototype.resolvePtr = resolveMap.PTR = resolver('queryPtr');

Diff for: src/cares_wrap.cc

+96
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
#include <vector>
4141
#include <unordered_set>
4242

43+
#ifndef T_TLSA
44+
#define T_TLSA 52 /* TLSA certificate association */
45+
#endif
46+
4347
#ifndef T_CAA
4448
# define T_CAA 257 /* Certification Authority Authorization */
4549
#endif
@@ -57,6 +61,7 @@ namespace node {
5761
namespace cares_wrap {
5862

5963
using v8::Array;
64+
using v8::ArrayBuffer;
6065
using v8::Context;
6166
using v8::EscapableHandleScope;
6267
using v8::Exception;
@@ -352,6 +357,65 @@ int ParseCaaReply(
352357
return ARES_SUCCESS;
353358
}
354359

360+
int ParseTlsaReply(Environment* env,
361+
unsigned char* buf,
362+
int len,
363+
Local<Array> ret) {
364+
EscapableHandleScope handle_scope(env->isolate());
365+
366+
ares_dns_record_t* dnsrec = nullptr;
367+
368+
int status = ares_dns_parse(buf, len, 0, &dnsrec);
369+
if (status != ARES_SUCCESS) {
370+
ares_dns_record_destroy(dnsrec);
371+
return status;
372+
}
373+
374+
uint32_t offset = ret->Length();
375+
size_t rr_count = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ANSWER);
376+
377+
for (size_t i = 0; i < rr_count; i++) {
378+
const ares_dns_rr_t* rr =
379+
ares_dns_record_rr_get(dnsrec, ARES_SECTION_ANSWER, i);
380+
381+
if (ares_dns_rr_get_type(rr) != ARES_REC_TYPE_TLSA) continue;
382+
383+
unsigned char certusage = ares_dns_rr_get_u8(rr, ARES_RR_TLSA_CERT_USAGE);
384+
unsigned char selector = ares_dns_rr_get_u8(rr, ARES_RR_TLSA_SELECTOR);
385+
unsigned char match = ares_dns_rr_get_u8(rr, ARES_RR_TLSA_MATCH);
386+
size_t data_len;
387+
const unsigned char* data =
388+
ares_dns_rr_get_bin(rr, ARES_RR_TLSA_DATA, &data_len);
389+
if (!data || data_len == 0) continue;
390+
391+
Local<ArrayBuffer> data_ab = ArrayBuffer::New(env->isolate(), data_len);
392+
memcpy(data_ab->Data(), data, data_len);
393+
394+
Local<Object> tlsa_rec = Object::New(env->isolate());
395+
tlsa_rec
396+
->Set(env->context(),
397+
env->cert_usage_string(),
398+
Integer::NewFromUnsigned(env->isolate(), certusage))
399+
.Check();
400+
tlsa_rec
401+
->Set(env->context(),
402+
env->selector_string(),
403+
Integer::NewFromUnsigned(env->isolate(), selector))
404+
.Check();
405+
tlsa_rec
406+
->Set(env->context(),
407+
env->match_string(),
408+
Integer::NewFromUnsigned(env->isolate(), match))
409+
.Check();
410+
tlsa_rec->Set(env->context(), env->data_string(), data_ab).Check();
411+
412+
ret->Set(env->context(), offset + i, tlsa_rec).Check();
413+
}
414+
415+
ares_dns_record_destroy(dnsrec);
416+
return ARES_SUCCESS;
417+
}
418+
355419
int ParseTxtReply(
356420
Environment* env,
357421
const unsigned char* buf,
@@ -861,6 +925,11 @@ int NsTraits::Send(QueryWrap<NsTraits>* wrap, const char* name) {
861925
return ARES_SUCCESS;
862926
}
863927

928+
int TlsaTraits::Send(QueryWrap<TlsaTraits>* wrap, const char* name) {
929+
wrap->AresQuery(name, ARES_CLASS_IN, ARES_REC_TYPE_TLSA);
930+
return ARES_SUCCESS;
931+
}
932+
864933
int TxtTraits::Send(QueryWrap<TxtTraits>* wrap, const char* name) {
865934
wrap->AresQuery(name, ARES_CLASS_IN, ARES_REC_TYPE_TXT);
866935
return ARES_SUCCESS;
@@ -1045,6 +1114,10 @@ int AnyTraits::Parse(
10451114
if (!soa_record.IsEmpty())
10461115
ret->Set(env->context(), ret->Length(), soa_record).Check();
10471116

1117+
/* Parse TLSA records */
1118+
status = ParseTlsaReply(env, buf, len, ret);
1119+
if (status != ARES_SUCCESS && status != ARES_ENODATA) return status;
1120+
10481121
/* Parse CAA records */
10491122
status = ParseCaaReply(env, buf, len, ret, true);
10501123
if (status != ARES_SUCCESS && status != ARES_ENODATA)
@@ -1219,6 +1292,27 @@ int NsTraits::Parse(
12191292
return ARES_SUCCESS;
12201293
}
12211294

1295+
int TlsaTraits::Parse(QueryTlsaWrap* wrap,
1296+
const std::unique_ptr<ResponseData>& response) {
1297+
if (response->is_host) [[unlikely]] {
1298+
return ARES_EBADRESP;
1299+
}
1300+
1301+
unsigned char* buf = response->buf.data;
1302+
int len = response->buf.size;
1303+
1304+
Environment* env = wrap->env();
1305+
HandleScope handle_scope(env->isolate());
1306+
Context::Scope context_scope(env->context());
1307+
1308+
Local<Array> tlsa_records = Array::New(env->isolate());
1309+
int status = ParseTlsaReply(env, buf, len, tlsa_records);
1310+
if (status != ARES_SUCCESS) return status;
1311+
1312+
wrap->CallOnComplete(tlsa_records);
1313+
return ARES_SUCCESS;
1314+
}
1315+
12221316
int TxtTraits::Parse(
12231317
QueryTxtWrap* wrap,
12241318
const std::unique_ptr<ResponseData>& response) {
@@ -1998,6 +2092,7 @@ void Initialize(Local<Object> target,
19982092
SetProtoMethod(isolate, channel_wrap, "queryCname", Query<QueryCnameWrap>);
19992093
SetProtoMethod(isolate, channel_wrap, "queryMx", Query<QueryMxWrap>);
20002094
SetProtoMethod(isolate, channel_wrap, "queryNs", Query<QueryNsWrap>);
2095+
SetProtoMethod(isolate, channel_wrap, "queryTlsa", Query<QueryTlsaWrap>);
20012096
SetProtoMethod(isolate, channel_wrap, "queryTxt", Query<QueryTxtWrap>);
20022097
SetProtoMethod(isolate, channel_wrap, "querySrv", Query<QuerySrvWrap>);
20032098
SetProtoMethod(isolate, channel_wrap, "queryPtr", Query<QueryPtrWrap>);
@@ -2029,6 +2124,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
20292124
registry->Register(Query<QueryCnameWrap>);
20302125
registry->Register(Query<QueryMxWrap>);
20312126
registry->Register(Query<QueryNsWrap>);
2127+
registry->Register(Query<QueryTlsaWrap>);
20322128
registry->Register(Query<QueryTxtWrap>);
20332129
registry->Register(Query<QuerySrvWrap>);
20342130
registry->Register(Query<QueryPtrWrap>);

Diff for: src/cares_wrap.h

+8
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,13 @@ struct NsTraits final {
460460
const std::unique_ptr<ResponseData>& response);
461461
};
462462

463+
struct TlsaTraits final {
464+
static constexpr const char* name = "resolveTlsa";
465+
static int Send(QueryWrap<TlsaTraits>* wrap, const char* name);
466+
static int Parse(QueryWrap<TlsaTraits>* wrap,
467+
const std::unique_ptr<ResponseData>& response);
468+
};
469+
463470
struct TxtTraits final {
464471
static constexpr const char* name = "resolveTxt";
465472
static int Send(QueryWrap<TxtTraits>* wrap, const char* name);
@@ -515,6 +522,7 @@ using QueryCaaWrap = QueryWrap<CaaTraits>;
515522
using QueryCnameWrap = QueryWrap<CnameTraits>;
516523
using QueryMxWrap = QueryWrap<MxTraits>;
517524
using QueryNsWrap = QueryWrap<NsTraits>;
525+
using QueryTlsaWrap = QueryWrap<TlsaTraits>;
518526
using QueryTxtWrap = QueryWrap<TxtTraits>;
519527
using QuerySrvWrap = QueryWrap<SrvTraits>;
520528
using QueryPtrWrap = QueryWrap<PtrTraits>;

Diff for: src/env_properties.h

+4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
V(cached_data_rejected_string, "cachedDataRejected") \
9191
V(cached_data_string, "cachedData") \
9292
V(cache_key_string, "cacheKey") \
93+
V(cert_usage_string, "certUsage") \
9394
V(change_string, "change") \
9495
V(changes_string, "changes") \
9596
V(channel_string, "channel") \
@@ -134,6 +135,7 @@
134135
V(dns_ptr_string, "PTR") \
135136
V(dns_soa_string, "SOA") \
136137
V(dns_srv_string, "SRV") \
138+
V(dns_tlsa_string, "TLSA") \
137139
V(dns_txt_string, "TXT") \
138140
V(done_string, "done") \
139141
V(duration_string, "duration") \
@@ -237,6 +239,7 @@
237239
V(line_number_string, "lineNumber") \
238240
V(loop_count, "loopCount") \
239241
V(mac_string, "mac") \
242+
V(match_string, "match") \
240243
V(max_buffer_string, "maxBuffer") \
241244
V(max_concurrent_streams_string, "maxConcurrentStreams") \
242245
V(message_port_constructor_string, "MessagePort") \
@@ -334,6 +337,7 @@
334337
V(script_id_string, "scriptId") \
335338
V(script_name_string, "scriptName") \
336339
V(search_string, "search") \
340+
V(selector_string, "selector") \
337341
V(serial_number_string, "serialNumber") \
338342
V(serial_string, "serial") \
339343
V(servername_string, "servername") \

Diff for: test/common/internet.js

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const addresses = {
3838
CNAME_HOST: 'blog.nodejs.org',
3939
// A host with NS records registered
4040
NS_HOST: 'nodejs.org',
41+
// A host with TLSA records registered
42+
TLSA_HOST: '_443._tcp.fedoraproject.org',
4143
// A host with TXT records registered
4244
TXT_HOST: 'nodejs.org',
4345
// An accessible IPv4 DNS server

Diff for: test/internet/test-dns-cares-domains.js

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const methods = [
1111
'resolveCname',
1212
'resolveMx',
1313
'resolveNs',
14+
'resolveTlsa',
1415
'resolveTxt',
1516
'resolveSrv',
1617
'resolvePtr',

0 commit comments

Comments
 (0)