/* * 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; } }