VALIDATE state and positive response validation.

git-svn-id: file:///svn/unbound/trunk@532 be551aaa-1e26-0410-a405-d3ace91eadb9
This commit is contained in:
Wouter Wijngaards 2007-08-20 12:31:12 +00:00
parent 3f8b0b1cfe
commit 1b42a51048
9 changed files with 472 additions and 0 deletions

View 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.

View 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);
}

View 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;
}

View 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 */

View 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;
}

View 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 */

View 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;
}

View 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 */

View 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",