/*
* dnssecjava - a DNSSEC validating stub resolver for Java
* Copyright (c) 2013-2015 Ingo Bauersachs
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* This file is based on work under the following copyright and permission
* notice:
*
* Copyright (c) 2005 VeriSign. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.jitsi.dnssec.validator;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jitsi.dnssec.SMessage;
import org.jitsi.dnssec.SRRset;
import org.jitsi.dnssec.SecurityStatus;
import org.jitsi.dnssec.R;
import org.jitsi.dnssec.validator.ValUtils.NsecProvesNodataResponse;
import org.xbill.DNS.CNAMERecord;
import org.xbill.DNS.DClass;
import org.xbill.DNS.DNAMERecord;
import org.xbill.DNS.ExtendedFlags;
import org.xbill.DNS.Flags;
import org.xbill.DNS.Header;
import org.xbill.DNS.Master;
import org.xbill.DNS.Message;
import org.xbill.DNS.NSECRecord;
import org.xbill.DNS.Name;
import org.xbill.DNS.NameTooLongException;
import org.xbill.DNS.Rcode;
import org.xbill.DNS.Record;
import org.xbill.DNS.Resolver;
import org.xbill.DNS.ResolverListener;
import org.xbill.DNS.Section;
import org.xbill.DNS.TSIG;
import org.xbill.DNS.TXTRecord;
import org.xbill.DNS.Type;
/**
* This resolver validates responses with DNSSEC.
*/
public class ValidatingResolver implements Resolver {
/**
* The QCLASS being used for the injection of the reason why the validator
* came to the returned result.
*/
public static final int VALIDATION_REASON_QCLASS = 65280;
private static final Logger logger = LoggerFactory.getLogger(ValidatingResolver.class);
/**
* This is the TTL to use when a trust anchor priming query failed to
* validate.
*/
private static final long DEFAULT_TA_BAD_KEY_TTL = 60;
/**
* This is a cache of validated, but expirable DNSKEY rrsets.
*/
private KeyCache keyCache;
/**
* A data structure holding all trust anchors. Trust anchors must be
* "primed" into the cache before being used to validate.
*/
private TrustAnchorStore trustAnchors;
/**
* The local validation utilities.
*/
private ValUtils valUtils;
/**
* The local NSEC3 validation utilities.
*/
private NSEC3ValUtils n3valUtils;
/**
* The resolver that performs the actual DNS lookups.
*/
private Resolver headResolver;
/**
* Creates a new instance of this class.
*
* @param headResolver The resolver to which queries for DS, DNSKEY and
* referring CNAME records are sent.
*/
public ValidatingResolver(Resolver headResolver) {
this.headResolver = headResolver;
headResolver.setEDNS(0, 0, ExtendedFlags.DO, null);
headResolver.setIgnoreTruncation(false);
this.keyCache = new KeyCache();
this.valUtils = new ValUtils();
this.n3valUtils = new NSEC3ValUtils();
this.trustAnchors = new TrustAnchorStore();
}
// ---------------- Module Initialization -------------------
/**
* Initialize the module. The only recognized configuration value is
* <tt>org.jitsi.dnssec.trust_anchor_file</tt>.
*
* @param config The configuration data for this module.
* @throws IOException When the file specified in the config does not exist
* or cannot be read.
*/
public void init(Properties config) throws IOException {
this.keyCache.init(config);
this.n3valUtils.init(config);
this.valUtils.init(config);
// Load trust anchors
String s = config.getProperty("org.jitsi.dnssec.trust_anchor_file");
if (s != null) {
logger.debug("reading trust anchor file file: " + s);
this.loadTrustAnchors(new FileInputStream(s));
}
}
/**
* Load the trust anchor file into the trust anchor store. The trust anchors
* are currently stored in a zone file format list of DNSKEY or DS records.
*
* @param data The trust anchor data.
* @throws IOException when the trust anchor data could not be read.
*/
@SuppressWarnings("unchecked")
public void loadTrustAnchors(InputStream data) throws IOException {
// First read in the whole trust anchor file.
Master master = new Master(data, Name.root, 0);
List<Record> records = new ArrayList<Record>();
Record mr;
while ((mr = master.nextRecord()) != null) {
records.add(mr);
}
// Record.compareTo() should sort them into DNSSEC canonical order.
// Don't care about canonical order per se, but do want them to be
// formable into RRsets.
Collections.sort(records);
SRRset currentRrset = new SRRset();
for (Record r : records) {
// Skip RR types that cannot be used as trust anchors.
if (r.getType() != Type.DNSKEY && r.getType() != Type.DS) {
continue;
}
// If our current set is empty, we can just add it.
if (currentRrset.size() == 0) {
currentRrset.addRR(r);
continue;
}
// If this record matches our current RRset, we can just add it.
if (currentRrset.getName().equals(r.getName()) && currentRrset.getType() == r.getType() && currentRrset.getDClass() == r.getDClass()) {
currentRrset.addRR(r);
continue;
}
// Otherwise, we add the rrset to our set of trust anchors and begin
// a new set
this.trustAnchors.store(currentRrset);
currentRrset = new SRRset();
currentRrset.addRR(r);
}
// add the last rrset (if it was not empty)
if (currentRrset.size() > 0) {
this.trustAnchors.store(currentRrset);
}
}
/**
* Gets the store with the loaded trust anchors.
*
* @return The store with the loaded trust anchors.
*/
public TrustAnchorStore getTrustAnchors() {
return this.trustAnchors;
}
/**
* Given a "postive" 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.
*
* Given an "ANY" response -- a response that contains an answer to a
* qtype==ANY question, with answers. This consists of simply verifying all
* present answer/auth RRsets, with no checking that all types are present.
*
* NOTE: it may be possible to get parent-side delegation point records
* here, which won't all be signed. Right now, this routine relies on the
* upstream iterative resolver to not return these responses -- instead
* treating them as referrals.
*
* NOTE: RFC 4035 is silent on this issue, so this may change upon
* clarification.
*
* @param request The request that generated this response.
* @param response The response to validate.
*/
private void validatePositiveResponse(Message request, SMessage response) {
int qtype = request.getQuestion().getType();
Map<Name, Name> wcs = new HashMap<Name, Name>(1);
List<SRRset> nsec3s = new ArrayList<SRRset>(0);
List<SRRset> nsecs = new ArrayList<SRRset>(0);
if (!this.validateAnswerAndGetWildcards(response, qtype, wcs)) {
return;
}
// validate the AUTHORITY section as well - this will generally be the
// NS rrset (which could be missing, no problem)
SRRset keyRrset;
int[] sections;
if (request.getQuestion().getType() == Type.ANY) {
sections = new int[] { Section.ANSWER, Section.AUTHORITY };
}
else {
sections = new int[] { Section.AUTHORITY };
}
for (int section : sections) {
for (SRRset set : response.getSectionRRsets(section)) {
KeyEntry ke = this.prepareFindKey(set);
if (!this.processKeyValidate(response, set.getSignerName(), ke)) {
return;
}
keyRrset = ke.getRRset();
SecurityStatus status = this.valUtils.verifySRRset(set, keyRrset);
// If anything in the authority section fails to be secure, we
// have a bad message.
if (status != SecurityStatus.SECURE) {
response.setBogus(R.get("failed.authority.positive", set));
return;
}
if (wcs.size() > 0) {
if (set.getType() == Type.NSEC) {
nsecs.add(set);
}
else if (set.getType() == Type.NSEC3) {
nsec3s.add(set);
}
}
}
}
// If this is a positive wildcard response, and we have NSEC records,
// try to use them to
// 1) prove that qname doesn't exist and
// 2) that the correct wildcard was used.
if (wcs.size() > 0) {
for (Map.Entry<Name, Name> wc : wcs.entrySet()) {
boolean wcNsecOk = false;
for (SRRset set : nsecs) {
NSECRecord nsec = (NSECRecord)set.first();
if (ValUtils.nsecProvesNameError(nsec, wc.getKey(), set.getSignerName())) {
try {
Name nsecWc = ValUtils.nsecWildcard(wc.getKey(), nsec);
if (wc.getValue().equals(nsecWc)) {
wcNsecOk = true;
break;
}
}
catch (NameTooLongException e) {
// COVERAGE:OFF -> a NTLE can only be thrown when
// the qname is equal to the NSEC owner or NSEC next
// name, so that the wildcard is appended to
// CE=qname=owner=next. This would however indicate
// that the qname exists, which is proofed not the
// be the case beforehand.
throw new RuntimeException(R.get("failed.positive.wildcardgeneration"));
}
}
}
// 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 (!wcNsecOk && nsec3s.size() > 0) {
if (this.n3valUtils.allNSEC3sIgnoreable(nsec3s, this.keyCache)) {
response.setStatus(SecurityStatus.INSECURE, R.get("failed.nsec3_ignored"));
return;
}
SecurityStatus status = this.n3valUtils.proveWildcard(nsec3s, wc.getKey(), nsec3s.get(0).getSignerName(), wc.getValue());
if (status == SecurityStatus.INSECURE) {
response.setStatus(status);
return;
}
else if (status == SecurityStatus.SECURE) {
wcNsecOk = true;
}
}
// If after all this, we still haven't proven the positive
// wildcard response, fail.
if (!wcNsecOk) {
response.setBogus(R.get("failed.positive.wildcard_too_broad"));
return;
}
}
}
response.setStatus(SecurityStatus.SECURE);
}
private boolean validateAnswerAndGetWildcards(SMessage response, int qtype, Map<Name, Name> wcs) {
// validate the ANSWER section - this will be the answer itself
DNAMERecord dname = null;
for (SRRset set : response.getSectionRRsets(Section.ANSWER)) {
// Validate the CNAME following a (validated) DNAME is correctly
// synthesized.
if (set.getType() == Type.CNAME && dname != null) {
if (set.size() > 1) {
response.setBogus(R.get("failed.synthesize.multiple"));
return false;
}
CNAMERecord cname = (CNAMERecord)set.first();
try {
Name expected = Name.concatenate(cname.getName().relativize(dname.getName()), dname.getTarget());
if (!expected.equals(cname.getTarget())) {
response.setBogus(R.get("failed.synthesize.nomatch", cname.getTarget(), expected));
return false;
}
}
catch (NameTooLongException e) {
response.setBogus(R.get("failed.synthesize.toolong"));
return false;
}
set.setSecurityStatus(SecurityStatus.SECURE);
dname = null;
continue;
}
// Verify the answer rrset.
KeyEntry ke = this.prepareFindKey(set);
if (!this.processKeyValidate(response, set.getSignerName(), ke)) {
return false;
}
SecurityStatus status = this.valUtils.verifySRRset(set, ke.getRRset());
// If the answer rrset failed to validate, then this message is BAD
if (status != SecurityStatus.SECURE) {
response.setBogus(R.get("failed.answer.positive", set));
return false;
}
// 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.
Name wc = null;
try {
wc = ValUtils.rrsetWildcard(set);
}
catch (RuntimeException ex) {
response.setBogus(R.get(ex.getMessage(), set.getName()));
return false;
}
if (wc != null) {
// RFC 4592, Section 4.4 does not allow wildcarded DNAMEs
if (set.getType() == Type.DNAME) {
response.setBogus(R.get("failed.dname.wildcard", set.getName()));
return false;
}
wcs.put(set.getName(), wc);
}
// Notice a DNAME that should be followed by an unsigned CNAME.
if (qtype != Type.DNAME && set.getType() == Type.DNAME) {
dname = (DNAMERecord)set.first();
}
}
return true;
}
/**
* Validate a NOERROR/NODATA signed response -- a response that has a
* NOERROR Rcode but no ANSWER section RRsets. This consists of verifying
* the authority section rrsets and making certain that the authority
* section NSEC/NSEC3s proves that the qname does exist and the qtype
* doesn't.
*
* 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 request The request that generated this response.
* @param response The response to validate.
*/
private void validateNodataResponse(Message request, SMessage response) {
Name qname = request.getQuestion().getName();
int qtype = request.getQuestion().getType();
// Since we are here, the ANSWER section is either empty (and hence
// there's only the NODATA to validate) OR it contains an incomplete
// chain. In this case, the records were already validated before and we
// can concentrate on following the qname that lead to the NODATA
// classification
for (SRRset set : response.getSectionRRsets(Section.ANSWER)) {
if (set.getSecurityStatus() != SecurityStatus.SECURE) {
response.setBogus(R.get("failed.answer.cname_nodata", set.getName()));
return;
}
if (set.getType() == Type.CNAME) {
qname = ((CNAMERecord)set.first()).getTarget();
}
}
// If true, then the NODATA has been proven.
boolean hasValidNSEC = false;
// for wildcard nodata responses. This is the proven closest encloser.
Name ce = null;
// for wildcard nodata responses. This is the wildcard NSEC.
NsecProvesNodataResponse ndp = new NsecProvesNodataResponse();
// A collection of NSEC3 RRs found in the authority section.
List<SRRset> nsec3s = new ArrayList<SRRset>(0);
// The RRSIG signer field for the NSEC3 RRs.
Name nsec3Signer = null;
// validate the AUTHORITY section
for (SRRset set : response.getSectionRRsets(Section.AUTHORITY)) {
KeyEntry ke = this.prepareFindKey(set);
if (!this.processKeyValidate(response, set.getSignerName(), ke)) {
return;
}
SecurityStatus status = this.valUtils.verifySRRset(set, ke.getRRset());
if (status != SecurityStatus.SECURE) {
response.setBogus(R.get("failed.authority.nodata", set));
return;
}
// If we encounter an NSEC record, try to use it to prove NODATA.
// This needs to handle the empty non-terminal (ENT) NODATA case.
if (set.getType() == Type.NSEC) {
NSECRecord nsec = (NSECRecord)set.first();
ndp = ValUtils.nsecProvesNodata(nsec, qname, qtype);
if (ndp.result) {
hasValidNSEC = true;
}
if (ValUtils.nsecProvesNameError(nsec, qname, set.getSignerName())) {
ce = ValUtils.closestEncloser(qname, nsec);
}
}
// Collect any NSEC3 records present.
if (set.getType() == Type.NSEC3) {
nsec3s.add(set);
nsec3Signer = set.getSignerName();
}
}
// check to see if we have a wildcard NODATA proof.
// The wildcard NODATA is 1 NSEC proving that qname does not exists (and
// also proving what the closest encloser is), and 1 NSEC showing the
// matching wildcard, which must be *.closest_encloser.
if (ndp.wc != null && (ce == null || (!ce.equals(ndp.wc) && !qname.equals(ce)))) {
hasValidNSEC = false;
}
this.n3valUtils.stripUnknownAlgNSEC3s(nsec3s);
if (!hasValidNSEC && nsec3s.size() > 0) {
if (this.n3valUtils.allNSEC3sIgnoreable(nsec3s, this.keyCache)) {
response.setStatus(SecurityStatus.BOGUS, R.get("failed.nsec3_ignored"));
return;
}
// try to prove NODATA with our NSEC3 record(s)
SecurityStatus status = this.n3valUtils.proveNodata(nsec3s, qname, qtype, nsec3Signer);
if (status == SecurityStatus.INSECURE) {
response.setStatus(SecurityStatus.INSECURE);
return;
}
hasValidNSEC = status == SecurityStatus.SECURE;
}
if (!hasValidNSEC) {
response.setBogus(R.get("failed.nodata"));
logger.trace("Failed NODATA for " + qname);
return;
}
logger.trace("sucessfully validated NODATA response.");
response.setStatus(SecurityStatus.SECURE);
}
/**
* Validate a NAMEERROR signed response -- a response that has a NXDOMAIN
* Rcode. This consists of verifying the authority section rrsets and making
* certain that the authority section NSEC proves that the qname doesn't
* exist and the covering wildcard also doesn't exist..
*
* 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 request The request to be proved to not exist.
* @param response The response to validate.
*/
private void validateNameErrorResponse(Message request, SMessage response) {
Name qname = request.getQuestion().getName();
// The ANSWER section is either empty OR it contains an xNAME chain that
// ultimately lead to the NAMEERROR response. In this case the ANSWER
// section has already been validated before and we can concentrate on
// following the xNAMEs to find the qname that caused the NXDOMAIN.
for (SRRset set : response.getSectionRRsets(Section.ANSWER)) {
if (set.getSecurityStatus() != SecurityStatus.SECURE) {
response.setBogus(R.get("failed.nxdomain.cname_nxdomain", set));
return;
}
if (set.getType() == Type.CNAME) {
qname = ((CNAMERecord)set.first()).getTarget();
}
}
// Validate the authority section -- all RRsets in the authority section
// must be signed and valid.
// In addition, the NSEC record(s) must prove the NXDOMAIN condition.
boolean hasValidNSEC = false;
boolean hasValidWCNSEC = false;
List<SRRset> nsec3s = new ArrayList<SRRset>(0);
Name nsec3Signer = null;
SRRset keyRrset;
for (SRRset set : response.getSectionRRsets(Section.AUTHORITY)) {
KeyEntry ke = this.prepareFindKey(set);
if (!this.processKeyValidate(response, set.getSignerName(), ke)) {
return;
}
keyRrset = ke.getRRset();
SecurityStatus status = this.valUtils.verifySRRset(set, keyRrset);
if (status != SecurityStatus.SECURE) {
response.setBogus(R.get("failed.nxdomain.authority", set));
return;
}
if (set.getType() == Type.NSEC) {
NSECRecord nsec = (NSECRecord)set.first();
if (ValUtils.nsecProvesNameError(nsec, qname, set.getSignerName())) {
hasValidNSEC = true;
}
if (ValUtils.nsecProvesNoWC(nsec, qname, set.getSignerName())) {
hasValidWCNSEC = true;
}
}
if (set.getType() == Type.NSEC3) {
nsec3s.add(set);
nsec3Signer = set.getSignerName();
}
}
this.n3valUtils.stripUnknownAlgNSEC3s(nsec3s);
if ((!hasValidNSEC || !hasValidWCNSEC) && nsec3s.size() > 0) {
logger.debug("Validating nxdomain: using NSEC3 records");
// Attempt to prove name error with nsec3 records.
if (this.n3valUtils.allNSEC3sIgnoreable(nsec3s, this.keyCache)) {
response.setStatus(SecurityStatus.INSECURE, R.get("failed.nsec3_ignored"));
return;
}
SecurityStatus status = this.n3valUtils.proveNameError(nsec3s, qname, nsec3Signer);
if (status != SecurityStatus.SECURE) {
if (status == SecurityStatus.INSECURE) {
response.setStatus(status, R.get("failed.nxdomain.nsec3_insecure"));
}
else {
response.setStatus(status, R.get("failed.nxdomain.nsec3_bogus"));
}
return;
}
// Note that we assume that the NSEC3ValUtils proofs encompass the
// wildcard part of the proof.
hasValidNSEC = true;
hasValidWCNSEC = true;
}
// If the message fails to prove either condition, it is bogus.
if (!hasValidNSEC) {
response.setBogus(R.get("failed.nxdomain.exists", response.getQuestion().getName()));
return;
}
if (!hasValidWCNSEC) {
response.setBogus(R.get("failed.nxdomain.haswildcard"));
return;
}
// Otherwise, we consider the message secure.
logger.trace("successfully validated NAME ERROR response.");
response.setStatus(SecurityStatus.SECURE);
}
private SMessage sendRequest(Message request) {
Record q = request.getQuestion();
logger.trace("sending request: <" + q.getName() + "/" + Type.string(q.getType()) + "/" + DClass.string(q.getDClass()) + ">");
// Send the request along by using a local copy of the request
Message localRequest = (Message)request.clone();
localRequest.getHeader().setFlag(Flags.CD);
try {
Message resp = this.headResolver.send(localRequest);
return new SMessage(resp);
}
catch (SocketTimeoutException e) {
logger.error("Query timed out, returning fail", e);
return ValidatingResolver.errorMessage(localRequest, Rcode.SERVFAIL);
}
catch (UnknownHostException e) {
logger.error("failed to send query", e);
return ValidatingResolver.errorMessage(localRequest, Rcode.SERVFAIL);
}
catch (IOException e) {
logger.error("failed to send query", e);
return ValidatingResolver.errorMessage(localRequest, Rcode.SERVFAIL);
}
}
private KeyEntry prepareFindKey(SRRset rrset) {
FindKeyState state = new FindKeyState();
state.signerName = rrset.getSignerName();
state.qclass = rrset.getDClass();
if (state.signerName == null) {
state.signerName = rrset.getName();
}
SRRset trustAnchorRRset = this.trustAnchors.find(state.signerName, rrset.getDClass());
if (trustAnchorRRset == null) {
// response isn't under a trust anchor, so we cannot validate.
return KeyEntry.newNullKeyEntry(rrset.getSignerName(), rrset.getDClass(), DEFAULT_TA_BAD_KEY_TTL);
}
state.keyEntry = this.keyCache.find(state.signerName, rrset.getDClass());
if (state.keyEntry == null || (!state.keyEntry.getName().equals(state.signerName) && state.keyEntry.isGood())) {
// start the FINDKEY phase with the trust anchor
state.dsRRset = trustAnchorRRset;
state.keyEntry = null;
state.currentDSKeyName = new Name(trustAnchorRRset.getName(), 1);
// and otherwise, don't continue processing this event.
// (it will be reactivated when the priming query returns).
this.processFindKey(state);
}
return state.keyEntry;
}
/**
* Process the FINDKEY state. Generally this just calculates the next name
* to query and either issues a DS or a DNSKEY query. It will check to see
* if the correct key has already been reached, in which case it will
* advance the event to the next state.
*
* @param state The state associated with the current key finding phase.
*/
private void processFindKey(FindKeyState state) {
// We know that state.keyEntry is not a null or bad key -- if it were,
// then previous processing should have directed this event to a
// different state.
int qclass = state.qclass;
Name targetKeyName = state.signerName;
Name currentKeyName = Name.empty;
if (state.keyEntry != null) {
currentKeyName = state.keyEntry.getName();
}
if (state.currentDSKeyName != null) {
currentKeyName = state.currentDSKeyName;
state.currentDSKeyName = null;
}
// If our current key entry matches our target, then we are done.
if (currentKeyName.equals(targetKeyName)) {
return;
}
if (state.emptyDSName != null) {
currentKeyName = state.emptyDSName;
}
// Calculate the next lookup name.
int targetLabels = targetKeyName.labels();
int currentLabels = currentKeyName.labels();
int l = targetLabels - currentLabels - 1;
// the next key name would be trying to invent a name, so we stop here
if (l < 0) {
return;
}
Name nextKeyName = new Name(targetKeyName, l);
logger.trace("findKey: targetKeyName = " + targetKeyName + ", currentKeyName = " + currentKeyName + ", nextKeyName = " + nextKeyName);
// The next step is either to query for the next DS, or to query for the
// next DNSKEY.
if (state.dsRRset == null || !state.dsRRset.getName().equals(nextKeyName)) {
Message dsRequest = Message.newQuery(Record.newRecord(nextKeyName, Type.DS, qclass));
SMessage dsResponse = this.sendRequest(dsRequest);
this.processDSResponse(dsRequest, dsResponse, state);
return;
}
// Otherwise, it is time to query for the DNSKEY
Message dnskeyRequest = Message.newQuery(Record.newRecord(state.dsRRset.getName(), Type.DNSKEY, qclass));
SMessage dnskeyResponse = this.sendRequest(dnskeyRequest);
this.processDNSKEYResponse(dnskeyRequest, dnskeyResponse, state);
}
/**
* Given a DS response, the DS request, and the current key rrset, validate
* the DS response, returning a KeyEntry.
*
* @param response The DS response.
* @param request The DS request.
* @param keyRrset The current DNSKEY rrset from the forEvent state.
*
* @return A KeyEntry, bad if the DS response fails to validate, null if the
* DS response indicated an end to secure space, good if the DS
* validated. It returns null if the DS response indicated that the
* request wasn't a delegation point.
*/
private KeyEntry dsResponseToKE(SMessage response, Message request, SRRset keyRrset) {
Name qname = request.getQuestion().getName();
int qclass = request.getQuestion().getDClass();
SecurityStatus status;
ResponseClassification subtype = ValUtils.classifyResponse(response);
KeyEntry bogusKE = KeyEntry.newBadKeyEntry(qname, qclass, DEFAULT_TA_BAD_KEY_TTL);
switch (subtype) {
case POSITIVE:
// Verify only returns BOGUS or SECURE. If the rrset is bogus,
// then we are done.
SRRset dsRrset = response.findAnswerRRset(qname, Type.DS, qclass);
status = this.valUtils.verifySRRset(dsRrset, keyRrset);
if (status != SecurityStatus.SECURE) {
bogusKE.setBadReason(R.get("failed.ds"));
return bogusKE;
}
if (!ValUtils.atLeastOneSupportedAlgorithm(dsRrset)) {
KeyEntry nullKey = KeyEntry.newNullKeyEntry(qname, qclass, dsRrset.getTTL());
nullKey.setBadReason(R.get("insecure.ds.noalgorithms", qname));
return nullKey;
}
// Otherwise, we return the positive response.
logger.trace("DS rrset was good.");
return KeyEntry.newKeyEntry(dsRrset);
case CNAME:
// Verify only returns BOGUS or SECURE. If the rrset is bogus,
// then we are done.
SRRset cnameRrset = response.findAnswerRRset(qname, Type.CNAME, qclass);
status = this.valUtils.verifySRRset(cnameRrset, keyRrset);
if (status == SecurityStatus.SECURE) {
return null;
}
bogusKE.setBadReason(R.get("failed.ds.cname"));
return bogusKE;
case NODATA:
case NAMEERROR:
return this.dsReponseToKeForNodata(response, request, keyRrset);
default:
// We've encountered an unhandled classification for this
// response.
bogusKE.setBadReason(R.get("failed.ds.notype", subtype));
return bogusKE;
}
}
/**
* Given a DS response, the DS request, and the current key rrset, validate
* the DS response for the NODATA case, returning a KeyEntry.
*
* @param response The DS response.
* @param request The DS request.
* @param keyRrset The current DNSKEY rrset from the forEvent state.
*
* @return A KeyEntry, bad if the DS response fails to validate, null if the
* DS response indicated an end to secure space, good if the DS
* validated. It returns null if the DS response indicated that the
* request wasn't a delegation point.
*/
private KeyEntry dsReponseToKeForNodata(SMessage response, Message request, SRRset keyRrset) {
Name qname = request.getQuestion().getName();
int qclass = request.getQuestion().getDClass();
KeyEntry bogusKE = KeyEntry.newBadKeyEntry(qname, qclass, DEFAULT_TA_BAD_KEY_TTL);
if (!this.valUtils.hasSignedNsecs(response)) {
bogusKE.setBadReason(R.get("failed.ds.nonsec", qname));
return bogusKE;
}
// Try to prove absence of the DS with NSEC
JustifiedSecStatus status = this.valUtils.nsecProvesNodataDsReply(request, response, keyRrset);
switch (status.status) {
case SECURE:
KeyEntry nullKey = KeyEntry.newNullKeyEntry(qname, qclass, DEFAULT_TA_BAD_KEY_TTL);
nullKey.setBadReason(R.get("insecure.ds.nsec"));
return nullKey;
case INSECURE:
return null;
case BOGUS:
bogusKE.setBadReason(status.reason);
return bogusKE;
default:
// NSEC proof did not work, try NSEC3
break;
}
// Or it could be using NSEC3.
SRRset[] nsec3Rrsets = response.getSectionRRsets(Section.AUTHORITY, Type.NSEC3);
List<SRRset> nsec3s = new ArrayList<SRRset>(0);
Name nsec3Signer = null;
long nsec3TTL = -1;
if (nsec3Rrsets.length > 0) {
// Attempt to prove no DS with NSEC3s.
for (SRRset nsec3set : nsec3Rrsets) {
SecurityStatus sstatus = this.valUtils.verifySRRset(nsec3set, keyRrset);
if (sstatus != SecurityStatus.SECURE) {
// We could just fail here as there is an invalid rrset, but
// skipping doesn't matter because we might not need it or
// the proof will fail anyway.
logger.debug("skipping bad nsec3");
continue;
}
nsec3Signer = nsec3set.getSignerName();
if (nsec3TTL < 0 || nsec3set.getTTL() < nsec3TTL) {
nsec3TTL = nsec3set.getTTL();
}
nsec3s.add(nsec3set);
}
switch (this.n3valUtils.proveNoDS(nsec3s, qname, nsec3Signer)) {
case INSECURE:
logger.debug("nsec3s proved no delegation.");
return null;
case SECURE:
KeyEntry nullKey = KeyEntry.newNullKeyEntry(qname, qclass, nsec3TTL);
nullKey.setBadReason(R.get("insecure.ds.nsec3"));
return nullKey;
default:
bogusKE.setBadReason(R.get("failed.ds.nsec3"));
return bogusKE;
}
}
// Apparently, no available NSEC/NSEC3 proved NODATA, so this is
// BOGUS.
bogusKE.setBadReason(R.get("failed.ds.unknown"));
return bogusKE;
}
/**
* This handles the responses to locally generated DS queries.
*
* @param request The request for which the response is processed.
* @param response The response to process.
* @param state The state associated with the current key finding phase.
*/
private void processDSResponse(Message request, SMessage response, FindKeyState state) {
Name qname = request.getQuestion().getName();
state.emptyDSName = null;
state.dsRRset = null;
KeyEntry dsKE = this.dsResponseToKE(response, request, state.keyEntry.getRRset());
if (dsKE == null) {
// DS response indicated that we aren't on a delegation point.
state.emptyDSName = qname;
}
else if (dsKE.isGood()) {
state.dsRRset = dsKE.getRRset();
state.currentDSKeyName = new Name(dsKE.getRRset().getName(), 1);
}
else {
// The reason for the DS to be not good (that is, either bad
// or null) should have been logged by dsResponseToKE.
state.keyEntry = dsKE;
if (dsKE.isNull()) {
this.keyCache.store(dsKE);
}
// The FINDKEY phase has ended, so move on.
return;
}
this.processFindKey(state);
}
private void processDNSKEYResponse(Message request, SMessage response, FindKeyState state) {
Name qname = request.getQuestion().getName();
int qclass = request.getQuestion().getDClass();
SRRset dnskeyRrset = response.findAnswerRRset(qname, Type.DNSKEY, qclass);
if (dnskeyRrset == null) {
// If the DNSKEY rrset was missing, this is the end of the line.
state.keyEntry = KeyEntry.newBadKeyEntry(qname, qclass, DEFAULT_TA_BAD_KEY_TTL);
state.keyEntry.setBadReason(R.get("dnskey.no_rrset", qname));
return;
}
state.keyEntry = this.valUtils.verifyNewDNSKEYs(dnskeyRrset, state.dsRRset, DEFAULT_TA_BAD_KEY_TTL);
// If the key entry isBad or isNull, then we can move on to the next
// state.
if (!state.keyEntry.isGood()) {
return;
}
// The DNSKEY validated, so cache it as a trusted key rrset.
this.keyCache.store(state.keyEntry);
// If good, we stay in the FINDKEY state.
this.processFindKey(state);
}
private boolean processKeyValidate(SMessage response, Name signerName, KeyEntry keyEntry) {
// signerName being null is the indicator that this response was
// unsigned
if (signerName == null) {
logger.debug("processKeyValidate: no signerName.");
// Unsigned responses must be underneath a "null" key entry.
if (keyEntry.isNull()) {
String reason = keyEntry.getBadReason();
if (reason == null) {
reason = R.get("validate.insecure_unsigned");
}
response.setStatus(SecurityStatus.INSECURE, reason);
return false;
}
if (keyEntry.isGood()) {
response.setStatus(SecurityStatus.BOGUS, R.get("validate.bogus.missingsig"));
return false;
}
response.setStatus(SecurityStatus.BOGUS, R.get("validate.bogus", keyEntry.getBadReason()));
return false;
}
if (keyEntry.isBad()) {
response.setStatus(SecurityStatus.BOGUS, R.get("validate.bogus.badkey", keyEntry.getName(), keyEntry.getBadReason()));
return false;
}
if (keyEntry.isNull()) {
String reason = keyEntry.getBadReason();
if (reason == null) {
reason = R.get("validate.insecure");
}
response.setStatus(SecurityStatus.INSECURE, reason);
return false;
}
return true;
}
private SMessage processValidate(Message request, SMessage response) {
ResponseClassification subtype = ValUtils.classifyResponse(response);
switch (subtype) {
case POSITIVE:
case CNAME:
case ANY:
logger.trace("Validating a positive response");
this.validatePositiveResponse(request, response);
break;
case NODATA:
logger.trace("Validating a nodata response");
this.validateNodataResponse(request, response);
break;
case CNAME_NODATA:
logger.trace("Validating a CNAME_NODATA response");
this.validatePositiveResponse(request, response);
if (response.getStatus() != SecurityStatus.INSECURE) {
response.setStatus(SecurityStatus.UNCHECKED);
this.validateNodataResponse(request, response);
}
break;
case NAMEERROR:
logger.trace("Validating a nxdomain response");
this.validateNameErrorResponse(request, response);
break;
case CNAME_NAMEERROR:
logger.trace("Validating a cname_nxdomain response");
this.validatePositiveResponse(request, response);
if (response.getStatus() != SecurityStatus.INSECURE) {
response.setStatus(SecurityStatus.UNCHECKED);
this.validateNameErrorResponse(request, response);
}
break;
default:
response.setStatus(SecurityStatus.BOGUS, R.get("validate.response.unknown", subtype));
}
return this.processFinishedState(request, response);
}
/**
* Apply any final massaging to a response before returning up the pipeline.
* Primarily this means setting the AD bit or not and possibly stripping
* DNSSEC data.
*/
private SMessage processFinishedState(Message request, SMessage response) {
// If the response message validated, set the AD bit.
SecurityStatus status = response.getStatus();
String reason = response.getBogusReason();
switch (status) {
case BOGUS:
// For now, in the absence of any other API information, we
// return SERVFAIL.
int code = response.getHeader().getRcode();
if (code == Rcode.NOERROR || code == Rcode.NXDOMAIN || code == Rcode.YXDOMAIN) {
code = Rcode.SERVFAIL;
}
response = ValidatingResolver.errorMessage(request, code);
break;
case SECURE:
response.getHeader().setFlag(Flags.AD);
break;
case UNCHECKED:
case INSECURE:
break;
default:
throw new RuntimeException("unexpected security status");
}
response.setStatus(status, reason);
return response;
}
// Resolver-interface implementation --------------------------------------
/**
* Forwards the data to the head resolver passed at construction time.
*
* @param port The IP destination port for the queries sent.
* @see org.xbill.DNS.Resolver#setPort(int)
*/
public void setPort(int port) {
this.headResolver.setPort(port);
}
/**
* Forwards the data to the head resolver passed at construction time.
*
* @param flag <code>true</code> to enable TCP, <code>false</code> to
* disable it.
* @see org.xbill.DNS.Resolver#setTCP(boolean)
*/
public void setTCP(boolean flag) {
this.headResolver.setTCP(flag);
}
/**
* This is a no-op, truncation is never ignored.
*
* @param flag unused
*/
public void setIgnoreTruncation(boolean flag) {
}
/**
* This is a no-op, EDNS is always set to level 0.
*
* @param level unused
*/
public void setEDNS(int level) {
}
/**
* The method is forwarded to the resolver, but always ensure that the level
* is 0 and the flags contains DO.
*
* @param level unused, always set to 0.
* @param payloadSize The maximum DNS packet size that this host is capable
* of receiving over UDP. If 0 is specified, the default (1280)
* is used.
* @param flags EDNS extended flags to be set in the OPT record,
* {@link ExtendedFlags#DO} is always appended.
* @param options EDNS options to be set in the OPT record, specified as a
* List of OPTRecord.Option elements.
* @see org.xbill.DNS.Resolver#setEDNS(int, int, int, java.util.List)
*/
public void setEDNS(int level, int payloadSize, int flags, @SuppressWarnings("rawtypes") List options) {
this.headResolver.setEDNS(0, payloadSize, flags | ExtendedFlags.DO, options);
}
/**
* Forwards the data to the head resolver passed at construction time.
*
* @param key The key.
* @see org.xbill.DNS.Resolver#setTSIGKey(org.xbill.DNS.TSIG)
*/
public void setTSIGKey(TSIG key) {
this.headResolver.setTSIGKey(key);
}
/**
* Sets the amount of time to wait for a response before giving up. This
* applies only to the head resolver, the time for an actual query to the
* validating resolver IS higher.
*
* @param secs The number of seconds to wait.
* @param msecs The number of milliseconds to wait.
*/
public void setTimeout(int secs, int msecs) {
this.headResolver.setTimeout(secs, msecs);
}
/**
* Sets the amount of time to wait for a response before giving up. This
* applies only to the head resolver, the time for an actual query to the
* validating resolver IS higher.
*
* @param secs The number of seconds to wait.
*/
public void setTimeout(int secs) {
this.headResolver.setTimeout(secs);
}
/**
* Sends a message and validates the response with DNSSEC before returning
* it.
*
* @param query The query to send.
* @return The validated response message.
* @throws IOException An error occurred while sending or receiving.
*/
public Message send(Message query) throws IOException {
SMessage response = this.sendRequest(query);
response.getHeader().unsetFlag(Flags.AD);
// If the CD bit is set, do not process the (cached) validation status.
if (query.getHeader().getFlag(Flags.CD)) {
return response.getMessage();
}
// Positive RRSIG responses cannot be validated as there are no
// signatures on signatures. Negative answers CAN be validated.
Message rrsigResponse = response.getMessage();
if (query.getQuestion().getType() == Type.RRSIG && rrsigResponse.getHeader().getRcode() == Rcode.NOERROR
&& rrsigResponse.getSectionRRsets(Section.ANSWER).length > 0) {
rrsigResponse.getHeader().unsetFlag(Flags.AD);
return rrsigResponse;
}
final SMessage validated = this.processValidate(query, response);
Message m = validated.getMessage();
String reason = validated.getBogusReason();
if (reason != null) {
final int maxTxtRecordStringLength = 255;
String[] parts = new String[reason.length() / maxTxtRecordStringLength + 1];
for (int i = 0; i < parts.length; i++) {
int length = Math.min((i + 1) * maxTxtRecordStringLength, reason.length());
parts[i] = reason.substring(i * maxTxtRecordStringLength, length);
}
m.addRecord(new TXTRecord(Name.root, VALIDATION_REASON_QCLASS, 0, Arrays.asList(parts)), Section.ADDITIONAL);
}
return m;
}
/**
* Not implemented.
*
* @param query The query to send
* @param listener The object containing the callbacks.
* @return An identifier, which is also a parameter in the callback
* @throws UnsupportedOperationException Always
*/
public Object sendAsync(Message query, ResolverListener listener) {
throw new UnsupportedOperationException("Not implemented");
}
/**
* Creates a response message with the given return code.
*
* @param request The request for which the response belongs.
* @param rcode The response code, @see Rcode
* @return The response message for <code>request</code>.
*/
private static SMessage errorMessage(Message request, int rcode) {
SMessage m = new SMessage(request.getHeader().getID(), request.getQuestion());
Header h = m.getHeader();
h.setRcode(rcode);
h.setFlag(Flags.QR);
return m;
}
}