Skip to content

Commit

Permalink
net: add support for resolving DNS CAA records
Browse files Browse the repository at this point in the history
This adds support for DNS Certification Authority Authorization
(RFC 8659) to Node.js.

PR-URL: nodejs#35466
Fixes: nodejs#19239
Refs: nodejs#14713
Reviewed-By: Anna Henningsen <[email protected]>
  • Loading branch information
lxdicted authored and aduh95 committed Oct 16, 2020
1 parent cfbbeea commit 6f34498
Show file tree
Hide file tree
Showing 17 changed files with 421 additions and 1 deletion.
1 change: 1 addition & 0 deletions deps/cares/cares.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions deps/cares/include/ares.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions deps/cares/include/nameser.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 */
Expand Down
18 changes: 18 additions & 0 deletions deps/cares/src/ares_data.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions deps/cares/src/ares_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
};

Expand Down
209 changes: 209 additions & 0 deletions deps/cares/src/ares_parse_caa_reply.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@

/* Copyright 2020 by <[email protected]>
*
* 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;
}
35 changes: 35 additions & 0 deletions doc/api/dns.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()`]
Expand Down Expand Up @@ -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()`][] |
Expand Down Expand Up @@ -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:[email protected]'}, {critical: 128, issue: 'pki.example.com'}]`).

## `dns.resolveMx(hostname, callback)`
<!-- YAML
added: v0.1.27
Expand Down Expand Up @@ -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()`]
Expand Down Expand Up @@ -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()`][] |
Expand Down Expand Up @@ -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:[email protected]'},{critical: 128, issue:
'pki.example.com'}]`).

### `dnsPromises.resolveCname(hostname)`
<!-- YAML
added: v10.6.0
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 6f34498

Please sign in to comment.