All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jitsi.dnssec.validator.ValidatingResolver Maven / Gradle / Ivy

/*
 * 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 static java.util.concurrent.CompletableFuture.completedFuture;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.atomic.AtomicInteger;
import org.jitsi.dnssec.R;
import org.jitsi.dnssec.SMessage;
import org.jitsi.dnssec.SRRset;
import org.jitsi.dnssec.SecurityStatus;
import org.jitsi.dnssec.validator.ValUtils.NsecProvesNodataResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.CNAMERecord;
import org.xbill.DNS.DClass;
import org.xbill.DNS.DNAMERecord;
import org.xbill.DNS.EDNSOption;
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.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;

  /** The clock used to validate messages. */
  private final Clock clock;

  /**
   * 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, Clock.systemUTC());
  }

  /**
   * Creates a new instance of this class.
   *
   * @param headResolver The resolver to which queries for DS, DNSKEY and referring CNAME records
   *     are sent.
   * @param clock the Clock to validate messages.
   */
  public ValidatingResolver(Resolver headResolver, Clock clock) {
    this.headResolver = headResolver;
    this.clock = clock;
    headResolver.setEDNS(0, 0, ExtendedFlags.DO);
    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
   * org.jitsi.dnssec.trust_anchor_file.
   *
   * @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.
   */
  public void loadTrustAnchors(InputStream data) throws IOException {
    // First read in the whole trust anchor file.
    List records = new ArrayList<>();
    try (Master master = new Master(data, Name.root, 0)) {
      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;
  }

  /**
   * For messages that are not referrals, if the chase reply contains an unsigned NS record in the
   * authority section it could have been inserted by a (BIND) forwarder that thinks the zone is
   * insecure, and that has an NS record without signatures in cache. Remove the NS record since the
   * reply does not hinge on that record (in the authority section), but do not remove it if it
   * removes the last record from the answer+authority sections.
   *
   * @param response: the chased reply, we have a key for this contents, so we should have
   *     signatures for these rrsets and not having signatures means it will be bogus.
   */
  private void removeSpuriousAuthority(SMessage response) {
    // if no answer and only 1 auth RRset, do not remove that one
    if (response.getSectionRRsets(Section.ANSWER).size() == 0
        && response.getSectionRRsets(Section.AUTHORITY).size() == 1) {
      return;
    }

    // search authority section for unsigned NS records
    Iterator authRrsetIterator = response.getSectionRRsets(Section.AUTHORITY).iterator();
    while (authRrsetIterator.hasNext()) {
      SRRset rrset = authRrsetIterator.next();
      if (rrset.getType() == Type.NS) {
        if (rrset.sigs().isEmpty()) {
          logger.trace(
              "Removing spurious unsigned NS record (likely inserted by forwarder) {}/{}/{}",
              rrset.getName(),
              Type.string(rrset.getType()),
              DClass.string(rrset.getDClass()));
          authRrsetIterator.remove();
        }
      }
    }
  }

  /**
   * 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 CompletionStage validatePositiveResponse(Message request, SMessage response) { Map wcs = new HashMap<>(1); List nsec3s = new ArrayList<>(0); List nsecs = new ArrayList<>(0); return this.validateAnswerAndGetWildcards(response, request.getQuestion().getType(), wcs) .thenCompose( success -> { if (success) { // validate the AUTHORITY section as well - this will generally be the // NS rrset (which could be missing, no problem) int[] sections; if (request.getQuestion().getType() == Type.ANY) { sections = new int[] {Section.ANSWER, Section.AUTHORITY}; } else { sections = new int[] {Section.AUTHORITY}; } return this.validatePositiveResponseRecursive( response, wcs, nsec3s, nsecs, sections, new AtomicInteger(0), new AtomicInteger(0)); } return completedFuture(false); }) .thenAccept( success -> { if (!success) { return; } // 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 wc : wcs.entrySet()) { boolean wcNsecOk = false; for (SRRset set : nsecs) { NSECRecord nsec = (NSECRecord) set.first(); if (ValUtils.nsecProvesNameError(set, nsec, wc.getKey())) { try { Name nsecWc = ValUtils.nsecWildcard(wc.getKey(), set, 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 CompletionStage validatePositiveResponseRecursive( SMessage response, Map wcs, List nsec3s, List nsecs, int[] sections, AtomicInteger sectionIndex, AtomicInteger setIndex) { // reached the end of the sections to validate, end recursion, success if (sectionIndex.get() >= sections.length) { return completedFuture(true); } List sectionRRsets = response.getSectionRRsets(sections[sectionIndex.get()]); // reached the end of the rrset in the current section, advance to next section if (setIndex.get() >= sectionRRsets.size()) { sectionIndex.getAndIncrement(); setIndex.set(0); return this.validatePositiveResponseRecursive( response, wcs, nsec3s, nsecs, sections, sectionIndex, setIndex); } SRRset set = sectionRRsets.get(setIndex.getAndIncrement()); return this.prepareFindKey(set) .thenCompose( ke -> { JustifiedSecStatus kve = ke.validateKeyFor(set.getSignerName()); if (kve != null) { kve.applyToResponse(response); return completedFuture(false); } SecurityStatus status = this.valUtils.verifySRRset(set, ke, this.clock.instant()); // 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 completedFuture(false); } if (wcs.size() > 0) { if (set.getType() == Type.NSEC) { nsecs.add(set); } else if (set.getType() == Type.NSEC3) { nsec3s.add(set); } } return this.validatePositiveResponseRecursive( response, wcs, nsec3s, nsecs, sections, sectionIndex, setIndex); }); } private CompletionStage validateAnswerAndGetWildcards( SMessage response, int qtype, Map wcs) { return this.validateAnswerAndGetWildcardsRecursive(response, qtype, wcs, new AtomicInteger(0)); } private CompletionStage validateAnswerAndGetWildcardsRecursive( SMessage response, int qtype, Map wcs, AtomicInteger setIndex) { // validate the ANSWER section - this will be the answer itself List sectionRRsets = response.getSectionRRsets(Section.ANSWER); // reached the end of the answer section, success if (setIndex.get() >= sectionRRsets.size()) { return completedFuture(true); } SRRset set = sectionRRsets.get(setIndex.get()); // Verify the answer rrset. return this.prepareFindKey(set) .thenCompose( ke -> { JustifiedSecStatus kve = ke.validateKeyFor(set.getSignerName()); if (kve != null) { kve.applyToResponse(response); return completedFuture(false); } SecurityStatus status = this.valUtils.verifySRRset(set, ke, this.clock.instant()); // 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 completedFuture(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; try { wc = ValUtils.rrsetWildcard(set); } catch (RuntimeException ex) { response.setBogus(R.get(ex.getMessage(), set.getName())); return completedFuture(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 completedFuture(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) { DNAMERecord dname = (DNAMERecord) set.first(); if (setIndex.getAndIncrement() < sectionRRsets.size()) { SRRset cnameSet = sectionRRsets.get(setIndex.get()); // Validate the CNAME following a (validated) DNAME is correctly // synthesized. if (cnameSet.getType() == Type.CNAME && dname != null) { if (cnameSet.size() > 1) { response.setBogus(R.get("failed.synthesize.multiple")); return completedFuture(false); } CNAMERecord cname = (CNAMERecord) cnameSet.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 completedFuture(false); } } catch (NameTooLongException e) { response.setBogus(R.get("failed.synthesize.toolong")); return completedFuture(false); } cnameSet.setSecurityStatus(SecurityStatus.SECURE); } } } setIndex.getAndIncrement(); return this.validateAnswerAndGetWildcardsRecursive(response, qtype, wcs, setIndex); }); } /** * 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 CompletionStage validateNodataResponse(Message request, SMessage response) { Name intermediateQname = 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 completedFuture(null); } if (set.getType() == Type.CNAME) { intermediateQname = ((CNAMERecord) set.first()).getTarget(); } } // validate the AUTHORITY section Name qname = intermediateQname; return this.validateNodataResponseRecursive(response, new AtomicInteger(0)) .handleAsync( (result, ex) -> { if (ex != null) { return null; } // 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 nsec3s = new ArrayList<>(0); // The RRSIG signer field for the NSEC3 RRs. Name nsec3Signer = null; for (SRRset set : response.getSectionRRsets(Section.AUTHORITY)) { // 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(set, nsec, qname, qtype); if (ndp.result) { hasValidNSEC = true; } if (ValUtils.nsecProvesNameError(set, nsec, qname)) { ce = ValUtils.closestEncloser(qname, set.getName(), nsec.getNext()); } } // 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) { logger.debug("Validating nodata: using NSEC3 records"); // try to prove NODATA with our NSEC3 record(s) if (this.n3valUtils.allNSEC3sIgnoreable(nsec3s, this.keyCache)) { response.setStatus(SecurityStatus.BOGUS, R.get("failed.nsec3_ignored")); return null; } SecurityStatus status = this.n3valUtils.proveNodata(nsec3s, qname, qtype, nsec3Signer); if (status == SecurityStatus.INSECURE) { response.setStatus(SecurityStatus.INSECURE); return null; } hasValidNSEC = status == SecurityStatus.SECURE; } if (!hasValidNSEC) { response.setBogus(R.get("failed.nodata")); logger.trace("Failed NODATA for " + qname); return null; } logger.trace("successfully validated NODATA response"); response.setStatus(SecurityStatus.SECURE); return null; }); } private CompletionStage validateNodataResponseRecursive( SMessage response, AtomicInteger setIndex) { if (setIndex.get() >= response.getSectionRRsets(Section.AUTHORITY).size()) { return completedFuture(null); } SRRset set = response.getSectionRRsets(Section.AUTHORITY).get(setIndex.getAndIncrement()); return this.prepareFindKey(set) .thenComposeAsync( ke -> { JustifiedSecStatus kve = ke.validateKeyFor(set.getSignerName()); if (kve != null) { kve.applyToResponse(response); return this.failedFuture(new Exception(kve.reason)); } SecurityStatus status = this.valUtils.verifySRRset(set, ke, this.clock.instant()); if (status != SecurityStatus.SECURE) { response.setBogus(R.get("failed.authority.nodata", set)); return this.failedFuture(new Exception("failed.authority.nodata")); } return this.validateNodataResponseRecursive(response, setIndex); }); } private CompletionStage failedFuture(Throwable e) { CompletableFuture f = new CompletableFuture<>(); f.completeExceptionally(e); return f; } /** * 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 CompletionStage validateNameErrorResponse(Message request, SMessage response) { Name intermediateQname = 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 completedFuture(null); } if (set.getType() == Type.CNAME) { intermediateQname = ((CNAMERecord) set.first()).getTarget(); } } // validate the AUTHORITY section Name qname = intermediateQname; return this.validateNameErrorResponseRecursive(response, new AtomicInteger(0)) .thenComposeAsync( v -> { // 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 nsec3s = new ArrayList<>(0); Name nsec3Signer = null; int previousClosestEncloseLabels = 0; for (SRRset set : response.getSectionRRsets(Section.AUTHORITY)) { // 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(); if (ValUtils.nsecProvesNameError(set, nsec, qname)) { hasValidNSEC = true; } Name next = nsec.getNext(); int closestEncloserLabels = ValUtils.closestEncloser(qname, set.getName(), next).labels(); if (closestEncloserLabels > previousClosestEncloseLabels || (closestEncloserLabels == previousClosestEncloseLabels && !hasValidWCNSEC)) { hasValidWCNSEC = ValUtils.nsecProvesNoWC(set, nsec, qname); } previousClosestEncloseLabels = closestEncloserLabels; } 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 completedFuture(null); } 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 completedFuture(null); } // Note that we assume that the NSEC3ValUtils proofs encompass the // wildcard part of the proof. hasValidNSEC = true; hasValidWCNSEC = true; } if (!hasValidNSEC || !hasValidWCNSEC) { boolean hasValidNSEC2 = hasValidNSEC; // Be lenient with RCODE in NSEC NameError responses return this.validateNodataResponse(request, response) .thenRun( () -> { if (response.getStatus() == SecurityStatus.SECURE) { response.getHeader().setRcode(Rcode.NOERROR); } else { // If the message fails to prove either condition, it is bogus. if (!hasValidNSEC2) { response.setBogus( R.get( "failed.nxdomain.exists", response.getQuestion().getName())); return; } response.setBogus(R.get("failed.nxdomain.haswildcard")); } }); } // Otherwise, we consider the message secure. logger.trace("successfully validated NAME ERROR response."); response.setStatus(SecurityStatus.SECURE); return completedFuture(null); }) .exceptionally(ex -> null); } private CompletionStage validateNameErrorResponseRecursive( SMessage response, AtomicInteger setIndex) { if (setIndex.get() >= response.getSectionRRsets(Section.AUTHORITY).size()) { return completedFuture(null); } SRRset set = response.getSectionRRsets(Section.AUTHORITY).get(setIndex.getAndIncrement()); return this.prepareFindKey(set) .thenCompose( ke -> { JustifiedSecStatus kve = ke.validateKeyFor(set.getSignerName()); if (kve != null) { kve.applyToResponse(response); return this.failedFuture(new Exception(kve.reason)); } SecurityStatus status = this.valUtils.verifySRRset(set, ke, this.clock.instant()); if (status != SecurityStatus.SECURE) { response.setBogus(R.get("failed.nxdomain.authority", set)); return this.failedFuture(new Exception("failed.nxdomain.authority")); } return this.validateNameErrorResponseRecursive(response, setIndex); }); } private CompletionStage 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 = request.clone(); localRequest.getHeader().setFlag(Flags.CD); return this.headResolver.sendAsync(localRequest).thenApply(SMessage::new); } private CompletionStage 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. KeyEntry ke = KeyEntry.newNullKeyEntry(state.signerName, rrset.getDClass(), DEFAULT_TA_BAD_KEY_TTL); return completedFuture(ke); } 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). return this.processFindKey(state).thenApply(v -> state.keyEntry); } return completedFuture(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 CompletionStage 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 completedFuture(null); } 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 completedFuture(null); } 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)); return this.sendRequest(dsRequest) .thenComposeAsync(dsResponse -> this.processDSResponse(dsRequest, dsResponse, state)); } // Otherwise, it is time to query for the DNSKEY Message dnskeyRequest = Message.newQuery(Record.newRecord(state.dsRRset.getName(), Type.DNSKEY, qclass)); return this.sendRequest(dnskeyRequest) .thenComposeAsync( dnskeyResponse -> 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(request, 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, this.clock.instant()); 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, this.clock.instant()); 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, this.clock.instant()); 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. List nsec3Rrsets = response.getSectionRRsets(Section.AUTHORITY, Type.NSEC3); List nsec3s = new ArrayList<>(0); Name nsec3Signer = null; long nsec3TTL = -1; if (!nsec3Rrsets.isEmpty()) { // Attempt to prove no DS with NSEC3s. for (SRRset nsec3set : nsec3Rrsets) { SecurityStatus sstatus = this.valUtils.verifySRRset(nsec3set, keyRrset, this.clock.instant()); 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: // case insecure also continues to unsigned space. // If nsec3-iter-count too high or optout, then treat below as unsigned case SECURE: KeyEntry nullKey = KeyEntry.newNullKeyEntry(qname, qclass, nsec3TTL); nullKey.setBadReason(R.get("insecure.ds.nsec3")); return nullKey; case INDETERMINATE: logger.debug("nsec3s for the referral proved no delegation."); return null; case BOGUS: bogusKE.setBadReason(R.get("failed.ds.nsec3")); return bogusKE; default: bogusKE.setBadReason(R.get("unknown.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 CompletionStage 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); if (dsKE == null) { // DS response indicated that we aren't on a delegation point. state.emptyDSName = qname; } else if (dsKE.isGood()) { state.dsRRset = dsKE; state.currentDSKeyName = new Name(dsKE.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 completedFuture(null); } return this.processFindKey(state); } private CompletionStage 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 completedFuture(null); } state.keyEntry = this.valUtils.verifyNewDNSKEYs( dnskeyRrset, state.dsRRset, DEFAULT_TA_BAD_KEY_TTL, this.clock.instant()); // If the key entry isBad or isNull, then we can move on to the next // state. if (!state.keyEntry.isGood()) { return completedFuture(null); } // The DNSKEY validated, so cache it as a trusted key rrset. this.keyCache.store(state.keyEntry); // If good, we stay in the FINDKEY state. return this.processFindKey(state); } private CompletionStage processValidate(Message request, SMessage response) { ResponseClassification subtype = ValUtils.classifyResponse(request, response); if (subtype != ResponseClassification.REFERRAL) { this.removeSpuriousAuthority(response); } CompletionStage completionStage; switch (subtype) { case POSITIVE: case CNAME: case ANY: logger.trace("Validating a positive response"); completionStage = this.validatePositiveResponse(request, response); break; case NODATA: logger.trace("Validating a nodata response"); completionStage = this.validateNodataResponse(request, response); break; case CNAME_NODATA: logger.trace("Validating a CNAME_NODATA response"); completionStage = this.validatePositiveResponse(request, response) .thenCompose( v -> { if (response.getStatus() != SecurityStatus.INSECURE) { response.setStatus(SecurityStatus.UNCHECKED); return this.validateNodataResponse(request, response); } return completedFuture(null); }); break; case NAMEERROR: logger.trace("Validating a nxdomain response"); completionStage = this.validateNameErrorResponse(request, response); break; case CNAME_NAMEERROR: logger.trace("Validating a cname_nxdomain response"); completionStage = this.validatePositiveResponse(request, response) .thenCompose( v -> { if (response.getStatus() != SecurityStatus.INSECURE) { response.setStatus(SecurityStatus.UNCHECKED); return this.validateNameErrorResponse(request, response); } return completedFuture(null); }); break; default: response.setStatus(SecurityStatus.BOGUS, R.get("validate.response.unknown", subtype)); completionStage = completedFuture(null); break; } return completionStage.thenApply(v -> 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.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 true to enable TCP, false 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) {} /** * The method is forwarded to the resolver, but always ensure that the level is 0 and the flags * contains DO. * * @param version The EDNS level to use. 0 indicates EDNS0. * @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 version, int payloadSize, int flags, List options) { if (version == -1) { throw new IllegalArgumentException("EDNS cannot be disabled"); } this.headResolver.setEDNS(version, 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); } @Override public Duration getTimeout() { return this.headResolver.getTimeout(); } @Override public void setTimeout(Duration duration) { this.headResolver.setTimeout(duration); } /** * Asynchronously sends a message and validates the response with DNSSEC before returning it. * * @param query The query to send. * @return A future that completes when the query is finished. */ public CompletionStage sendAsync(Message query) { return this.sendRequest(query) .thenCompose( response -> { 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 completedFuture(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).isEmpty()) { rrsigResponse.getHeader().unsetFlag(Flags.AD); return completedFuture(rrsigResponse); } return this.processValidate(query, response) .thenApply( validated -> { 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; }); }); } /** * 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 request. */ 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; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy