/*
 * 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.util.Iterator;
import java.util.Properties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.jitsi.dnssec.R;
import org.jitsi.dnssec.SMessage;
import org.jitsi.dnssec.SRRset;
import org.jitsi.dnssec.SecurityStatus;
import org.xbill.DNS.DClass;
import org.xbill.DNS.DNSKEYRecord;
import org.xbill.DNS.DNSSEC.Algorithm;
import org.xbill.DNS.DSRecord;
import org.xbill.DNS.DSRecord.Digest;
import org.xbill.DNS.Message;
import org.xbill.DNS.NSECRecord;
import org.xbill.DNS.Name;
import org.xbill.DNS.NameTooLongException;
import org.xbill.DNS.RRSIGRecord;
import org.xbill.DNS.RRset;
import org.xbill.DNS.Rcode;
import org.xbill.DNS.Record;
import org.xbill.DNS.Section;
import org.xbill.DNS.Type;

/**
 * This is a collection of routines encompassing the logic of validating
 * different message types.
 * 
 * @author davidb
 */
public class ValUtils {
    public static final String DIGEST_PREFERENCE = "org.jitsi.dnssec.digest_preference";

    private static final Logger logger = LoggerFactory.getLogger(ValUtils.class);
    private static final Name WILDCARD = Name.fromConstantString("*");

    /** A local copy of the verifier object. */
    private DnsSecVerifier verifier;
    private int[] digestPreference = null;

    /**
     * Creates a new instance of this class.
     */
    public ValUtils() {
        this.verifier = new DnsSecVerifier();
    }

    /**
     * Initialize the module. The only recognized configuration value is
     * {@link #DIGEST_PREFERENCE}.
     * 
     * @param config The configuration data for this module.
     */
    public void init(Properties 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 digest ID in digest preferences");
                }
            }
        }
    }

    /**
     * Given a response, classify ANSWER responses into a subtype.
     * 
     * @param m The response to classify.
     * 
     * @return A subtype ranging from UNKNOWN to NAMEERROR.
     */
    public static ResponseClassification classifyResponse(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;
        }

        // Next is NODATA
        if (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;
            }
        }

        logger.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.
     * 
     * @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) {
        if (!atLeastOneDigestSupported(dsRrset)) {
            KeyEntry ke = KeyEntry.newNullKeyEntry(dsRrset.getName(), dsRrset.getDClass(), dsRrset.getTTL());
            ke.setBadReason(R.get("failed.ds.nodigest", dsRrset.getName()));
            return ke;
        }

        if (!atLeastOneSupportedAlgorithm(dsRrset)) {
            KeyEntry ke = KeyEntry.newNullKeyEntry(dsRrset.getName(), dsRrset.getDClass(), dsRrset.getTTL());
            ke.setBadReason(R.get("failed.ds.noalg", dsRrset.getName()));
            return ke;
        }

        int favoriteDigestID = this.favoriteDSDigestID(dsRrset);
        for (Iterator<?> i = dsRrset.rrs(); i.hasNext();) {
            DSRecord ds = (DSRecord)i.next();
            if (ds.getDigestID() != favoriteDigestID) {
                continue;
            }

            DNSKEY: for (Iterator<?> j = dnskeyRrset.rrs(); j.hasNext();) {
                DNSKEYRecord dnskey = (DNSKEYRecord)j.next();

                // Skip DNSKEYs that don't match the basic criteria.
                if (ds.getFootprint() != dnskey.getFootprint() || ds.getAlgorithm() != dnskey.getAlgorithm()) {
                    continue;
                }

                // 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)
                if (keyHash.length != dsHash.length) {
                    continue;
                }

                for (int k = 0; k < keyHash.length; k++) {
                    if (keyHash[k] != dsHash[k]) {
                        continue DNSKEY;
                    }
                }

                // Otherwise, we have a match! Make sure that the DNSKEY
                // verifies *with this key*.
                SecurityStatus res = this.verifier.verify(dnskeyRrset, dnskey);
                if (res == SecurityStatus.SECURE) {
                    logger.trace("DS matched DNSKEY.");
                    dnskeyRrset.setSecurityStatus(SecurityStatus.SECURE);
                    return KeyEntry.newKeyEntry(dnskeyRrset);
                }

                // If it didn't validate with the DNSKEY, try the next one!
            }
        }

        // If any were understandable, then it is bad.
        KeyEntry badKey = KeyEntry.newBadKeyEntry(dsRrset.getName(), dsRrset.getDClass(), badKeyTTL);
        badKey.setBadReason(R.get("dnskey.no_ds_match"));
        return badKey;
    }

    /**
     * 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 (Iterator<?> rrs = dsset.rrs(); rrs.hasNext();) {
                DSRecord r = (DSRecord)rrs.next();
                if (r.getDigestID() > max && isDigestSupported(r.getDigestID()) && isAlgorithmSupported(r.getAlgorithm())) {
                    max = r.getDigestID();
                }
            }

            return max;
        }
        else {
            for (int i = 0; i < this.digestPreference.length; i++) {
                for (Iterator<?> rrs = dsset.rrs(); rrs.hasNext();) {
                    DSRecord r = (DSRecord)rrs.next();
                    if (r.getDigestID() == this.digestPreference[i]) {
                        return r.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.
     * @return The status (BOGUS or SECURE).
     */
    public SecurityStatus verifySRRset(SRRset rrset, SRRset keyRrset) {
        String rrsetName = rrset.getName() + "/" + Type.string(rrset.getType()) + "/" + DClass.string(rrset.getDClass());

        if (rrset.getSecurityStatus() == SecurityStatus.SECURE) {
            logger.trace("verifySRRset: rrset <" + rrsetName + "> previously found to be SECURE");
            return SecurityStatus.SECURE;
        }

        SecurityStatus status = this.verifier.verify(rrset, keyRrset);
        if (status != SecurityStatus.SECURE) {
            logger.debug("verifySRRset: rrset <" + rrsetName + "> found to be BAD");
            status = SecurityStatus.BOGUS;
        }
        else {
            logger.trace("verifySRRset: rrset <" + rrsetName + "> found to be SECURE");
        }

        rrset.setSecurityStatus(status);
        return status;
    }

    /**
     * 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) {
        @SuppressWarnings("unchecked")
        Iterator<RRSIGRecord> it = (Iterator<RRSIGRecord>)rrset.sigs();
        RRSIGRecord rrsig = it.next();

        // check rest of signatures have identical label count
        while (it.hasNext()) {
            if (it.next().getLabels() != rrsig.getLabels()) {
                throw new RuntimeException("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) - rrsig.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 <code>domain</code> and ({@link NSECRecord#getName()} or
     * {@link NSECRecord#getNext()}).
     * 
     * @param domain The name for which the closest encloser is queried.
     * @param nsec The covering {@link NSECRecord} to check.
     * @return The closest encloser name of <code>domain</code> as defined by
     *         <code>nsec</code>.
     */
    public static Name closestEncloser(Name domain, NSECRecord nsec) {
        Name n1 = longestCommonName(domain, nsec.getName());
        Name n2 = longestCommonName(domain, nsec.getNext());

        return (n1.labels() > n2.labels()) ? n1 : n2;
    }

    /**
     * Gets the closest encloser of <code>domain</code> prepended with a
     * wildcard label.
     * 
     * @param domain The name for which the wildcard closest encloser is
     *            demanded.
     * @param nsec The covering NSEC that defines the encloser.
     * @return The wildcard closest encloser name of <code>domain</code> as
     *         defined by <code>nsec</code>.
     * @throws NameTooLongException If adding the wildcard label to the closest
     *             encloser results in an invalid name.
     */
    public static Name nsecWildcard(Name domain, NSECRecord nsec) throws NameTooLongException {
        Name origin = closestEncloser(domain, nsec);
        return Name.concatenate(WILDCARD, origin);
    }

    /**
     * Determine if the given NSEC proves a NameError (NXDOMAIN) for a given
     * qname.
     * 
     * @param nsec The NSEC to check.
     * @param qname The qname to check against.
     * @param signerName The signer of the NSEC RRset.
     * @return true if the NSEC proves the condition.
     */
    public static boolean nsecProvesNameError(NSECRecord nsec, Name qname, Name signerName) {
        Name owner = nsec.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(signerName)) {
            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
            if (strictSubdomain(qname, next)) {
                return true;
            }
        }
        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).
            if (owner.compareTo(qname) < 0 && strictSubdomain(qname, next)) {
                return true;
            }
        }
        else {
            // regular NSEC, (smaller) NSEC (larger)
            if (owner.compareTo(qname) < 0 && qname.compareTo(next) < 0) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determine if a NSEC record proves the non-existence of a wildcard that
     * could have produced qname.
     * 
     * @param nsec The nsec to check.
     * @param qname The qname to check against.
     * @param signerName The signer of the NSEC RRset.
     * @return true if the NSEC proves the condition.
     */
    public static boolean nsecProvesNoWC(NSECRecord nsec, Name qname, Name signerName) {
        int qnameLabels = qname.labels();
        Name ce = closestEncloser(qname, nsec);
        int ceLabels = ce.labels();

        for (int i = qnameLabels - ceLabels; i > 0; i--) {
            Name wcName = qname.wild(i);
            if (nsecProvesNameError(nsec, wcName, signerName)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Container for responses of
     * {@link ValUtils#nsecProvesNodata(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 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(NSECRecord nsec, Name qname, int qtype) {
        NsecProvesNodataResponse result = new NsecProvesNodataResponse();
        if (!nsec.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) && nsec.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 (nsec.getName().isWild()) {
                // the is the purported closest encloser.
                Name ce = new Name(nsec.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
                        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
                        result.result = false;
                        return result;
                    }

                    if (nsec.hasType(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)) {
            result.result = false;
            return result;
        }

        // if the name is a CNAME node, then we should have gotten the CNAME
        if (nsec.hasType(Type.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)) {
            result.result = false;
            return result;
        }
        else if (qtype == Type.DS && nsec.hasType(Type.SOA) && !Name.root.equals(qname)) {
            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.
     * @return The NODATA proof along with the reason of the result.
     */
    public JustifiedSecStatus nsecProvesNodataDsReply(Message request, SMessage response, SRRset keyRrset) {
        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.
            SecurityStatus status = this.verifySRRset(nsecRrset, keyRrset);
            if (status != SecurityStatus.SECURE) {
                return new JustifiedSecStatus(SecurityStatus.BOGUS, R.get("failed.ds.nsec"));
            }

            NSECRecord nsec = (NSECRecord)nsecRrset.first();
            status = ValUtils.nsecProvesNoDS(nsec, qname);
            switch (status) {
                case INSECURE: // this wasn't a delegation point.
                    return new JustifiedSecStatus(status, R.get("failed.ds.nodelegation"));
                case SECURE: // this proved no DS.
                    return new JustifiedSecStatus(status, R.get("insecure.ds.nsec"));
                default: // something was wrong.
                    return new JustifiedSecStatus(status, 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)) {
            SecurityStatus status = this.verifySRRset(set, keyRrset);
            if (status != SecurityStatus.SECURE) {
                return new JustifiedSecStatus(status, R.get("failed.ds.nsec.ent"));
            }

            NSECRecord nsec = (NSECRecord)set.first();
            ndp = ValUtils.nsecProvesNodata(nsec, qname, Type.DS);
            if (ndp.result) {
                hasValidNSEC = true;
                if (ndp.wc != null && nsec.getName().isWild()) {
                    wcNsec = nsec;
                }
            }

            if (ValUtils.nsecProvesNameError(nsec, qname, set.getSignerName())) {
                ce = closestEncloser(qname, nsec);
            }
        }

        // 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, R.get("failed.ds.nowildcardproof"));
            }

            return new JustifiedSecStatus(SecurityStatus.INSECURE, R.get("insecure.ds.nsec.ent"));
        }

        return new JustifiedSecStatus(SecurityStatus.UNCHECKED, 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) {
                if (set.sigs().hasNext()) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Determines whether the given {@link NSECRecord} proves that there is no
     * {@link DSRecord} for <code>qname</code>.
     * 
     * @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.
     */
    static boolean atLeastOneSupportedAlgorithm(RRset dsRRset) {
        Iterator<?> it = dsRRset.rrs();
        while (it.hasNext()) {
            Record r = (Record)it.next();
            if (r.getType() == Type.DS) {
                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.
     */
    static boolean isAlgorithmSupported(int alg) {
        switch (alg) {
            case Algorithm.RSAMD5:
                return false; // obsoleted by rfc6944
            case Algorithm.DSA:
            case Algorithm.DSA_NSEC3_SHA1:
            case Algorithm.RSASHA1:
            case Algorithm.RSA_NSEC3_SHA1:
            case Algorithm.RSASHA256:
            case Algorithm.RSASHA512:
            case Algorithm.ECDSAP256SHA256:
            case Algorithm.ECDSAP384SHA384:
                return true;
            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.
     */
    static boolean atLeastOneDigestSupported(RRset dsRRset) {
        Iterator<?> it = dsRRset.rrs();
        while (it.hasNext()) {
            Record r = (Record)it.next();
            if (r.getType() == Type.DS) {
                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.
     */
    static boolean isDigestSupported(int digestID) {
        switch (digestID) {
            case Digest.SHA1:
            case Digest.SHA256:
            case Digest.SHA384:
                return true;
            default:
                return false;
        }
    }
}
