From 1b42a5104859123ba5771055a3b606a726c07cda Mon Sep 17 00:00:00 2001 From: Wouter Wijngaards Date: Mon, 20 Aug 2007 12:31:12 +0000 Subject: [PATCH] VALIDATE state and positive response validation. git-svn-id: file:///svn/unbound/trunk@532 be551aaa-1e26-0410-a405-d3ace91eadb9 --- doc/Changelog | 1 + testcode/unitdname.c | 22 ++++ util/data/dname.c | 11 ++ util/data/dname.h | 8 ++ validator/val_nsec.c | 87 ++++++++++++++ validator/val_nsec.h | 21 ++++ validator/val_utils.c | 44 +++++++ validator/val_utils.h | 19 ++++ validator/validator.c | 259 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 472 insertions(+) diff --git a/doc/Changelog b/doc/Changelog index c80ae65fa..761d65233 100644 --- a/doc/Changelog +++ b/doc/Changelog @@ -1,5 +1,6 @@ 18 August 2007: Wouter - process DNSKEY response in FINDKEY state. + - validate and positive validation, positive wildcard NSEC validation. 17 August 2007: Wouter - work on DS2KE routine. diff --git a/testcode/unitdname.c b/testcode/unitdname.c index f8662f7e6..24b52b783 100644 --- a/testcode/unitdname.c +++ b/testcode/unitdname.c @@ -711,6 +711,27 @@ dname_test_canoncmp() ) == 0); } +/** Test dname_get_shared_topdomain */ +static void +dname_test_topdomain() +{ + unit_assert( query_dname_compare( + dname_get_shared_topdomain( + (uint8_t*)"", + (uint8_t*)""), + (uint8_t*)"") == 0); + unit_assert( query_dname_compare( + dname_get_shared_topdomain( + (uint8_t*)"\003www\007example\003com", + (uint8_t*)"\003www\007example\003com"), + (uint8_t*)"\003www\007example\003com") == 0); + unit_assert( query_dname_compare( + dname_get_shared_topdomain( + (uint8_t*)"\003www\007example\003com", + (uint8_t*)"\003bla\007example\003com"), + (uint8_t*)"\007example\003com") == 0); +} + void dname_test() { ldns_buffer* buff = ldns_buffer_new(65800); @@ -729,5 +750,6 @@ void dname_test() dname_test_sigcount(); dname_test_iswild(); dname_test_canoncmp(); + dname_test_topdomain(); ldns_buffer_free(buff); } diff --git a/util/data/dname.c b/util/data/dname.c index 2d3289831..47598f812 100644 --- a/util/data/dname.c +++ b/util/data/dname.c @@ -725,3 +725,14 @@ dname_canonical_compare(uint8_t* d1, uint8_t* d2) labs2 = dname_count_labels(d2); return dname_canon_lab_cmp(d1, labs1, d2, labs2, &m); } + +uint8_t* dname_get_shared_topdomain(uint8_t* d1, uint8_t* d2) +{ + int labs1, labs2, m; + size_t len = LDNS_MAX_DOMAINLEN; + labs1 = dname_count_labels(d1); + labs2 = dname_count_labels(d2); + (void)dname_lab_cmp(d1, labs1, d2, labs2, &m); + dname_remove_labels(&d1, &len, labs1-m); + return d1; +} diff --git a/util/data/dname.h b/util/data/dname.h index f2b299877..4d1d7b34f 100644 --- a/util/data/dname.h +++ b/util/data/dname.h @@ -280,4 +280,12 @@ int dname_canon_lab_cmp(uint8_t* d1, int labs1, uint8_t* d2, int labs2, */ int dname_canonical_compare(uint8_t* d1, uint8_t* d2); +/** + * Get the shared topdomain between two names. Root "." or longer. + * @param d1: first dname. pointer to uncompressed wireformat. + * @param d2: second dname. pointer to uncompressed wireformat. + * @return pointer to shared topdomain. Ptr to a part of d1. + */ +uint8_t* dname_get_shared_topdomain(uint8_t* d1, uint8_t* d2); + #endif /* UTIL_DATA_DNAME_H */ diff --git a/validator/val_nsec.c b/validator/val_nsec.c index 9eac00bed..89eb14a94 100644 --- a/validator/val_nsec.c +++ b/validator/val_nsec.c @@ -304,3 +304,90 @@ int nsec_proves_nodata(struct ub_packed_rrset_key* nsec, return 1; } + +int +val_nsec_proves_name_error(struct ub_packed_rrset_key* nsec, uint8_t* qname) +{ + uint8_t* owner = nsec->rk.dname; + uint8_t* next; + size_t nlen; + if(!nsec_get_next(nsec, &next, &nlen)) + return 0; + + /* If NSEC owner == qname, then this NSEC proves that qname exists. */ + if(query_dname_compare(qname, owner) == 0) { + return 0; + } + + /* If NSEC is a parent of qname, we need to check the type map + * If the parent name has a DNAME or is a delegation point, then + * this NSEC is being misused. */ + if(dname_subdomain_c(qname, owner) && + (nsec_has_type(nsec, LDNS_RR_TYPE_DNAME) || + (nsec_has_type(nsec, LDNS_RR_TYPE_NS) + && !nsec_has_type(nsec, LDNS_RR_TYPE_SOA)) + )) { + return 0; + } + + /* see if this nsec is the only nsec */ + if(query_dname_compare(owner, next) == 0) { + /* only zone.name NSEC zone.name, disproves everything else */ + return 1; + } + /* see if this nsec is the last nsec */ + if(dname_canonical_compare(owner, next) > 0) { + /* this is the last nsec, ....(bigger) NSEC zonename(smaller) */ + /* the names after the last (owner) name do not exist */ + if(dname_canonical_compare(owner, qname) < 0) + return 1; + /* there are no names before the zone name in the zone */ + /* if(dname_canonical_compare(qname, next) < 0) return 1; */ + } else { + /* regular NSEC, (smaller) NSEC (larger) */ + if(dname_canonical_compare(owner, qname) < 0 && + dname_canonical_compare(qname, next) < 0) { + return 1; + } + } + return 0; +} + +/** + * Determine closest encloser of a query name and the NSEC that covers it + * (and thus disproved it). + */ +static uint8_t* nsec_closest_encloser(uint8_t* qname, + struct ub_packed_rrset_key* nsec) +{ + uint8_t* next; + size_t nlen; + uint8_t* common1, *common2; + if(!nsec_get_next(nsec, &next, &nlen)) + return NULL; + /* longest common with owner or next name */ + common1 = dname_get_shared_topdomain(nsec->rk.dname, qname); + common2 = dname_get_shared_topdomain(next, qname); + if(dname_count_labels(common1) > dname_count_labels(common2)) + return common1; + return common2; +} + +int val_nsec_proves_positive_wildcard(struct ub_packed_rrset_key* nsec, + struct query_info* qinf, uint8_t* wc) +{ + uint8_t* ce; + /* 1) prove that qname doesn't exist and + * 2) that the correct wildcard was used + * nsec has been verified already. */ + if(!val_nsec_proves_name_error(nsec, qinf->qname)) + return 0; + /* check wildcard name */ + ce = nsec_closest_encloser(qinf->qname, nsec); + if(!ce) + return 0; + if(query_dname_compare(wc, ce) != 0) { + return 0; + } + return 1; +} diff --git a/validator/val_nsec.h b/validator/val_nsec.h index aee1483be..bc73af859 100644 --- a/validator/val_nsec.h +++ b/validator/val_nsec.h @@ -91,4 +91,25 @@ int unitest_nsec_has_type_rdata(char* bitmap, size_t len, uint16_t type); int nsec_proves_nodata(struct ub_packed_rrset_key* nsec, struct query_info* qinfo); +/** + * Determine if the given NSEC proves a NameError (NXDOMAIN) for a given + * qname. + * + * @param nsec: the nsec to check + * @param qname: what was queried. + * @return true if proven. + */ +int val_nsec_proves_name_error(struct ub_packed_rrset_key* nsec, + uint8_t* qname); + +/** + * Determine if the given NSEC proves a positive wildcard response. + * @param nsec: the nsec to check + * @param qinf: what was queried. + * @param wc: wildcard (without *. label) + * @return true if proven. + */ +int val_nsec_proves_positive_wildcard(struct ub_packed_rrset_key* nsec, + struct query_info* qinf, uint8_t* wc); + #endif /* VALIDATOR_VAL_NSEC_H */ diff --git a/validator/val_utils.c b/validator/val_utils.c index 795d1b857..a080d359e 100644 --- a/validator/val_utils.c +++ b/validator/val_utils.c @@ -343,3 +343,47 @@ val_dsset_isusable(struct ub_packed_rrset_key* ds_rrset) } return 0; } + +/** get label count for a signature */ +static uint8_t +rrsig_get_labcount(struct packed_rrset_data* d, size_t sig) +{ + if(d->rr_len[sig] < 2+4) + return 0; /* bad sig length */ + return d->rr_data[sig][2+3]; +} + +int +val_rrset_wildcard(struct ub_packed_rrset_key* rrset, uint8_t** wc) +{ + struct packed_rrset_data* d = (struct packed_rrset_data*)rrset-> + entry.data; + uint8_t labcount; + int labdiff; + size_t i; + if(d->rrsig_count == 0) { + *wc = NULL; + return 0; + } + labcount = rrsig_get_labcount(d, d->count + 0); + /* check rest of signatures identical */ + for(i=1; irrsig_count; i++) { + if(labcount != rrsig_get_labcount(d, d->count + i)) { + *wc = NULL; + return 0; + } + } + /* OK the rrsigs check out */ + /* if the RRSIG label count is shorter than the number of actual + * labels, then this rrset was synthesized from a wildcard. + * Note that the RRSIG label count doesn't count the root label. */ + labdiff = (dname_count_labels(rrset->rk.dname) - 1) - (int)labcount; + if(labdiff > 0) { + size_t wl = rrset->rk.dname_len; + *wc = rrset->rk.dname; + dname_remove_labels(wc, &wl, labdiff); + return 1; + } + *wc = NULL; + return 1; +} diff --git a/validator/val_utils.h b/validator/val_utils.h index 7aa48cefd..2819c6469 100644 --- a/validator/val_utils.h +++ b/validator/val_utils.h @@ -150,4 +150,23 @@ struct key_entry_key* val_verify_new_DNSKEYs(struct region* region, */ int val_dsset_isusable(struct ub_packed_rrset_key* ds_rrset); +/** + * Determine by looking at a signed RRset whether or not the RRset name was + * the result of a wildcard expansion. If so, return the name of the + * generating wildcard. + * + * @param rrset The rrset to chedck. + * @param wc: the wildcard name, if the rrset was synthesized from a wildcard. + * null if not. The wildcard name, without "*." in front, is returned. + * This is a pointer into the rrset owner name. + * @return false if the signatures are inconsistent in indicating the + * wildcard status; possible spoofing of wildcard response for other + * responses is being tried. We lost the status which rrsig was verified + * after the verification routine finished, so we simply check if + * the signatures are consistent; inserting a fake signature is a denial + * of service; but in that you could also have removed the real + * signature anyway. + */ +int val_rrset_wildcard(struct ub_packed_rrset_key* rrset, uint8_t** wc); + #endif /* VALIDATOR_VAL_UTILS_H */ diff --git a/validator/validator.c b/validator/validator.c index 2defeeff4..a1cc0a269 100644 --- a/validator/validator.c +++ b/validator/validator.c @@ -262,6 +262,157 @@ prime_trust_anchor(struct module_qstate* qstate, struct val_qstate* vq, return 1; } +/** + * Given a "positive" response -- a response that contains an answer to the + * question, and no CNAME chain, validate this response. This generally + * consists of verifying the answer RRset and the authority RRsets. + * + * Note that by the time this method is called, the process of finding the + * trusted DNSKEY rrset that signs this response must already have been + * completed. + * + * @param env: module env for verify. + * @param ve: validator env for verify. + * @param qchase: query that was made. + * @param chase_reply: answer to that query to validate. + * @param key_entry: the key entry, which is trusted, and which matches + * the signer of the answer. The key entry isgood(). + */ +static void +validate_positive_response(struct module_env* env, struct val_env* ve, + struct query_info* qchase, struct reply_info* chase_reply, + struct key_entry_key* key_entry) +{ + uint8_t* wc = NULL; + int wc_NSEC_ok = 0; + int dname_seen = 0; + int nsec3s_seen = 0; + size_t i; + struct ub_packed_rrset_key* s; + enum sec_status sec; + + /* validate the ANSWER section - this will be the answer itself */ + for(i=0; ian_numrrsets; i++) { + s = chase_reply->rrsets[i]; + /* Skip the CNAME following a (validated) DNAME. + * Because of the normalization routines in the iterator, + * there will always be an unsigned CNAME following a DNAME + * (unless qtype=DNAME). */ + if(dname_seen && ntohs(s->rk.type) == LDNS_RR_TYPE_CNAME) { + dname_seen = 0; + continue; + } + + /* Verify the answer rrset */ + sec = val_verify_rrset_entry(env, ve, s, key_entry); + /* If the (answer) rrset failed to validate, then this + * message is BAD. */ + if(sec != sec_status_secure) { + log_nametypeclass(VERB_ALGO, "Positive response has " + "failed ANSWER rrset: ", s->rk.dname, + ntohs(s->rk.type), ntohs(s->rk.rrset_class)); + chase_reply->security = sec_status_bogus; + return; + } + /* Check to see if the rrset is the result of a wildcard + * expansion. If so, an additional check will need to be + * made in the authority section. */ + if(!val_rrset_wildcard(s, &wc)) { + log_nametypeclass(VERB_ALGO, "Positive response has " + "inconsistent wildcard sigs: ", s->rk.dname, + ntohs(s->rk.type), ntohs(s->rk.rrset_class)); + chase_reply->security = sec_status_bogus; + return; + } + + /* Notice a DNAME that should be followed by an unsigned + * CNAME. */ + if(qchase->qtype != LDNS_RR_TYPE_DNAME && + ntohs(s->rk.type) == LDNS_RR_TYPE_DNAME) { + dname_seen = 1; + } + } + + /* validate the AUTHORITY section as well - this will generally be + * the NS rrset (which could be missing, no problem) */ + for(i=chase_reply->an_numrrsets; ian_numrrsets+ + chase_reply->ns_numrrsets; i++) { + s = chase_reply->rrsets[i]; + sec = val_verify_rrset_entry(env, ve, s, key_entry); + /* If anything in the authority section fails to be secure, + * we have a bad message. */ + if(sec != sec_status_secure) { + log_nametypeclass(VERB_ALGO, "Positive response has " + "failed AUTHORITY rrset: ", s->rk.dname, + ntohs(s->rk.type), ntohs(s->rk.rrset_class)); + chase_reply->security = sec_status_bogus; + return; + } + + /* If this is a positive wildcard response, and we have a + * (just verified) NSEC record, try to use it to 1) prove + * that qname doesn't exist and 2) that the correct wildcard + * was used. */ + if(wc != NULL && ntohs(s->rk.type) == LDNS_RR_TYPE_NSEC) { + if(val_nsec_proves_positive_wildcard(s, qchase, wc)) { + wc_NSEC_ok = 1; + } + /* if not, continue looking for proof */ + } + + /* Otherwise, if this is a positive wildcard response and + * we have NSEC3 records */ + if(wc != NULL && ntohs(s->rk.type) == LDNS_RR_TYPE_NSEC3) { + nsec3s_seen = 1; + } + } + + /* If this was a positive wildcard response that we haven't already + * proven, and we have NSEC3 records, try to prove it using the NSEC3 + * records. */ + if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) { + /* TODO NSEC3 positive wildcard proof */ + /* possibly: wc_NSEC_ok = 1; */ + } + + /* If after all this, we still haven't proven the positive wildcard + * response, fail. */ + if(wc != NULL && !wc_NSEC_ok) { + verbose(VERB_ALGO, "positive response was wildcard " + "expansion and did not prove original data " + "did not exist"); + chase_reply->security = sec_status_bogus; + return; + } + + verbose(VERB_ALGO, "Successfully validated postive response"); + chase_reply->security = sec_status_secure; +} + +/** validate NODATA */ +static void +validate_nodata_response(struct module_env* env, struct val_env* ve, + struct query_info* qchase, struct reply_info* chase_reply, + struct key_entry_key* key_entry) +{ +} + +/** validate NAME ERROR (nxdomain) response */ +static void +validate_nameerror_response(struct module_env* env, struct val_env* ve, + struct query_info* qchase, struct reply_info* chase_reply, + struct key_entry_key* key_entry) +{ +} + +/** validate positive ANY response */ +static void +validate_any_response(struct module_env* env, struct val_env* ve, + struct query_info* qchase, struct reply_info* chase_reply, + struct key_entry_key* key_entry) +{ +} + /** * Process init state for validator. * Process the INIT state. First tier responses start in the INIT state. @@ -426,6 +577,112 @@ processFindKey(struct module_qstate* qstate, struct val_qstate* vq, int id) return 0; } +/** + * Process the VALIDATE stage, the init and findkey stages are finished, + * and the right keys are available to validate the response. + * Or, there are no keys available, in order to invalidate the response. + * + * After validation, the status is recorded in the message and rrsets, + * and finished state is started. + * + * @param qstate: query state. + * @param vq: validator query state. + * @param ve: validator shared global environment. + * @param id: module id. + * @return true if the event should be processed further on return, false if + * not. + */ +static int +processValidate(struct module_qstate* qstate, struct val_qstate* vq, + struct val_env* ve, int id) +{ + enum val_classification subtype; + + if(!vq->key_entry) { + verbose(VERB_ALGO, "validate: no key entry, failed"); + qstate->ext_state[id] = module_error; + return 0; + } + + /* This is the default next state. */ + vq->state = VAL_FINISHED_STATE; + + /* signerName being null is the indicator that this response was + * unsigned */ + if(vq->signer_name == NULL) { + log_query_info(VERB_ALGO, "processValidate: state has no " + "signer name", &vq->qchase); + /* Unsigned responses must be underneath a "null" key entry.*/ + if(key_entry_isnull(vq->key_entry)) { + verbose(VERB_ALGO, "Unsigned response was proven to " + "be validly INSECURE"); + vq->chase_reply->security = sec_status_insecure; + return 1; + } + verbose(VERB_ALGO, "Could not establish validation of " + "INSECURE status of unsigned response."); + vq->chase_reply->security = sec_status_bogus; + return 1; + } + + if(key_entry_isbad(vq->key_entry)) { + log_nametypeclass(VERB_ALGO, "Could not establish a chain " + "of trust to keys for", vq->key_entry->name, + LDNS_RR_TYPE_DNSKEY, vq->key_entry->key_class); + vq->chase_reply->security = sec_status_bogus; + return 1; + } + + if(key_entry_isnull(vq->key_entry)) { + verbose(VERB_ALGO, "Verified that response is INSECURE"); + vq->chase_reply->security = sec_status_insecure; + return 1; + } + + subtype = val_classify_response(&vq->qchase, vq->chase_reply); + switch(subtype) { + case VAL_CLASS_POSITIVE: + verbose(VERB_ALGO, "Validating a positive response"); + validate_positive_response(qstate->env, ve, + &vq->qchase, vq->chase_reply, vq->key_entry); + break; + + case VAL_CLASS_NODATA: + verbose(VERB_ALGO, "Validating a nodata response"); + validate_nodata_response(qstate->env, ve, + &vq->qchase, vq->chase_reply, vq->key_entry); + break; + + case VAL_CLASS_NAMEERROR: + verbose(VERB_ALGO, "Validating a nxdomain response"); + validate_nameerror_response(qstate->env, ve, + &vq->qchase, vq->chase_reply, vq->key_entry); + break; + + case VAL_CLASS_CNAME: + verbose(VERB_ALGO, "Validating a cname response"); + /* + * TODO special CNAME state or routines + validate_cname_response(vq->qchase, vq->chase_reply, + vq->key_entry); + */ + break; + + case VAL_CLASS_ANY: + verbose(VERB_ALGO, "Validating a positive ANY " + "response"); + validate_any_response(qstate->env, ve, + &vq->qchase, vq->chase_reply, vq->key_entry); + break; + + default: + log_err("validate: unhandled response subtype: %d", + subtype); + } + + return 1; +} + /** * Handle validator state. * If a method returns true, the next state is started. If false, then @@ -451,6 +708,8 @@ val_handle(struct module_qstate* qstate, struct val_qstate* vq, cont = processFindKey(qstate, vq, id); break; case VAL_VALIDATE_STATE: + cont = processValidate(qstate, vq, ve, id); + break; case VAL_FINISHED_STATE: default: log_warn("validator: invalid state %d", -- 2.47.2