From 6f34498148612e8aa3537a9c865d2940cb9e8c7a Mon Sep 17 00:00:00 2001 From: Danny Sonnenschein Date: Fri, 2 Oct 2020 17:52:47 +0200 Subject: [PATCH] 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 --- deps/cares/cares.gyp | 1 + deps/cares/include/ares.h | 13 ++ deps/cares/include/nameser.h | 2 + deps/cares/src/ares_data.c | 18 +++ deps/cares/src/ares_data.h | 2 + deps/cares/src/ares_parse_caa_reply.c | 209 +++++++++++++++++++++++++ doc/api/dns.md | 35 +++++ lib/dns.js | 1 + lib/internal/dns/promises.js | 1 + lib/internal/dns/utils.js | 1 + src/cares_wrap.cc | 79 ++++++++++ src/env.h | 2 + test/common/dns.js | 11 +- test/common/internet.js | 2 + test/internet/test-dns.js | 39 +++++ test/internet/test-trace-events-dns.js | 1 + test/parallel/test-dns-resolveany.js | 5 + 17 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 deps/cares/src/ares_parse_caa_reply.c diff --git a/deps/cares/cares.gyp b/deps/cares/cares.gyp index 02b2774c02683e..0dc4ff9317745c 100644 --- a/deps/cares/cares.gyp +++ b/deps/cares/cares.gyp @@ -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', diff --git a/deps/cares/include/ares.h b/deps/cares/include/ares.h index 91dc754dfb6a18..70a1baf7d71738 100644 --- a/deps/cares/include/ares.h +++ b/deps/cares/include/ares.h @@ -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, diff --git a/deps/cares/include/nameser.h b/deps/cares/include/nameser.h index a0302fd39894e6..5de783b47171be 100644 --- a/deps/cares/include/nameser.h +++ b/deps/cares/include/nameser.h @@ -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 */ diff --git a/deps/cares/src/ares_data.c b/deps/cares/src/ares_data.c index 18dd650c7b91fb..aa925b8b511b2e 100644 --- a/deps/cares/src/ares_data.c +++ b/deps/cares/src/ares_data.c @@ -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; diff --git a/deps/cares/src/ares_data.h b/deps/cares/src/ares_data.h index ffee2be6baab8a..b0182fd6907499 100644 --- a/deps/cares/src/ares_data.h +++ b/deps/cares/src/ares_data.h @@ -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; }; diff --git a/deps/cares/src/ares_parse_caa_reply.c b/deps/cares/src/ares_parse_caa_reply.c new file mode 100644 index 00000000000000..759ab8f7d123e6 --- /dev/null +++ b/deps/cares/src/ares_parse_caa_reply.c @@ -0,0 +1,209 @@ + +/* Copyright 2020 by + * + * 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 +#endif +#ifdef HAVE_NETDB_H +# include +#endif +#ifdef HAVE_ARPA_INET_H +# include +#endif +#ifdef HAVE_ARPA_NAMESER_H +# include +#else +# include "nameser.h" +#endif +#ifdef HAVE_ARPA_NAMESER_COMPAT_H +# include +#endif + +#ifdef HAVE_STRINGS_H +# include +#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; +} diff --git a/doc/api/dns.md b/doc/api/dns.md index 62eee6f90b05ec..875655fe629cfa 100644 --- a/doc/api/dns.md +++ b/doc/api/dns.md @@ -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)` + + +* `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)` + +* `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)`