/* * 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 #include #include #include #ifdef HAVE_SSL #include 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] \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 */