freeswitch/libs/ldns/examples/ldns-verify-zone.c

714 lines
18 KiB
C

/*
* read a zone file from disk and prints it, one RR per line
*
* (c) NLnetLabs 2008
*
* See the file LICENSE for the license
*
* Missing from the checks: empty non-terminals
*/
#include "config.h"
#include <unistd.h>
#include <stdlib.h>
#include <ldns/ldns.h>
#include <errno.h>
#ifdef HAVE_SSL
#include <openssl/err.h>
int verbosity = 3;
/* returns 1 if the list is empty, or if there are only ns rrs in the
* list, 0 otherwise */
static int
only_ns_in_rrsets(ldns_dnssec_rrsets *rrsets) {
ldns_dnssec_rrsets *cur_rrset = rrsets;
while (cur_rrset) {
if (cur_rrset->type != LDNS_RR_TYPE_NS) {
return 0;
}
cur_rrset = cur_rrset->next;
}
return 1;
}
static int
zone_is_nsec3_optout(ldns_rbtree_t *zone_nodes)
{
/* simply find the first NSEC3 RR and check its flags */
/* TODO: maybe create a general function that uses the active
* NSEC3PARAM RR? */
ldns_rbnode_t *cur_node;
ldns_dnssec_name *cur_name;
cur_node = ldns_rbtree_first(zone_nodes);
while (cur_node != LDNS_RBTREE_NULL) {
cur_name = (ldns_dnssec_name *) cur_node->data;
if (cur_name && cur_name->nsec &&
ldns_rr_get_type(cur_name->nsec) == LDNS_RR_TYPE_NSEC3) {
if (ldns_nsec3_optout(cur_name->nsec)) {
return 1;
} else {
return 0;
}
}
cur_node = ldns_rbtree_next(cur_node);
}
return 0;
}
static bool
ldns_rr_list_contains_name(const ldns_rr_list *rr_list,
const ldns_rdf *name)
{
size_t i;
for (i = 0; i < ldns_rr_list_rr_count(rr_list); i++) {
if (ldns_dname_compare(name,
ldns_rr_owner(ldns_rr_list_rr(rr_list, i))) == 0) {
return true;
}
}
return false;
}
static void
print_type(ldns_rr_type type)
{
const ldns_rr_descriptor *descriptor;
descriptor = ldns_rr_descript(type);
if (descriptor && descriptor->_name) {
fprintf(stdout, "%s", descriptor->_name);
} else {
fprintf(stdout, "TYPE%u",
type);
}
}
static ldns_dnssec_zone *
create_dnssec_zone(ldns_zone *orig_zone)
{
size_t i;
ldns_dnssec_zone *dnssec_zone;
ldns_rr *cur_rr;
ldns_status status;
/* when reading NSEC3s, there is a chance that we encounter nsecs
for empty nonterminals, whose nonterminals we cannot derive yet
because the needed information is to be read later. in that case
we keep a list of those nsec3's and retry to add them later */
ldns_rr_list *failed_nsec3s = ldns_rr_list_new();
dnssec_zone = ldns_dnssec_zone_new();
if (ldns_dnssec_zone_add_rr(dnssec_zone, ldns_zone_soa(orig_zone)) !=
LDNS_STATUS_OK) {
if (verbosity > 0) {
fprintf(stderr,
"Error adding SOA to dnssec zone, skipping record\n");
}
}
for (i = 0; i < ldns_rr_list_rr_count(ldns_zone_rrs(orig_zone)); i++) {
cur_rr = ldns_rr_list_rr(ldns_zone_rrs(orig_zone), i);
status = ldns_dnssec_zone_add_rr(dnssec_zone, cur_rr);
if (status != LDNS_STATUS_OK) {
if (status == LDNS_STATUS_DNSSEC_NSEC3_ORIGINAL_NOT_FOUND) {
ldns_rr_list_push_rr(failed_nsec3s, cur_rr);
} else {
if (verbosity > 0) {
fprintf(stderr, "Error adding RR to dnssec zone");
fprintf(stderr, ", skipping record:\n");
ldns_rr_print(stderr, cur_rr);
}
}
}
}
if (ldns_rr_list_rr_count(failed_nsec3s) > 0) {
(void) ldns_dnssec_zone_add_empty_nonterminals(dnssec_zone);
for (i = 0; i < ldns_rr_list_rr_count(failed_nsec3s); i++) {
cur_rr = ldns_rr_list_rr(failed_nsec3s, i);
status = ldns_dnssec_zone_add_rr(dnssec_zone, cur_rr);
}
}
ldns_rr_list_free(failed_nsec3s);
return dnssec_zone;
}
static ldns_status
verify_dnssec_rrset(ldns_rdf *zone_name,
ldns_rdf *name,
ldns_dnssec_rrsets *rrset,
ldns_rr_list *keys,
ldns_rr_list *glue_rrs)
{
ldns_rr_list *rrset_rrs;
ldns_dnssec_rrs *cur_rr, *cur_sig;
ldns_status status;
ldns_rr_list *good_keys;
ldns_status result = LDNS_STATUS_OK;
if (!rrset->rrs) return LDNS_STATUS_OK;
rrset_rrs = ldns_rr_list_new();
cur_rr = rrset->rrs;
while(cur_rr && cur_rr->rr) {
ldns_rr_list_push_rr(rrset_rrs, cur_rr->rr);
cur_rr = cur_rr->next;
}
cur_sig = rrset->signatures;
if (cur_sig) {
while (cur_sig) {
good_keys = ldns_rr_list_new();
status = ldns_verify_rrsig_keylist(rrset_rrs,
cur_sig->rr,
keys,
good_keys);
if (status != LDNS_STATUS_OK) {
if (verbosity > 0) {
printf("Error: %s",
ldns_get_errorstr_by_id(status));
printf(" for ");
ldns_rdf_print(stdout,
ldns_rr_owner(rrset->rrs->rr));
printf("\t");
print_type(rrset->type);
printf("\n");
if (result == LDNS_STATUS_OK) {
result = status;
}
if (status == LDNS_STATUS_SSL_ERR) {
ERR_load_crypto_strings();
ERR_print_errors_fp(stdout);
}
if (verbosity >= 4) {
printf("RRSet:\n");
ldns_dnssec_rrs_print(stdout, rrset->rrs);
printf("Signature:\n");
ldns_rr_print(stdout, cur_sig->rr);
printf("\n");
}
}
}
ldns_rr_list_free(good_keys);
cur_sig = cur_sig->next;
}
} else {
/* delegations are unsigned */
if (rrset->type != LDNS_RR_TYPE_NS ||
ldns_dname_compare(name, zone_name) == 0) {
if (verbosity > 0) {
printf("Error: no signatures for ");
ldns_rdf_print(stdout, ldns_rr_owner(rrset->rrs->rr));
printf("\t");
print_type(rrset->type);
printf("\n");
}
}
}
ldns_rr_list_free(rrset_rrs);
return result;
}
static ldns_status
verify_single_rr(ldns_rr *rr,
ldns_dnssec_rrs *signature_rrs,
ldns_rr_list *keys)
{
ldns_rr_list *rrset_rrs;
ldns_rr_list *good_keys;
ldns_dnssec_rrs *cur_sig;
ldns_status status;
ldns_status result = LDNS_STATUS_OK;
rrset_rrs = ldns_rr_list_new();
ldns_rr_list_push_rr(rrset_rrs, rr);
cur_sig = signature_rrs;
while (cur_sig) {
good_keys = ldns_rr_list_new();
status = ldns_verify_rrsig_keylist(rrset_rrs,
cur_sig->rr,
keys,
good_keys);
if (status != LDNS_STATUS_OK) {
if (verbosity >= 1) {
printf("Error: %s ", ldns_get_errorstr_by_id(status));
if (result == LDNS_STATUS_OK) {
result = status;
}
printf("for ");
ldns_rdf_print(stdout, ldns_rr_owner(rr));
printf("\t");
print_type(ldns_rr_get_type(rr));
printf("\n");
if (status == LDNS_STATUS_SSL_ERR) {
ERR_load_crypto_strings();
ERR_print_errors_fp(stdout);
}
if (verbosity >= 4) {
printf("RRSet:\n");
ldns_rr_list_print(stdout, rrset_rrs);
printf("Signature:\n");
ldns_rr_print(stdout, cur_sig->rr);
printf("\n");
}
}
result = status;
}
ldns_rr_list_free(good_keys);
cur_sig = cur_sig->next;
}
ldns_rr_list_free(rrset_rrs);
return result;
}
static ldns_status
verify_next_hashed_name(ldns_rbtree_t *zone_nodes,
ldns_dnssec_name *name)
{
ldns_rbnode_t *next_node;
ldns_dnssec_name *next_name;
ldns_dnssec_name *cur_next_name = NULL;
ldns_dnssec_name *cur_first_name = NULL;
int cmp;
char *next_owner_str;
ldns_rdf *next_owner_dname;
if (!name->hashed_name) {
name->hashed_name = ldns_nsec3_hash_name_frm_nsec3(name->nsec,
name->name);
}
next_node = ldns_rbtree_first(zone_nodes);
while (next_node != LDNS_RBTREE_NULL) {
next_name = (ldns_dnssec_name *)next_node->data;
/* skip over names that have no NSEC3 records (whether it
* actually should or should not should have been checked
* already */
if (!next_name->nsec) {
next_node = ldns_rbtree_next(next_node);
continue;
}
if (!next_name->hashed_name) {
next_name->hashed_name = ldns_nsec3_hash_name_frm_nsec3(
name->nsec, next_name->name);
}
/* we keep track of what 'so far' is the next hashed name;
* it must of course be 'larger' than the current name
* if we find one that is larger, but smaller than what we
* previously thought was the next one, that one is the next
*/
cmp = ldns_dname_compare(name->hashed_name,
next_name->hashed_name);
if (cmp < 0) {
if (!cur_next_name) {
cur_next_name = next_name;
} else {
cmp = ldns_dname_compare(next_name->hashed_name,
cur_next_name->hashed_name);
if (cmp < 0) {
cur_next_name = next_name;
}
}
}
/* in case the hashed name of the nsec we are checking is the
* last one, we need the first hashed name of the zone */
if (!cur_first_name) {
cur_first_name = next_name;
} else {
cmp = ldns_dname_compare(next_name->hashed_name,
cur_first_name->hashed_name);
if (cmp < 0) {
cur_first_name = next_name;
}
}
next_node = ldns_rbtree_next(next_node);
}
if (!cur_next_name) {
cur_next_name = cur_first_name;
}
next_owner_str = ldns_rdf2str(ldns_nsec3_next_owner(name->nsec));
next_owner_dname = ldns_dname_new_frm_str(next_owner_str);
cmp = ldns_dname_compare(next_owner_dname,
cur_next_name->hashed_name);
ldns_rdf_deep_free(next_owner_dname);
LDNS_FREE(next_owner_str);
if (cmp != 0) {
printf("Error: The NSEC3 record for ");
ldns_rdf_print(stdout, name->name);
printf(" points to the wrong next hashed owner name\n");
printf("(should point to ");
ldns_rdf_print(stdout, cur_next_name->name);
printf("(whose hashed name is ");
ldns_rdf_print(stdout, cur_next_name->hashed_name);
printf(")\n");
return LDNS_STATUS_ERR;
} else {
return LDNS_STATUS_OK;
}
}
static ldns_rbnode_t *
next_nonglue_node(ldns_rbnode_t *node, ldns_rr_list *glue_rrs)
{
ldns_rbnode_t *cur_node = ldns_rbtree_next(node);
ldns_dnssec_name *cur_name;
while (cur_node != LDNS_RBTREE_NULL) {
cur_name = (ldns_dnssec_name *) cur_node->data;
if (cur_name && cur_name->name) {
if (!ldns_rr_list_contains_name(glue_rrs, cur_name->name)) {
return cur_node;
}
}
cur_node = ldns_rbtree_next(cur_node);
}
return LDNS_RBTREE_NULL;
}
static ldns_status
verify_nsec(ldns_rbtree_t *zone_nodes,
ldns_rbnode_t *cur_node,
ldns_rr_list *keys,
ldns_rr_list *glue_rrs
)
{
ldns_rbnode_t *next_node;
ldns_dnssec_name *name, *next_name;
ldns_status status, result;
result = LDNS_STATUS_OK;
name = (ldns_dnssec_name *) cur_node->data;
if (name->nsec) {
if (name->nsec_signatures) {
status = verify_single_rr(name->nsec,
name->nsec_signatures,
keys);
if (result == LDNS_STATUS_OK) {
result = status;
}
} else {
if (verbosity >= 1) {
printf("Error: the NSEC(3) record of ");
ldns_rdf_print(stdout, name->name);
printf(" has no signatures\n");
}
if (result == LDNS_STATUS_OK) {
result = LDNS_STATUS_ERR;
}
}
/* check whether the NSEC record points to the right name */
switch (ldns_rr_get_type(name->nsec)) {
case LDNS_RR_TYPE_NSEC:
/* simply try next name */
next_node = next_nonglue_node(cur_node, glue_rrs);
if (next_node == LDNS_RBTREE_NULL) {
next_node = ldns_rbtree_first(zone_nodes);
}
next_name = (ldns_dnssec_name *)next_node->data;
if (ldns_dname_compare(next_name->name,
ldns_rr_rdf(name->nsec, 0))
!= 0) {
printf("Error: the NSEC record for ");
ldns_rdf_print(stdout, name->name);
printf(" points to the wrong next owner name\n");
if (result == LDNS_STATUS_OK) {
result = LDNS_STATUS_ERR;
}
}
break;
case LDNS_RR_TYPE_NSEC3:
/* find the hashed next name in the tree */
/* this is expensive, do we need to add support
* for this in the structs? (ie. pointer to next
* hashed name?)
*/
status = verify_next_hashed_name(zone_nodes, name);
if (result == LDNS_STATUS_OK) {
result = status;
}
break;
default:
break;
}
} else {
/* todo; do this once and cache result? */
if (zone_is_nsec3_optout(zone_nodes) &&
only_ns_in_rrsets(name->rrsets)) {
/* ok, no problem, but we need to remember to check
* whether the chain does not actually point to this
* name later */
} else {
if (verbosity >= 1) {
printf("Error: there is no NSEC(3) for ");
ldns_rdf_print(stdout, name->name);
printf("\n");
}
if (result == LDNS_STATUS_OK) {
result = LDNS_STATUS_ERR;
}
}
}
return result;
}
static int
ldns_dnssec_name_has_only_a(ldns_dnssec_name *cur_name)
{
ldns_dnssec_rrsets *cur_rrset;
cur_rrset = cur_name->rrsets;
while (cur_rrset) {
if (cur_rrset->type != LDNS_RR_TYPE_A &&
cur_rrset->type != LDNS_RR_TYPE_AAAA) {
return 0;
} else {
cur_rrset = cur_rrset->next;
}
}
return 1;
}
static ldns_status
verify_dnssec_name(ldns_rdf *zone_name,
ldns_dnssec_zone *zone,
ldns_rbtree_t *zone_nodes,
ldns_rbnode_t *cur_node,
ldns_rr_list *keys,
ldns_rr_list *glue_rrs)
{
ldns_status result = LDNS_STATUS_OK;
ldns_status status;
ldns_dnssec_rrsets *cur_rrset;
ldns_dnssec_name *name;
/* for NSEC chain checks */
name = (ldns_dnssec_name *) cur_node->data;
if (verbosity >= 3) {
printf("Checking: ");
ldns_rdf_print(stdout, name->name);
printf("\n");
}
if (ldns_rr_list_contains_name(glue_rrs, name->name) &&
ldns_dnssec_name_has_only_a(name)
) {
/* glue */
cur_rrset = name->rrsets;
while (cur_rrset) {
if (cur_rrset->signatures) {
if (verbosity >= 1) {
printf("Error: ");
ldns_rdf_print(stdout, name->name);
printf("\t");
print_type(cur_rrset->type);
printf(" has signature(s), but is glue\n");
}
result = LDNS_STATUS_ERR;
}
cur_rrset = cur_rrset->next;
}
if (name->nsec) {
if (verbosity >= 1) {
printf("Error: ");
ldns_rdf_print(stdout, name->name);
printf("\thas an NSEC(3), but is glue\n");
}
result = LDNS_STATUS_ERR;
}
} else {
/* not glue, do real verify */
cur_rrset = name->rrsets;
while(cur_rrset) {
if (cur_rrset->type != LDNS_RR_TYPE_A ||
!ldns_dnssec_zone_find_rrset(zone, name->name, LDNS_RR_TYPE_NS)) {
status = verify_dnssec_rrset(zone_name, name->name, cur_rrset, keys, glue_rrs);
if (status != LDNS_STATUS_OK && result == LDNS_STATUS_OK) {
result = status;
}
}
cur_rrset = cur_rrset->next;
}
status = verify_nsec(zone_nodes, cur_node, keys, glue_rrs);
if (result == LDNS_STATUS_OK) {
result = status;
}
}
return result;
}
static ldns_status
verify_dnssec_zone(ldns_dnssec_zone *dnssec_zone,
ldns_rdf *zone_name,
ldns_rr_list *glue_rrs)
{
ldns_rr_list *keys;
ldns_rbnode_t *cur_node;
ldns_dnssec_rrsets *cur_key_rrset;
ldns_dnssec_rrs *cur_key;
ldns_dnssec_name *cur_name;
ldns_status status;
ldns_status result = LDNS_STATUS_OK;
keys = ldns_rr_list_new();
cur_key_rrset = ldns_dnssec_zone_find_rrset(dnssec_zone,
zone_name,
LDNS_RR_TYPE_DNSKEY);
if (!cur_key_rrset || !cur_key_rrset->rrs) {
if (verbosity >= 1) {
printf("No DNSKEY records at zone apex\n");
}
result = LDNS_STATUS_ERR;
} else {
cur_key = cur_key_rrset->rrs;
while (cur_key) {
if (verbosity >= 4) {
printf("DNSKEY: ");
ldns_rr_print(stdout, cur_key->rr);
}
ldns_rr_list_push_rr(keys, cur_key->rr);
cur_key = cur_key->next;
}
cur_node = ldns_rbtree_first(dnssec_zone->names);
if (cur_node == LDNS_RBTREE_NULL) {
if (verbosity >= 1) {
printf("Empty zone?\n");
}
result = LDNS_STATUS_ERR;
}
while (cur_node != LDNS_RBTREE_NULL) {
cur_name = (ldns_dnssec_name *) cur_node->data;
status = verify_dnssec_name(zone_name,
dnssec_zone,
dnssec_zone->names,
cur_node,
keys,
glue_rrs);
if (status != LDNS_STATUS_OK && result == LDNS_STATUS_OK) {
result = status;
}
cur_node = ldns_rbtree_next(cur_node);
}
}
ldns_rr_list_free(keys);
return result;
}
int
main(int argc, char **argv)
{
char *filename;
FILE *fp;
ldns_zone *z;
int line_nr = 0;
int c;
ldns_status s;
ldns_dnssec_zone *dnssec_zone;
ldns_status result = LDNS_STATUS_ERR;
ldns_rr_list *glue_rrs;
while ((c = getopt(argc, argv, "hvV:")) != -1) {
switch(c) {
case 'h':
printf("Usage: %s [OPTIONS] <zonefile>\n", argv[0]);
printf("\tReads the zonefile and checks for DNSSEC errors.\n");
printf("\nIt checks whether NSEC(3)s are present,");
printf(" and verifies all signatures\n");
printf("It also checks the NSEC(3) chain, but it will error on opted-out delegations\n");
printf("\nOPTIONS:\n");
printf("\t-h show this text\n");
printf("\t-v shows the version and exits\n");
printf("\t-V [0-5]\tset verbosity level (default 3)\n");
printf("\nif no file is given standard input is read\n");
exit(EXIT_SUCCESS);
break;
case 'v':
printf("read zone version %s (ldns version %s)\n",
LDNS_VERSION, ldns_version());
exit(EXIT_SUCCESS);
break;
case 'V':
verbosity = atoi(optarg);
break;
}
}
argc -= optind;
argv += optind;
if (argc == 0) {
fp = stdin;
} else {
filename = argv[0];
fp = fopen(filename, "r");
if (!fp) {
fprintf(stderr,
"Unable to open %s: %s\n",
filename,
strerror(errno));
exit(EXIT_FAILURE);
}
}
s = ldns_zone_new_frm_fp_l(&z, fp, NULL, 0, LDNS_RR_CLASS_IN, &line_nr);
if (s == LDNS_STATUS_OK) {
if (!ldns_zone_soa(z)) {
fprintf(stderr, "; Error: no SOA in the zone\n");
exit(1);
}
glue_rrs = ldns_zone_glue_rr_list(z);
dnssec_zone = create_dnssec_zone(z);
if (verbosity >= 5) {
ldns_dnssec_zone_print(stdout, dnssec_zone);
}
result = verify_dnssec_zone(dnssec_zone,
ldns_rr_owner(ldns_zone_soa(z)),
glue_rrs);
if (result == LDNS_STATUS_OK) {
if (verbosity >= 1) {
printf("Zone is verified and complete\n");
}
} else {
if (verbosity >= 1) {
printf("There were errors in the zone\n");
}
}
ldns_zone_free(z);
ldns_dnssec_zone_deep_free(dnssec_zone);
} else {
fprintf(stderr, "%s at %d\n",
ldns_get_errorstr_by_id(s),
line_nr);
exit(EXIT_FAILURE);
}
fclose(fp);
exit(result);
}
#else
int
main(int argc, char **argv)
{
fprintf(stderr, "ldns-verifyzone needs OpenSSL support, which has not been compiled in\n");
return 1;
}
#endif /* HAVE_SSL */