net: add support for resolving DNS CAA records

This adds support for DNS Certification Authority Authorization
(RFC 8659) to Node.js.

PR-URL: https://github.com/nodejs/node/pull/35466
Fixes: https://github.com/nodejs/node/issues/19239
Refs: https://github.com/nodejs/node/issues/14713
Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
Danny Sonnenschein
2020-10-02 17:52:47 +02:00
committed by Antoine du Hamel
parent cfbbeea4a1
commit 6f34498148
17 changed files with 421 additions and 1 deletions

View File

@@ -75,6 +75,7 @@
'src/ares__parse_into_addrinfo.c',
'src/ares_parse_aaaa_reply.c',
'src/ares_parse_a_reply.c',
'src/ares_parse_caa_reply.c',
'src/ares_parse_mx_reply.c',
'src/ares_parse_naptr_reply.c',
'src/ares_parse_ns_reply.c',

View File

@@ -528,6 +528,15 @@ struct ares_addr6ttl {
int ttl;
};
struct ares_caa_reply {
struct ares_caa_reply *next;
int critical;
unsigned char *property;
size_t plength; /* plength excludes null termination */
unsigned char *value;
size_t length; /* length excludes null termination */
};
struct ares_srv_reply {
struct ares_srv_reply *next;
char *host;
@@ -637,6 +646,10 @@ CARES_EXTERN int ares_parse_aaaa_reply(const unsigned char *abuf,
struct ares_addr6ttl *addrttls,
int *naddrttls);
CARES_EXTERN int ares_parse_caa_reply(const unsigned char* abuf,
int alen,
struct ares_caa_reply** caa_out);
CARES_EXTERN int ares_parse_ptr_reply(const unsigned char *abuf,
int alen,
const void *addr,

View File

@@ -88,6 +88,7 @@ typedef enum __ns_type {
ns_t_maila = 254, /* Transfer mail agent records. */
ns_t_any = 255, /* Wildcard match. */
ns_t_zxfr = 256, /* BIND-specific, nonstandard. */
ns_t_caa = 257, /* CA Authorization (RFC8659) */
ns_t_max = 65536
} ns_type;
@@ -204,6 +205,7 @@ typedef enum __ns_rcode {
#define T_AXFR ns_t_axfr
#define T_MAILB ns_t_mailb
#define T_MAILA ns_t_maila
#define T_CAA ns_t_caa
#define T_ANY ns_t_any
#endif /* HAVE_ARPA_NAMESER_COMPAT_H */

View File

@@ -119,6 +119,16 @@ void ares_free_data(void *dataptr)
ares_free(ptr->data.soa_reply.hostmaster);
break;
case ARES_DATATYPE_CAA_REPLY:
if (ptr->data.caa_reply.next)
next_data = ptr->data.caa_reply.next;
if (ptr->data.caa_reply.property)
ares_free(ptr->data.caa_reply.property);
if (ptr->data.caa_reply.value)
ares_free(ptr->data.caa_reply.value);
break;
default:
return;
}
@@ -174,6 +184,14 @@ void *ares_malloc_data(ares_datatype type)
ptr->data.txt_reply.length = 0;
break;
case ARES_DATATYPE_CAA_REPLY:
ptr->data.caa_reply.next = NULL;
ptr->data.caa_reply.plength = 0;
ptr->data.caa_reply.property = NULL;
ptr->data.caa_reply.length = 0;
ptr->data.caa_reply.value = NULL;
break;
case ARES_DATATYPE_ADDR_NODE:
ptr->data.addr_node.next = NULL;
ptr->data.addr_node.family = 0;

View File

@@ -30,6 +30,7 @@ typedef enum {
ARES_DATATYPE_OPTIONS, /* struct ares_options */
#endif
ARES_DATATYPE_ADDR_PORT_NODE, /* struct ares_addr_port_node - introduced in 1.11.0 */
ARES_DATATYPE_CAA_REPLY, /* struct ares_caa_reply - introduced in 1.17 */
ARES_DATATYPE_LAST /* not used - introduced in 1.7.0 */
} ares_datatype;
@@ -65,6 +66,7 @@ struct ares_data {
struct ares_mx_reply mx_reply;
struct ares_naptr_reply naptr_reply;
struct ares_soa_reply soa_reply;
struct ares_caa_reply caa_reply;
} data;
};

209
deps/cares/src/ares_parse_caa_reply.c vendored Normal file
View File

@@ -0,0 +1,209 @@
/* Copyright 2020 by <danny.sonnenschein@platynum.ch>
*
* Permission to use, copy, modify, and distribute this
* software and its documentation for any purpose and without
* fee is hereby granted, provided that the above copyright
* notice appear in all copies and that both that copyright
* notice and this permission notice appear in supporting
* documentation, and that the name of M.I.T. not be used in
* advertising or publicity pertaining to distribution of the
* software without specific, written prior permission.
* M.I.T. makes no representations about the suitability of
* this software for any purpose. It is provided "as is"
* without express or implied warranty.
*/
#include "ares_setup.h"
#ifdef HAVE_NETINET_IN_H
# include <netinet/in.h>
#endif
#ifdef HAVE_NETDB_H
# include <netdb.h>
#endif
#ifdef HAVE_ARPA_INET_H
# include <arpa/inet.h>
#endif
#ifdef HAVE_ARPA_NAMESER_H
# include <arpa/nameser.h>
#else
# include "nameser.h"
#endif
#ifdef HAVE_ARPA_NAMESER_COMPAT_H
# include <arpa/nameser_compat.h>
#endif
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif
#include "ares.h"
#include "ares_dns.h"
#include "ares_data.h"
#include "ares_private.h"
#ifndef T_CAA
# define T_CAA 257 /* Certification Authority Authorization */
#endif
int
ares_parse_caa_reply (const unsigned char *abuf, int alen,
struct ares_caa_reply **caa_out)
{
unsigned int qdcount, ancount, i;
const unsigned char *aptr;
const unsigned char *strptr;
int status, rr_type, rr_class, rr_len;
long len;
char *hostname = NULL, *rr_name = NULL;
struct ares_caa_reply *caa_head = NULL;
struct ares_caa_reply *caa_last = NULL;
struct ares_caa_reply *caa_curr;
/* Set *caa_out to NULL for all failure cases. */
*caa_out = NULL;
/* Give up if abuf doesn't have room for a header. */
if (alen < HFIXEDSZ)
return ARES_EBADRESP;
/* Fetch the question and answer count from the header. */
qdcount = DNS_HEADER_QDCOUNT (abuf);
ancount = DNS_HEADER_ANCOUNT (abuf);
if (qdcount != 1)
return ARES_EBADRESP;
if (ancount == 0)
return ARES_ENODATA;
/* Expand the name from the question, and skip past the question. */
aptr = abuf + HFIXEDSZ;
status = ares_expand_name (aptr, abuf, alen, &hostname, &len);
if (status != ARES_SUCCESS)
return status;
if (aptr + len + QFIXEDSZ > abuf + alen)
{
ares_free (hostname);
return ARES_EBADRESP;
}
aptr += len + QFIXEDSZ;
/* Examine each answer resource record (RR) in turn. */
for (i = 0; i < ancount; i++)
{
/* Decode the RR up to the data field. */
status = ares_expand_name (aptr, abuf, alen, &rr_name, &len);
if (status != ARES_SUCCESS)
{
break;
}
aptr += len;
if (aptr + RRFIXEDSZ > abuf + alen)
{
status = ARES_EBADRESP;
break;
}
rr_type = DNS_RR_TYPE (aptr);
rr_class = DNS_RR_CLASS (aptr);
rr_len = DNS_RR_LEN (aptr);
aptr += RRFIXEDSZ;
if (aptr + rr_len > abuf + alen)
{
status = ARES_EBADRESP;
break;
}
/* Check if we are really looking at a CAA record */
if ((rr_class == C_IN || rr_class == C_CHAOS) && rr_type == T_CAA)
{
strptr = aptr;
/* Allocate storage for this CAA answer appending it to the list */
caa_curr = ares_malloc_data(ARES_DATATYPE_CAA_REPLY);
if (!caa_curr)
{
status = ARES_ENOMEM;
break;
}
if (caa_last)
{
caa_last->next = caa_curr;
}
else
{
caa_head = caa_curr;
}
caa_last = caa_curr;
if (rr_len < 2)
{
status = ARES_EBADRESP;
break;
}
caa_curr->critical = (int)*strptr++;
caa_curr->plength = (int)*strptr++;
if (caa_curr->plength <= 0 || (int)caa_curr->plength >= rr_len - 2)
{
status = ARES_EBADRESP;
break;
}
caa_curr->property = ares_malloc (caa_curr->plength + 1/* Including null byte */);
if (caa_curr->property == NULL)
{
status = ARES_ENOMEM;
break;
}
memcpy ((char *) caa_curr->property, strptr, caa_curr->plength);
/* Make sure we NULL-terminate */
caa_curr->property[caa_curr->plength] = 0;
strptr += caa_curr->plength;
caa_curr->length = rr_len - caa_curr->plength - 2;
if (caa_curr->length <= 0)
{
status = ARES_EBADRESP;
break;
}
caa_curr->value = ares_malloc (caa_curr->length + 1/* Including null byte */);
if (caa_curr->value == NULL)
{
status = ARES_ENOMEM;
break;
}
memcpy ((char *) caa_curr->value, strptr, caa_curr->length);
/* Make sure we NULL-terminate */
caa_curr->value[caa_curr->length] = 0;
}
/* Propagate any failures */
if (status != ARES_SUCCESS)
{
break;
}
/* Don't lose memory in the next iteration */
ares_free (rr_name);
rr_name = NULL;
/* Move on to the next record */
aptr += rr_len;
}
if (hostname)
ares_free (hostname);
if (rr_name)
ares_free (rr_name);
/* clean up on error */
if (status != ARES_SUCCESS)
{
if (caa_head)
ares_free_data (caa_head);
return status;
}
/* everything looks fine, return the data */
*caa_out = caa_head;
return ARES_SUCCESS;
}

View File

@@ -81,6 +81,7 @@ The following methods from the `dns` module are available:
* [`resolver.resolve4()`][`dns.resolve4()`]
* [`resolver.resolve6()`][`dns.resolve6()`]
* [`resolver.resolveAny()`][`dns.resolveAny()`]
* [`resolver.resolveCaa()`][`dns.resolveCaa()`]
* [`resolver.resolveCname()`][`dns.resolveCname()`]
* [`resolver.resolveMx()`][`dns.resolveMx()`]
* [`resolver.resolveNaptr()`][`dns.resolveNaptr()`]
@@ -289,6 +290,7 @@ records. The type and structure of individual results varies based on `rrtype`:
| `'A'` | IPv4 addresses (default) | {string} | [`dns.resolve4()`][] |
| `'AAAA'` | IPv6 addresses | {string} | [`dns.resolve6()`][] |
| `'ANY'` | any records | {Object} | [`dns.resolveAny()`][] |
| `'CAA'` | CA authorization records | {Object} | [`dns.resolveCaa()`][] |
| `'CNAME'` | canonical name records | {string} | [`dns.resolveCname()`][] |
| `'MX'` | mail exchange records | {Object} | [`dns.resolveMx()`][] |
| `'NAPTR'` | name authority pointer records | {Object} | [`dns.resolveNaptr()`][] |
@@ -414,6 +416,22 @@ Uses the DNS protocol to resolve `CNAME` records for the `hostname`. The
will contain an array of canonical name records available for the `hostname`
(e.g. `['bar.example.com']`).
## `dns.resolveCaa(hostname, callback)`
<!-- YAML
added: REPLACEME
-->
* `hostname` {string}
* `callback` {Function}
* `err` {Error}
* `records` {Object[]}
Uses the DNS protocol to resolve `CAA` records for the `hostname`. The
`addresses` argument passed to the `callback` function
will contain an array of certification authority authorization records
available for the `hostname` (e.g. `[{critial: 0, iodef:
'mailto:pki@example.com'}, {critical: 128, issue: 'pki.example.com'}]`).
## `dns.resolveMx(hostname, callback)`
<!-- YAML
added: v0.1.27
@@ -665,6 +683,7 @@ The following methods from the `dnsPromises` API are available:
* [`resolver.resolve4()`][`dnsPromises.resolve4()`]
* [`resolver.resolve6()`][`dnsPromises.resolve6()`]
* [`resolver.resolveAny()`][`dnsPromises.resolveAny()`]
* [`resolver.resolveCaa()`][`dnsPromises.resolveCaa()`]
* [`resolver.resolveCname()`][`dnsPromises.resolveCname()`]
* [`resolver.resolveMx()`][`dnsPromises.resolveMx()`]
* [`resolver.resolveNaptr()`][`dnsPromises.resolveNaptr()`]
@@ -806,6 +825,7 @@ based on `rrtype`:
| `'A'` | IPv4 addresses (default) | {string} | [`dnsPromises.resolve4()`][] |
| `'AAAA'` | IPv6 addresses | {string} | [`dnsPromises.resolve6()`][] |
| `'ANY'` | any records | {Object} | [`dnsPromises.resolveAny()`][] |
| `'CAA'` | CA authorization records | {Object} | [`dnsPromises.resolveCaa()`][] |
| `'CNAME'` | canonical name records | {string} | [`dnsPromises.resolveCname()`][] |
| `'MX'` | mail exchange records | {Object} | [`dnsPromises.resolveMx()`][] |
| `'NAPTR'` | name authority pointer records | {Object} | [`dnsPromises.resolveNaptr()`][] |
@@ -895,6 +915,19 @@ Here is an example of the result object:
minttl: 60 } ]
```
## `dnsPromises.resolveCaa(hostname)`
<!-- YAML
added: REPLACEME
-->
* `hostname` {string}
Uses the DNS protocol to resolve `CAA` records for the `hostname`. On success,
the `Promise` is resolved with an array of objects containing available
certification authority authorization records available for the `hostname`
(e.g. `[{critial: 0, iodef: 'mailto:pki@example.com'},{critical: 128, issue:
'pki.example.com'}]`).
### `dnsPromises.resolveCname(hostname)`
<!-- YAML
added: v10.6.0
@@ -1174,6 +1207,7 @@ uses. For instance, _they do not use the configuration from `/etc/hosts`_.
[`dns.resolve4()`]: #dns_dns_resolve4_hostname_options_callback
[`dns.resolve6()`]: #dns_dns_resolve6_hostname_options_callback
[`dns.resolveAny()`]: #dns_dns_resolveany_hostname_callback
[`dns.resolveCaa()`]: #dns_dns_resolvecaa_hostname_callback
[`dns.resolveCname()`]: #dns_dns_resolvecname_hostname_callback
[`dns.resolveMx()`]: #dns_dns_resolvemx_hostname_callback
[`dns.resolveNaptr()`]: #dns_dns_resolvenaptr_hostname_callback
@@ -1190,6 +1224,7 @@ uses. For instance, _they do not use the configuration from `/etc/hosts`_.
[`dnsPromises.resolve4()`]: #dns_dnspromises_resolve4_hostname_options
[`dnsPromises.resolve6()`]: #dns_dnspromises_resolve6_hostname_options
[`dnsPromises.resolveAny()`]: #dns_dnspromises_resolveany_hostname
[`dnsPromises.resolveCaa()`]: #dns_dnspromises_resolvecaa_hostname
[`dnsPromises.resolveCname()`]: #dns_dnspromises_resolvecname_hostname
[`dnsPromises.resolveMx()`]: #dns_dnspromises_resolvemx_hostname
[`dnsPromises.resolveNaptr()`]: #dns_dnspromises_resolvenaptr_hostname

View File

@@ -236,6 +236,7 @@ const resolveMap = ObjectCreate(null);
Resolver.prototype.resolveAny = resolveMap.ANY = resolver('queryAny');
Resolver.prototype.resolve4 = resolveMap.A = resolver('queryA');
Resolver.prototype.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
Resolver.prototype.resolveCaa = resolveMap.CAA = resolver('queryCaa');
Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname');
Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx');
Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs');

View File

@@ -220,6 +220,7 @@ Resolver.prototype.setServers = CallbackResolver.prototype.setServers;
Resolver.prototype.resolveAny = resolveMap.ANY = resolver('queryAny');
Resolver.prototype.resolve4 = resolveMap.A = resolver('queryA');
Resolver.prototype.resolve6 = resolveMap.AAAA = resolver('queryAaaa');
Resolver.prototype.resolveCaa = resolveMap.CAA = resolver('queryCaa');
Resolver.prototype.resolveCname = resolveMap.CNAME = resolver('queryCname');
Resolver.prototype.resolveMx = resolveMap.MX = resolver('queryMx');
Resolver.prototype.resolveNs = resolveMap.NS = resolver('queryNs');

View File

@@ -123,6 +123,7 @@ const resolverKeys = [
'resolve4',
'resolve6',
'resolveAny',
'resolveCaa',
'resolveCname',
'resolveMx',
'resolveNaptr',

View File

@@ -50,6 +50,10 @@
# include <arpa/nameser.h>
#endif
#ifndef T_CAA
# define T_CAA 257 /* Certification Authority Authorization */
#endif
#if defined(__OpenBSD__)
# define AI_V4MAPPED 0
#endif
@@ -882,6 +886,43 @@ int ParseMxReply(Environment* env,
return ARES_SUCCESS;
}
int ParseCaaReply(Environment* env,
const unsigned char* buf,
int len,
Local<Array> ret,
bool need_type = false) {
HandleScope handle_scope(env->isolate());
auto context = env->context();
struct ares_caa_reply* caa_start;
int status = ares_parse_caa_reply(buf, len, &caa_start);
if (status != ARES_SUCCESS) {
return status;
}
uint32_t offset = ret->Length();
ares_caa_reply* current = caa_start;
for (uint32_t i = 0; current != nullptr; ++i, current = current->next) {
Local<Object> caa_record = Object::New(env->isolate());
caa_record->Set(context,
env->dns_critical_string(),
Integer::New(env->isolate(), current->critical)).Check();
caa_record->Set(context,
OneByteString(env->isolate(), current->property),
OneByteString(env->isolate(), current->value)).Check();
if (need_type)
caa_record->Set(context,
env->type_string(),
env->dns_caa_string()).Check();
ret->Set(context, i + offset, caa_record).Check();
}
ares_free_data(caa_start);
return ARES_SUCCESS;
}
int ParseTxtReply(Environment* env,
const unsigned char* buf,
int len,
@@ -1345,6 +1386,13 @@ class QueryAnyWrap: public QueryWrap {
if (!soa_record.IsEmpty())
ret->Set(context, ret->Length(), soa_record).Check();
/* Parse CAA records */
status = ParseCaaReply(env(), buf, len, ret, true);
if (status != ARES_SUCCESS && status != ARES_ENODATA) {
ParseError(status);
return;
}
CallOnComplete(ret);
}
};
@@ -1441,6 +1489,36 @@ class QueryAaaaWrap: public QueryWrap {
}
};
class QueryCaaWrap: public QueryWrap {
public:
QueryCaaWrap(ChannelWrap* channel, Local<Object> req_wrap_obj)
: QueryWrap(channel, req_wrap_obj, "resolve6") {
}
int Send(const char* name) override {
AresQuery(name, ns_c_in, T_CAA);
return 0;
}
SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(QueryAaaaWrap)
SET_SELF_SIZE(QueryAaaaWrap)
protected:
void Parse(unsigned char* buf, int len) override {
HandleScope handle_scope(env()->isolate());
Context::Scope context_scope(env()->context());
Local<Array> ret = Array::New(env()->isolate());
int status = ParseCaaReply(env(), buf, len, ret);
if (status != ARES_SUCCESS) {
ParseError(status);
return;
}
this->CallOnComplete(ret);
}
};
class QueryCnameWrap: public QueryWrap {
public:
@@ -2238,6 +2316,7 @@ void Initialize(Local<Object> target,
env->SetProtoMethod(channel_wrap, "queryAny", Query<QueryAnyWrap>);
env->SetProtoMethod(channel_wrap, "queryA", Query<QueryAWrap>);
env->SetProtoMethod(channel_wrap, "queryAaaa", Query<QueryAaaaWrap>);
env->SetProtoMethod(channel_wrap, "queryCaa", Query<QueryCaaWrap>);
env->SetProtoMethod(channel_wrap, "queryCname", Query<QueryCnameWrap>);
env->SetProtoMethod(channel_wrap, "queryMx", Query<QueryMxWrap>);
env->SetProtoMethod(channel_wrap, "queryNs", Query<QueryNsWrap>);

View File

@@ -218,6 +218,8 @@ constexpr size_t kFsStatsBufferLength =
V(divisor_length_string, "divisorLength") \
V(dns_a_string, "A") \
V(dns_aaaa_string, "AAAA") \
V(dns_caa_string, "CAA") \
V(dns_critical_string, "critical") \
V(dns_cname_string, "CNAME") \
V(dns_mx_string, "MX") \
V(dns_naptr_string, "NAPTR") \

View File

@@ -13,7 +13,8 @@ const types = {
PTR: 12,
MX: 15,
TXT: 16,
ANY: 255
ANY: 255,
CAA: 257
};
const classes = {
@@ -269,6 +270,14 @@ function writeDNSPacket(parsed) {
]));
break;
}
case 'CAA':
{
rdLengthBuf[0] = 5 + rr.issue.length + 2;
buffers.push(Buffer.from([Number(rr.critical)]));
buffers.push(Buffer.from([Number(5)]));
buffers.push(Buffer.from('issue' + rr.issue));
break;
}
default:
throw new Error(`Unknown RR type ${rr.type}`);
}

View File

@@ -30,6 +30,8 @@ const addresses = {
NAPTR_HOST: 'sip2sip.info',
// A host with SOA records registered
SOA_HOST: 'nodejs.org',
// A host with CAA record registred
CAA_HOST: 'google.com',
// A host with CNAME records registered
CNAME_HOST: 'blog.nodejs.org',
// A host with NS records registered

View File

@@ -399,6 +399,45 @@ TEST(function test_resolveSoa_failure(done) {
checkWrap(req);
});
TEST(async function test_resolveCaa(done) {
function validateResult(result) {
assert.ok(Array.isArray(result[0]));
assert.strictEqual(result.length, 1);
assert.strictEqual(typeof result[0].critical, 'number');
assert.strictEqual(result[0].critical, 0);
assert.strictEqual(result[0].issue, 'pki.goog');
}
validateResult(await dnsPromises.resolveCaa(addresses.CAA_HOST));
const req = dns.resolveCaa(addresses.CAA_HOST, function(err, records) {
assert.ifError(err);
validateResult(records);
done();
});
checkWrap(req);
});
TEST(function test_resolveCaa_failure(done) {
dnsPromises.resolveTxt(addresses.INVALID_HOST)
.then(common.mustNotCall())
.catch(common.mustCall((err) => {
assert.strictEqual(err.code, 'ENOTFOUND');
}));
const req = dns.resolveCaa(addresses.INVALID_HOST, function(err, result) {
assert.ok(err instanceof Error);
assert.strictEqual(err.code, 'ENOTFOUND');
assert.strictEqual(result, undefined);
done();
});
checkWrap(req);
});
TEST(async function test_resolveCname(done) {
function validateResult(result) {
assert.ok(result.length > 0);

View File

@@ -29,6 +29,7 @@ const tests = {
'resolveAny': 'dns.resolveAny("example.com", (err, res) => {});',
'resolve4': 'dns.resolve4("example.com", (err, res) => {});',
'resolve6': 'dns.resolve6("example.com", (err, res) => {});',
'resolveCaa': 'dns.resolveCaa("example.com", (err, res) => {});',
'resolveCname': 'dns.resolveCname("example.com", (err, res) => {});',
'resolveMx': 'dns.resolveMx("example.com", (err, res) => {});',
'resolveNs': 'dns.resolveNs("example.com", (err, res) => {});',

View File

@@ -23,6 +23,11 @@ const answers = [
expire: 1800,
minttl: 60
},
{
type: 'CAA',
critical: 128,
issue: 'platynum.ch'
}
];
const server = dgram.createSocket('udp4');