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

import org.jitsi.dnssec.SecurityStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.DNSKEYRecord;
import org.xbill.DNS.DNSSEC;
import org.xbill.DNS.DNSSEC.DNSSECException;
import org.xbill.DNS.RRSIGRecord;
import org.xbill.DNS.RRset;

/**
 * A class for performing basic DNSSEC verification. The DNSJAVA package
 * contains a similar class. This is a reimplementation that allows us to have
 * finer control over the validation process.
 * 
 * @author davidb
 * @version $Revision: 361 $
 */
public class DnsSecVerifier {
    private static final Logger logger = LoggerFactory.getLogger(DnsSecVerifier.class);

    /**
     * Find the matching DNSKEY(s) to an RRSIG within a DNSKEY rrset. Normally
     * this will only return one DNSKEY. It can return more than one, since
     * KeyID/Footprints are not guaranteed to be unique.
     * 
     * @param dnskeyRrset The DNSKEY rrset to search.
     * @param signature The RRSIG to match against.
     * @return A List contains a one or more DNSKEYRecord objects, or null if a
     *         matching DNSKEY could not be found.
     */
    private List<DNSKEYRecord> findKey(RRset dnskeyRrset, RRSIGRecord signature) {
        if (!signature.getSigner().equals(dnskeyRrset.getName())) {
            logger.trace("findKey: could not find appropriate key because incorrect keyset was supplied. Wanted: "
                    + signature.getSigner() + ", got: " + dnskeyRrset.getName());
            return null;
        }

        int keyid = signature.getFootprint();
        int alg = signature.getAlgorithm();
        List<DNSKEYRecord> res = new ArrayList<DNSKEYRecord>(dnskeyRrset.size());
        for (Iterator<?> i = dnskeyRrset.rrs(); i.hasNext();) {
            DNSKEYRecord r = (DNSKEYRecord)i.next();
            if (r.getAlgorithm() == alg && r.getFootprint() == keyid) {
                res.add(r);
            }
        }

        if (res.size() == 0) {
            logger.trace("findKey: could not find a key matching the algorithm and footprint in supplied keyset. ");
            return null;
        }

        return res;
    }

    /**
     * Verify an RRset against a particular signature.
     * 
     * @param rrset The RRset to verify.
     * @param sigrec The signature record that signs the RRset.
     * @param keyRrset The keys used to create the signature record.
     * 
     * @return {@link SecurityStatus#SECURE} if the signature verified,
     *         {@link SecurityStatus#BOGUS} if it did not verify (for any
     *         reason), and {@link SecurityStatus#UNCHECKED} if verification
     *         could not be completed (usually because the public key was not
     *         available).
     */
    private SecurityStatus verifySignature(RRset rrset, RRSIGRecord sigrec,
            RRset keyRrset) {
        List<DNSKEYRecord> keys = this.findKey(keyRrset, sigrec);
        if (keys == null) {
            logger.trace("could not find appropriate key");
            return SecurityStatus.BOGUS;
        }

        SecurityStatus status = SecurityStatus.UNCHECKED;
        for (DNSKEYRecord key : keys) {
            try {
                if (!rrset.getName().subdomain(keyRrset.getName())) {
                    logger.debug("signer name is off-tree");
                    status = SecurityStatus.BOGUS;
                    continue;
                }

                DNSSEC.verify(rrset, sigrec, key);
                return SecurityStatus.SECURE;
            }
            catch (DNSSECException e) {
                logger.error("Failed to validate RRset", e);
                status = SecurityStatus.BOGUS;
            }
        }

        return status;
    }

    /**
     * Verifies an RRset. This routine does not modify the RRset. This RRset is
     * presumed to be verifiable, and the correct DNSKEY rrset is presumed to
     * have been found.
     * 
     * @param rrset The RRset to verify.
     * @param keyRrset The keys to verify the signatures in the RRset to check.
     * @return SecurityStatus.SECURE if the rrest verified positively,
     *         SecurityStatus.BOGUS otherwise.
     */
    public SecurityStatus verify(RRset rrset, RRset keyRrset) {
        Iterator<?> i = rrset.sigs();
        if (!i.hasNext()) {
            logger.info("RRset failed to verify due to lack of signatures");
            return SecurityStatus.BOGUS;
        }

        while (i.hasNext()) {
            RRSIGRecord sigrec = (RRSIGRecord)i.next();
            SecurityStatus res = this.verifySignature(rrset, sigrec, keyRrset);
            if (res == SecurityStatus.SECURE) {
                return res;
            }
        }

        logger.info("RRset failed to verify: all signatures were BOGUS");
        return SecurityStatus.BOGUS;
    }

    /**
     * Verify an RRset against a single DNSKEY. Use this when you must be
     * certain that an RRset signed and verifies with a particular DNSKEY (as
     * opposed to a particular DNSKEY rrset).
     * 
     * @param rrset The rrset to verify.
     * @param dnskey The DNSKEY to verify with.
     * @return SecurityStatus.SECURE if the rrset verified, BOGUS otherwise.
     */
    public SecurityStatus verify(RRset rrset, DNSKEYRecord dnskey) {
        Iterator<?> i = rrset.sigs();
        if (!i.hasNext()) {
            logger.info("RRset failed to verify due to lack of signatures");
            return SecurityStatus.BOGUS;
        }

        while (i.hasNext()) {
            RRSIGRecord sigrec = (RRSIGRecord)i.next();

            // Skip RRSIGs that do not match our given key's footprint.
            if (sigrec.getFootprint() != dnskey.getFootprint()) {
                continue;
            }

            try {
                DNSSEC.verify(rrset, sigrec, dnskey);
                return SecurityStatus.SECURE;
            }
            catch (DNSSECException e) {
                logger.error("Failed to validate RRset", e);
            }
        }

        logger.info("RRset failed to verify: all signatures were BOGUS");
        return SecurityStatus.BOGUS;
    }
}
