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

com.couchbase.client.core.deps.org.xbill.DNS.dnssec.ValUtils Maven / Gradle / Ivy

There is a newer version: 3.7.2
Show newest version
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2005 VeriSign. All rights reserved.
// Copyright (c) 2013-2021 Ingo Bauersachs
package com.couchbase.client.core.deps.org.xbill.DNS.dnssec;

import java.security.Security;
import java.time.Instant;
import java.util.List;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import com.couchbase.client.core.deps.org.xbill.DNS.DClass;
import com.couchbase.client.core.deps.org.xbill.DNS.DNSKEYRecord;
import com.couchbase.client.core.deps.org.xbill.DNS.DNSSEC;
import com.couchbase.client.core.deps.org.xbill.DNS.DNSSEC.Algorithm;
import com.couchbase.client.core.deps.org.xbill.DNS.DSRecord;
import com.couchbase.client.core.deps.org.xbill.DNS.ExtendedErrorCodeOption;
import com.couchbase.client.core.deps.org.xbill.DNS.Flags;
import com.couchbase.client.core.deps.org.xbill.DNS.Message;
import com.couchbase.client.core.deps.org.xbill.DNS.NSECRecord;
import com.couchbase.client.core.deps.org.xbill.DNS.Name;
import com.couchbase.client.core.deps.org.xbill.DNS.NameTooLongException;
import com.couchbase.client.core.deps.org.xbill.DNS.RRSIGRecord;
import com.couchbase.client.core.deps.org.xbill.DNS.RRset;
import com.couchbase.client.core.deps.org.xbill.DNS.Rcode;
import com.couchbase.client.core.deps.org.xbill.DNS.Record;
import com.couchbase.client.core.deps.org.xbill.DNS.Section;
import com.couchbase.client.core.deps.org.xbill.DNS.Type;

/**
 * This is a collection of routines encompassing the logic of validating different message types.
 *
 * @since 3.5
 */
@Slf4j
final class ValUtils {
  public static final String DIGEST_PREFERENCE = "dnsjava.dnssec.digest_preference";
  public static final String DIGEST_ENABLED = "dnsjava.dnssec.digest";
  public static final String DIGEST_HARDEN_DOWNGRADE = "dnsjava.dnssec.harden_algo_downgrade";
  public static final String ALGORITHM_ENABLED = "dnsjava.dnssec.algorithm";

  private static final Name WILDCARD = Name.fromConstantString("*");

  /** A local copy of the verifier object. */
  private final DnsSecVerifier verifier;

  private int[] digestPreference = null;
  private Properties config = null;
  private boolean digestHardenDowngrade = true;
  private boolean hasGost;
  private boolean hasEd25519;
  private boolean hasEd448;

  /** Creates a new instance of this class. */
  public ValUtils() {
    this.verifier = new DnsSecVerifier();
    hasGost = Security.getProviders("MessageDigest.GOST3411") != null;
    hasEd25519 = Security.getProviders("KeyFactory.Ed25519") != null;
    hasEd448 = Security.getProviders("KeyFactory.Ed448") != null;
  }

  /**
   * Set the owner name of NSEC RRsets to the canonical name, i.e. the name that is not
   * expanded from a wildcard label.
   *
   * @param set The RRset to canonicalize.
   * @param sig The signature that validated this RRset.
   */
  public static void setCanonicalNsecOwner(SRRset set, RRSIGRecord sig) {
    if (set.getType() != Type.NSEC) {
      return;
    }

    Record nsec = set.first();
    int fqdnLabelCount = nsec.getName().labels() - 1; // don't count the root label
    if (nsec.getName().isWild()) {
      --fqdnLabelCount; // don't count the wildcard label
    }

    if (sig.getLabels() == fqdnLabelCount) {
      set.setName(nsec.getName());
    } else if (sig.getLabels() < fqdnLabelCount) {
      set.setName(nsec.getName().wild(sig.getSigner().labels() - sig.getLabels()));
    } else {
      throw new IllegalArgumentException("invalid nsec record");
    }
  }

  /**
   * Initialize the module. The recognized configuration values are:
   *
   * 
    *
  • {@link #DIGEST_PREFERENCE} *
  • {@link #DIGEST_HARDEN_DOWNGRADE} *
  • {@link #DIGEST_ENABLED} *
  • {@link #ALGORITHM_ENABLED} *
* * @param config The configuration data for this module. */ public void init(Properties config) { hasGost = Security.getProviders("MessageDigest.GOST3411") != null; hasEd25519 = Security.getProviders("KeyFactory.Ed25519") != null; hasEd448 = Security.getProviders("KeyFactory.Ed448") != null; this.config = config; String dp = config.getProperty(DIGEST_PREFERENCE); if (dp != null) { String[] dpdata = dp.split(","); this.digestPreference = new int[dpdata.length]; for (int i = 0; i < dpdata.length; i++) { this.digestPreference[i] = Integer.parseInt(dpdata[i]); if (!isDigestSupported(this.digestPreference[i])) { throw new IllegalArgumentException( "Unsupported or disabled digest ID in digest preferences"); } } } this.digestHardenDowngrade = Boolean.parseBoolean(config.getProperty(DIGEST_HARDEN_DOWNGRADE)); } /** * Given a response, classify ANSWER responses into a subtype. * * @param request The original query message. * @param m The response to classify. * @return A subtype ranging from UNKNOWN to NAMEERROR. */ public static ResponseClassification classifyResponse(Message request, SMessage m) { // Normal Name Error's are easy to detect -- but don't mistake a CNAME // chain ending in NXDOMAIN. if (m.getRcode() == Rcode.NXDOMAIN && m.getCount(Section.ANSWER) == 0) { return ResponseClassification.NAMEERROR; } // check for referral: nonRD query and it looks like a nodata if (!request.getHeader().getFlag(Flags.RD) && m.getCount(Section.ANSWER) == 0 && m.getRcode() != Rcode.NOERROR) { // SOA record in auth indicates it is NODATA instead. // All validation requiring NODATA messages have SOA in // authority section. // uses fact that answer section is empty boolean sawNs = false; for (RRset set : m.getSectionRRsets(Section.AUTHORITY)) { if (set.getType() == Type.SOA) { return ResponseClassification.NODATA; } if (set.getType() == Type.DS) { return ResponseClassification.REFERRAL; } if (set.getType() == Type.NS) { sawNs = true; } } return sawNs ? ResponseClassification.REFERRAL : ResponseClassification.NODATA; } // root referral where NS set is in the answer section if (m.getSectionRRsets(Section.AUTHORITY).isEmpty() && m.getSectionRRsets(Section.ANSWER).size() == 1 && m.getRcode() == Rcode.NOERROR && m.getSectionRRsets(Section.ANSWER).get(0).getType() == Type.NS && !m.getSectionRRsets(Section.ANSWER) .get(0) .getName() .equals(request.getQuestion().getName())) { return ResponseClassification.REFERRAL; } // dump bad messages if (m.getRcode() != Rcode.NOERROR && m.getRcode() != Rcode.NXDOMAIN) { return ResponseClassification.UNKNOWN; } // Next is NODATA if (m.getRcode() == Rcode.NOERROR && m.getCount(Section.ANSWER) == 0) { return ResponseClassification.NODATA; } // We distinguish between CNAME response and other positive/negative // responses because CNAME answers require extra processing. int qtype = m.getQuestion().getType(); // We distinguish between ANY and CNAME or POSITIVE because ANY // responses are validated differently. if (qtype == Type.ANY) { return ResponseClassification.ANY; } boolean hadCname = false; for (RRset set : m.getSectionRRsets(Section.ANSWER)) { if (set.getType() == qtype) { return ResponseClassification.POSITIVE; } if (set.getType() == Type.CNAME || set.getType() == Type.DNAME) { hadCname = true; if (qtype == Type.DS) { return ResponseClassification.CNAME; } } } if (hadCname) { if (m.getRcode() == Rcode.NXDOMAIN) { return ResponseClassification.CNAME_NAMEERROR; } else { return ResponseClassification.CNAME_NODATA; } } log.warn("Failed to classify response message:\n{}", m); return ResponseClassification.UNKNOWN; } /** * Given a DS rrset and a DNSKEY rrset, match the DS to a DNSKEY and verify the DNSKEY rrset with * that key. * * @param dnskeyRrset The DNSKEY rrset to match against. The security status of this rrset will be * updated on a successful verification. * @param dsRrset The DS rrset to match with. This rrset must already be trusted. * @param badKeyTTL The TTL [s] for keys determined to be bad. * @param date The date against which to verify the rrset. * @return a KeyEntry. This will either contain the now trusted dnskey RRset, a "null" key entry * indicating that this DS rrset/DNSKEY pair indicate an secure end to the island of trust * (i.e., unknown algorithms), or a "bad" KeyEntry if the dnskey RRset fails to verify. Note * that the "null" response should generally only occur in a private algorithm scenario: * normally this sort of thing is checked before fetching the matching DNSKEY rrset. */ public KeyEntry verifyNewDNSKEYs( SRRset dnskeyRrset, SRRset dsRrset, long badKeyTTL, Instant date) { if (!atLeastOneDigestSupported(dsRrset)) { KeyEntry ke = KeyEntry.newNullKeyEntry(dsRrset.getName(), dsRrset.getDClass(), dsRrset.getTTL()); ke.setBadReason( ExtendedErrorCodeOption.UNSUPPORTED_DS_DIGEST_TYPE, R.get("failed.ds.nodigest", dsRrset.getName())); return ke; } if (!atLeastOneSupportedAlgorithm(dsRrset)) { KeyEntry ke = KeyEntry.newNullKeyEntry(dsRrset.getName(), dsRrset.getDClass(), dsRrset.getTTL()); ke.setBadReason( ExtendedErrorCodeOption.UNSUPPORTED_DNSKEY_ALGORITHM, R.get("failed.ds.noalg", dsRrset.getName())); return ke; } int favoriteDigestID = this.favoriteDSDigestID(dsRrset); KeyEntry ke = null; for (Record dsr : dsRrset.rrs()) { DSRecord ds = (DSRecord) dsr; if (this.digestHardenDowngrade && ds.getDigestID() != favoriteDigestID) { continue; } for (Record dsnkeyr : dnskeyRrset.rrs()) { DNSKEYRecord dnskey = (DNSKEYRecord) dsnkeyr; // Skip DNSKEYs that don't match the basic criteria. if (ds.getFootprint() != dnskey.getFootprint() || ds.getAlgorithm() != dnskey.getAlgorithm()) { continue; } ke = getKeyEntry(dnskeyRrset, date, ds, dnskey); if (ke.isGood()) { return ke; } // If it didn't validate with the DNSKEY, try the next one! } } // If any were understandable, then it is bad. if (ke == null) { ke = KeyEntry.newBadKeyEntry(dsRrset.getName(), dsRrset.getDClass(), badKeyTTL); ke.setBadReason(ExtendedErrorCodeOption.DNSKEY_MISSING, R.get("dnskey.no_ds_match")); } return ke; } private KeyEntry getKeyEntry(SRRset dnskeyRrset, Instant date, DSRecord ds, DNSKEYRecord dnskey) { // Convert the candidate DNSKEY into a hash using the same DS // hash algorithm. DSRecord keyDigest = new DSRecord(Name.root, ds.getDClass(), 0, ds.getDigestID(), dnskey); byte[] keyHash = keyDigest.getDigest(); byte[] dsHash = ds.getDigest(); // see if there is a length mismatch (unlikely) KeyEntry ke; if (keyHash.length != dsHash.length) { ke = KeyEntry.newBadKeyEntry(ds.getName(), ds.getDClass(), ds.getTTL()); ke.setBadReason(ExtendedErrorCodeOption.DNSSEC_BOGUS, R.get("dnskey.invalid")); return ke; } for (int k = 0; k < keyHash.length; k++) { if (keyHash[k] != dsHash[k]) { ke = KeyEntry.newBadKeyEntry(ds.getName(), ds.getDClass(), ds.getTTL()); ke.setBadReason(ExtendedErrorCodeOption.DNSSEC_BOGUS, R.get("dnskey.invalid")); return ke; } } // Otherwise, we have a match! Make sure that the DNSKEY // verifies *with this key*. JustifiedSecStatus res = this.verifier.verify(dnskeyRrset, dnskey, date); switch (res.status) { case SECURE: dnskeyRrset.setSecurityStatus(SecurityStatus.SECURE); ke = KeyEntry.newKeyEntry(dnskeyRrset); break; case BOGUS: ke = KeyEntry.newBadKeyEntry(ds.getName(), ds.getDClass(), ds.getTTL()); ke.setBadReason(res.edeReason, res.reason); break; default: throw new IllegalStateException("Unexpected security status"); } return ke; } /** * Gets the digest ID for the favorite (best) algorithm that is support in a given DS set. * *

The order of preference can be configured with the property {@value #DIGEST_PREFERENCE}. If * the property is not set, the highest supported number is returned. * * @param dsset The DS set to check for the favorite algorithm. * @return The favorite digest ID or 0 if none is supported. 0 is not a known digest ID. */ int favoriteDSDigestID(SRRset dsset) { if (this.digestPreference == null) { int max = 0; for (Record r : dsset.rrs()) { DSRecord ds = (DSRecord) r; if (ds.getDigestID() > max && isDigestSupported(ds.getDigestID()) && isAlgorithmSupported(ds.getAlgorithm())) { max = ds.getDigestID(); } } return max; } else { for (int preference : this.digestPreference) { for (Record r : dsset.rrs()) { DSRecord ds = (DSRecord) r; if (ds.getDigestID() == preference) { return ds.getDigestID(); } } } } return 0; } /** * Given an SRRset that is signed by a DNSKEY found in the key_rrset, verify it. This will return * the status (either BOGUS or SECURE) and set that status in rrset. * * @param rrset The SRRset to verify. * @param keyRrset The set of keys to verify against. * @param date The date against which to verify the rrset. * @return The status (BOGUS or SECURE). */ public JustifiedSecStatus verifySRRset(SRRset rrset, SRRset keyRrset, Instant date) { if (rrset.getSecurityStatus() == SecurityStatus.SECURE) { log.trace( "RRset <{}/{}/{}> previously found to be SECURE", rrset.getName(), Type.string(rrset.getType()), DClass.string(rrset.getDClass())); return new JustifiedSecStatus(SecurityStatus.SECURE, -1, null); } JustifiedSecStatus res = this.verifier.verify(rrset, keyRrset, date); rrset.setSecurityStatus(res.status); return res; } /** * Determine by looking at a signed RRset whether or not the RRset name was the result of a * wildcard expansion. If so, return the name of the generating wildcard. * * @param rrset The rrset to chedck. * @return the wildcard name, if the rrset was synthesized from a wildcard. null if not. */ public static Name rrsetWildcard(RRset rrset) { List sigs = rrset.sigs(); RRSIGRecord firstSig = sigs.get(0); // check rest of signatures have identical label count for (int i = 1; i < sigs.size(); i++) { if (sigs.get(i).getLabels() != firstSig.getLabels()) { throw new IllegalArgumentException("failed.wildcard.label_count_mismatch"); } } // if the RRSIG label count is shorter than the number of actual labels, // then this rrset was synthesized from a wildcard. // Note that the RRSIG label count doesn't count the root label. Name wn = rrset.getName(); // skip a leading wildcard label in the dname (RFC4035 2.2) if (rrset.getName().isWild()) { wn = new Name(wn, 1); } int labelDiff = (wn.labels() - 1) - firstSig.getLabels(); if (labelDiff > 0) { return wn.wild(labelDiff); } return null; } /** * Finds the longest domain name in common with the given name. * * @param domain1 The first domain to process. * @param domain2 The second domain to process. * @return The longest label in common of domain1 and domain2. The least common name is the root. */ public static Name longestCommonName(Name domain1, Name domain2) { int l = Math.min(domain1.labels(), domain2.labels()); domain1 = new Name(domain1, domain1.labels() - l); domain2 = new Name(domain2, domain2.labels() - l); for (int i = 0; i < l - 1; i++) { Name ns1 = new Name(domain1, i); if (ns1.equals(new Name(domain2, i))) { return ns1; } } return Name.root; } /** * Is the first Name strictly a subdomain of the second name (i.e., below but not equal to). * * @param domain1 The first domain to process. * @param domain2 The second domain to process. * @return True when domain1 is a strict subdomain of domain2. */ public static boolean strictSubdomain(Name domain1, Name domain2) { if (domain1.labels() <= domain2.labels()) { return false; } return new Name(domain1, domain1.labels() - domain2.labels()).equals(domain2); } /** * Determines the 'closest encloser' - the name that has the most common labels between * domain and ({@link NSECRecord#getName()} or {@link NSECRecord#getNext()}). * * @param domain The name for which the closest encloser is queried. * @param owner The beginning of the covering {@link Name} to check. * @param next The end of the covering {@link Name} to check. * @return The closest encloser name of domain as defined by {@code owner} and {@code * next}. */ public static Name closestEncloser(Name domain, Name owner, Name next) { Name n1 = longestCommonName(domain, owner); Name n2 = longestCommonName(domain, next); return (n1.labels() > n2.labels()) ? n1 : n2; } /** * Gets the closest encloser of domain prepended with a wildcard label. * * @param domain The name for which the wildcard closest encloser is demanded. * @param set The RRset containing {@code nsec} to check. * @param nsec The covering NSEC that defines the encloser. * @return The wildcard closest encloser name of domain as defined by nsec * . * @throws NameTooLongException If adding the wildcard label to the closest encloser results in an * invalid name. */ public static Name nsecWildcard(Name domain, SRRset set, NSECRecord nsec) throws NameTooLongException { Name origin = closestEncloser(domain, set.getName(), nsec.getNext()); return Name.concatenate(WILDCARD, origin); } /** * Determine if the given NSEC proves a NameError (NXDOMAIN) for a given qname. * * @param set The RRset that contains the NSEC. * @param nsec The NSEC to check. * @param qname The qname to check against. * @return true if the NSEC proves the condition. */ public static boolean nsecProvesNameError(SRRset set, NSECRecord nsec, Name qname) { Name owner = set.getName(); Name next = nsec.getNext(); // If NSEC owner == qname, then this NSEC proves that qname exists. if (qname.equals(owner)) { return false; } // deny overreaching NSECs if (!next.subdomain(set.getSignerName())) { return false; } // If NSEC is a parent of qname, we need to check the type map // If the parent name has a DNAME or is a delegation point, then this // NSEC is being misused. if (qname.subdomain(owner)) { if (nsec.hasType(Type.DNAME)) { return false; } if (nsec.hasType(Type.NS) && !nsec.hasType(Type.SOA)) { return false; } } if (owner.equals(next)) { // this nsec is the only nsec: zone.name NSEC zone.name // it disproves everything else but only for subdomains of that zone return strictSubdomain(qname, next); } else if (owner.compareTo(next) > 0) { // this is the last nsec, ....(bigger) NSEC zonename(smaller) // the names after the last (owner) name do not exist // there are no names before the zone name in the zone // but the qname must be a subdomain of the zone name(next). return owner.compareTo(qname) < 0 && strictSubdomain(qname, next); } else { // regular NSEC, (smaller) NSEC (larger) return owner.compareTo(qname) < 0 && qname.compareTo(next) < 0; } } /** * Determine if a NSEC record proves the non-existence of a wildcard that could have produced * qname. * * @param set The RRset of the NSEC record. * @param nsec The nsec record to check. * @param qname The qname to check against. * @return true if the NSEC proves the condition. */ public static boolean nsecProvesNoWC(SRRset set, NSECRecord nsec, Name qname) { Name ce = closestEncloser(qname, set.getName(), nsec.getNext()); int labelsToStrip = qname.labels() - ce.labels(); if (labelsToStrip > 0) { Name wcName = qname.wild(labelsToStrip); return nsecProvesNameError(set, nsec, wcName); } return false; } /** * Container for responses of {@link ValUtils#nsecProvesNodata(SRRset, NSECRecord, Name, int)}. */ public static class NsecProvesNodataResponse { boolean result; Name wc; } /** * Determine if a NSEC proves the NOERROR/NODATA conditions. This will also handle the empty * non-terminal (ENT) case and partially handle the wildcard case. If the ownername of 'nsec' is a * wildcard, the validator must still be provided proof that qname did not directly exist and that * the wildcard is, in fact, *.closest_encloser. * * @param set The RRset of the NSEC record. * @param nsec The NSEC to check * @param qname The query name to check against. * @param qtype The query type to check against. * @return true if the NSEC proves the condition. */ public static NsecProvesNodataResponse nsecProvesNodata( SRRset set, NSECRecord nsec, Name qname, int qtype) { NsecProvesNodataResponse result = new NsecProvesNodataResponse(); if (!set.getName().equals(qname)) { // empty-non-terminal checking. // Done before wildcard, because this is an exact match, // and would prevent a wildcard from matching. // If the nsec is proving that qname is an ENT, the nsec owner will // be less than qname, and the next name will be a child domain of // the qname. if (strictSubdomain(nsec.getNext(), qname) && set.getName().compareTo(qname) < 0) { result.result = true; return result; } // Wildcard checking: // If this is a wildcard NSEC, make sure that a) it was possible to // have generated qname from the wildcard and b) the type map does // not contain qtype. Note that this does NOT prove that this // wildcard was the applicable wildcard. if (set.getName().isWild()) { // the is the purported closest encloser. Name ce = new Name(set.getName(), 1); // The qname must be a strict subdomain of the closest encloser, // and the qtype must be absent from the type map. if (strictSubdomain(qname, ce)) { if (nsec.hasType(Type.CNAME)) { // should have gotten the wildcard CNAME log.debug("NSEC proofed wildcard CNAME"); result.result = false; return result; } if (nsec.hasType(Type.NS) && !nsec.hasType(Type.SOA)) { // wrong parentside (wildcard) NSEC used, and it really // should not exist anyway: // http://tools.ietf.org/html/rfc4592#section-4.2 log.debug("Wrong parent (wildcard) NSEC used"); result.result = false; return result; } if (nsec.hasType(qtype)) { log.debug("NSEC proofed that {} exists", Type.string(qtype)); result.result = false; return result; } } result.wc = ce; result.result = true; return result; } // Otherwise, this NSEC does not prove ENT, so it does not prove // NODATA. result.result = false; return result; } // If the qtype exists, then we should have gotten it. if (nsec.hasType(qtype)) { log.debug("NSEC proofed that {} exists", Type.string(qtype)); result.result = false; return result; } // if the name is a CNAME node, then we should have gotten the CNAME if (nsec.hasType(Type.CNAME)) { log.debug("NSEC proofed CNAME"); result.result = false; return result; } // If an NS set exists at this name, and NOT a SOA (so this is a zone // cut, not a zone apex), then we should have gotten a referral (or we // just got the wrong NSEC). // The reverse of this check is used when qtype is DS, since that // must use the NSEC from above the zone cut. if (qtype != Type.DS && nsec.hasType(Type.NS) && !nsec.hasType(Type.SOA)) { log.debug("NSEC proofed missing referral"); result.result = false; return result; } if (qtype == Type.DS && nsec.hasType(Type.SOA) && !Name.root.equals(qname)) { log.debug("NSEC from wrong zone"); result.result = false; return result; } result.result = true; return result; } /** * Check DS absence. There is a NODATA reply to a DS that needs checking. NSECs can prove this is * not a delegation point, or successfully prove that there is no DS. Or this fails. * * @param request The request that generated this response. * @param response The response to validate. * @param keyRrset The key that validate the NSECs. * @param date The date against which to verify the response. * @return The NODATA proof along with the reason of the result. */ public JustifiedSecStatus nsecProvesNodataDsReply( Message request, SMessage response, SRRset keyRrset, Instant date) { Name qname = request.getQuestion().getName(); int qclass = request.getQuestion().getDClass(); // If we have a NSEC at the same name, it must prove one of two things: // 1) this is a delegation point and there is no DS // 2) this is not a delegation point SRRset nsecRrset = response.findRRset(qname, Type.NSEC, qclass, Section.AUTHORITY); if (nsecRrset != null) { // The NSEC must verify, first of all. JustifiedSecStatus res = this.verifySRRset(nsecRrset, keyRrset, date); if (res.status != SecurityStatus.SECURE) { return new JustifiedSecStatus( SecurityStatus.BOGUS, ExtendedErrorCodeOption.DNSSEC_BOGUS, R.get("failed.ds.nsec", res.reason)); } NSECRecord nsec = (NSECRecord) nsecRrset.first(); SecurityStatus status = ValUtils.nsecProvesNoDS(nsec, qname); switch (status) { case INSECURE: // this wasn't a delegation point. return new JustifiedSecStatus(status, -1, R.get("failed.ds.nodelegation")); case SECURE: // this proved no DS. return new JustifiedSecStatus(status, -1, R.get("insecure.ds.nsec")); default: // something was wrong. return new JustifiedSecStatus( status, ExtendedErrorCodeOption.DNSSEC_BOGUS, R.get("failed.ds.nsec.hasdata")); } } // Otherwise, there is no NSEC at qname. This could be an ENT. // If not, this is broken. NsecProvesNodataResponse ndp = new NsecProvesNodataResponse(); Name ce = null; boolean hasValidNSEC = false; NSECRecord wcNsec = null; for (SRRset set : response.getSectionRRsets(Section.AUTHORITY, Type.NSEC)) { JustifiedSecStatus res = this.verifySRRset(set, keyRrset, date); if (res.status != SecurityStatus.SECURE) { return new JustifiedSecStatus(res.status, res.edeReason, R.get("failed.ds.nsec.ent")); } NSECRecord nsec = (NSECRecord) set.rrs().get(0); ndp = ValUtils.nsecProvesNodata(set, nsec, qname, Type.DS); if (ndp.result) { hasValidNSEC = true; if (ndp.wc != null && nsec.getName().isWild()) { wcNsec = nsec; } } if (ValUtils.nsecProvesNameError(set, nsec, qname)) { ce = closestEncloser(qname, set.getName(), nsec.getNext()); } } // 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))) { hasValidNSEC = false; } if (hasValidNSEC) { if (ndp.wc != null) { SecurityStatus status = nsecProvesNoDS(wcNsec, qname); return new JustifiedSecStatus( status, ExtendedErrorCodeOption.NSEC_MISSING, R.get("failed.ds.nowildcardproof")); } return new JustifiedSecStatus(SecurityStatus.INSECURE, -1, R.get("insecure.ds.nsec.ent")); } return new JustifiedSecStatus( SecurityStatus.UNCHECKED, ExtendedErrorCodeOption.DNSSEC_INDETERMINATE, R.get("failed.ds.nonconclusive")); } /** * Checks if the authority section of a message contains at least one signed NSEC or NSEC3 record. * * @param message The message to inspect. * @return True if at least one record is found, false otherwise. */ public boolean hasSignedNsecs(SMessage message) { for (SRRset set : message.getSectionRRsets(Section.AUTHORITY)) { if ((set.getType() == Type.NSEC || set.getType() == Type.NSEC3) && !set.sigs().isEmpty()) { return true; } } return false; } /** * Determines whether the given {@link NSECRecord} proves that there is no {@link DSRecord} for * qname. * * @param nsec The NSEC that should prove the non-existence. * @param qname The name for which the prove is made. * @return {@link SecurityStatus#BOGUS} when the NSEC is from the child domain or indicates that * there indeed is a DS record, {@link SecurityStatus#INSECURE} when there is not even a prove * for a NS record, {@link SecurityStatus#SECURE} when there is no DS record. */ public static SecurityStatus nsecProvesNoDS(NSECRecord nsec, Name qname) { // Could check to make sure the qname is a subdomain of nsec if ((nsec.hasType(Type.SOA) && !Name.root.equals(qname)) || nsec.hasType(Type.DS)) { // SOA present means that this is the NSEC from the child, not the // parent (so it is the wrong one) -> cannot happen because the // keyset is always from the parent zone and doesn't validate the // NSEC // DS present means that there should have been a positive response // to the DS query, so there is something wrong. return SecurityStatus.BOGUS; } if (!nsec.hasType(Type.NS)) { // If there is no NS at this point at all, then this doesn't prove // anything one way or the other. return SecurityStatus.INSECURE; } // Otherwise, this proves no DS. return SecurityStatus.SECURE; } /** * Determines if at least one of the DS records in the RRset has a supported algorithm. * * @param dsRRset The RR set to search in. * @return True when at least one DS record uses a supported algorithm, false otherwise. */ boolean atLeastOneSupportedAlgorithm(RRset dsRRset) { for (Record r : dsRRset.rrs()) { if (isAlgorithmSupported(((DSRecord) r).getAlgorithm())) { return true; } // do nothing, there could be another DS we understand } return false; } /** * Determines if the algorithm is supported. * * @param alg The algorithm to check. * @return True when the algorithm is supported, false otherwise. */ boolean isAlgorithmSupported(int alg) { String configKey = ALGORITHM_ENABLED + "." + alg; switch (alg) { case Algorithm.RSAMD5: return false; // obsoleted by rfc6725 case Algorithm.DSA: case Algorithm.DSA_NSEC3_SHA1: if (config == null) { return false; } return Boolean.parseBoolean(config.getProperty(configKey, Boolean.FALSE.toString())); case Algorithm.RSASHA1: case Algorithm.RSA_NSEC3_SHA1: case Algorithm.RSASHA256: case Algorithm.RSASHA512: case Algorithm.ECDSAP256SHA256: case Algorithm.ECDSAP384SHA384: return propertyOrTrueWithPrecondition(configKey, true); case Algorithm.ECC_GOST: return propertyOrTrueWithPrecondition(configKey, hasGost); case Algorithm.ED25519: return propertyOrTrueWithPrecondition(configKey, hasEd25519); case Algorithm.ED448: return propertyOrTrueWithPrecondition(configKey, hasEd448); default: return false; } } /** * Determines if at least one of the DS records in the RRset has a supported digest algorithm. * * @param dsRRset The RR set to search in. * @return True when at least one DS record uses a supported digest algorithm, false otherwise. */ boolean atLeastOneDigestSupported(RRset dsRRset) { for (Record r : dsRRset.rrs()) { if (isDigestSupported(((DSRecord) r).getDigestID())) { return true; } // do nothing, there could be another DS we understand } return false; } /** * Determines if the digest algorithm is supported. * * @param digestID the algorithm to check. * @return True when the digest algorithm is supported, false otherwise. */ boolean isDigestSupported(int digestID) { String configKey = DIGEST_ENABLED + "." + digestID; switch (digestID) { case DNSSEC.Digest.SHA1: case DNSSEC.Digest.SHA256: case DNSSEC.Digest.SHA384: if (config == null) { return true; } return Boolean.parseBoolean(config.getProperty(configKey, Boolean.TRUE.toString())); case DNSSEC.Digest.GOST3411: return propertyOrTrueWithPrecondition(configKey, hasGost); default: return false; } } private boolean propertyOrTrueWithPrecondition(String configKey, boolean precondition) { if (!precondition) { return false; } if (config == null) { return true; } return Boolean.parseBoolean(config.getProperty(configKey, Boolean.TRUE.toString())); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy