]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
VALIDATE state and positive response validation.
authorWouter Wijngaards <wouter@nlnetlabs.nl>
Mon, 20 Aug 2007 12:31:12 +0000 (12:31 +0000)
committerWouter Wijngaards <wouter@nlnetlabs.nl>
Mon, 20 Aug 2007 12:31:12 +0000 (12:31 +0000)
git-svn-id: file:///svn/unbound/trunk@532 be551aaa-1e26-0410-a405-d3ace91eadb9

doc/Changelog
testcode/unitdname.c
util/data/dname.c
util/data/dname.h
validator/val_nsec.c
validator/val_nsec.h
validator/val_utils.c
validator/val_utils.h
validator/validator.c

index c80ae65fa4ccc9c3908dc59c9b143adcd52811ca..761d6523336d26f71ee8dbc2eeec728805be283d 100644 (file)
@@ -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.
index f8662f7e6d7c73000e1610dd973afbbf7ca2aabd..24b52b783dae4cddb4682d348dff41519131c0cb 100644 (file)
@@ -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);
 }
index 2d3289831002e83f03fe8d6049928f73fc604375..47598f81206c555982d3fa0eaeba6da96f99e1a3 100644 (file)
@@ -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;
+}
index f2b29987744942985fd64bd9c1b6db2fdd6efe9b..4d1d7b34f2a8c5d0e1af0e953f50e51263cb09f5 100644 (file)
@@ -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 */
index 9eac00bed6f527e5301bd1ae0ef57669fc613ca4..89eb14a94e39d6226d342fe14ded33ac0ad4ac0e 100644 (file)
@@ -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;
+}
index aee1483beeb380e30aac146476593c3d2bdfbfb6..bc73af859b2afffbb2abca5431cdd116c0ac859a 100644 (file)
@@ -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 */
index 795d1b8575be7a14a847f34a81884ad948540bd3..a080d359ea0aabf8f9434c100442f4a7ed3877eb 100644 (file)
@@ -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; i<d->rrsig_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;
+}
index 7aa48cefd7998107bed2c387631a325e630ff797..2819c646935b570505a497f589e1ff89061d3869 100644 (file)
@@ -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 */
index 2defeeff463b433c17038cea0a5ddcc3a76ed684..a1cc0a26987d59a147b6fbec17fd8138772fb4dc 100644 (file)
@@ -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; i<chase_reply->an_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; i<chase_reply->an_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",