diff --git a/ChangeLog b/ChangeLog index 4bd5b8d..608bd2e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,37 @@ +2009-08-23 David Blacka + + * Released version 0.9.4 + +2009-07-15 David Blacka + + * SignUtils: Fix major issue where the code that generates that + canonical RRset given signature data wasn't obeying the "Orig TTL" + and "Labels" fields. This is a major issue with verification, + although it doesn't affect signature generation. + + * VerifyZone: Fix bug where the whole-zone security status was + still wrong: unsigned RRsets shouldn't make the zone Bogus. + +2009-06-12 David Blacka + + * VerifyZone: Fix bug in verification logic so that RRsets that + never find a valid signature (i.e., only have signatures by keys + that aren't in the zone) are considered Bogus. Note that + VerifyZone still can't tell if a RRset that should be signed + wasn't (or vice versa). + + * dnsjava: Update local copy of dnsjava library. This version + adds NSEC3 agorithms to DNSSECVerifier and KEYConverter, emulates + DiG's "OPT PSEUDOSECTION" formatting in Message.toString(), and + adds a minimal DHCIDRecord type. Note that the DNSjava trunk has + a different (although functional similar) version of this type. + +2009-06-09 David Blacka + + * VerifyZone: Improve the output. + + * SignKeyset: Add a command line tool for just signing DNSKEY RRsets. + 2009-02-10 David Blacka * Released version 0.9.0 diff --git a/VERSION b/VERSION index 68b492f..c86294a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -version=0.9.0 +version=0.9.4 diff --git a/bin/_jdnssec-dstool b/bin/_jdnssec-dstool index b2972db..9fa11c4 100755 --- a/bin/_jdnssec-dstool +++ b/bin/_jdnssec-dstool @@ -3,7 +3,10 @@ thisdir=`dirname $0` basedir=`cd $thisdir/..; pwd` -ulimit -n `ulimit -H -n` +ulimit_max=`ulimit -H -n` +if [ $ulimit_max != "unlimited" ]; then + ulimit -n $ulimit_max +fi # set the classpath CLASSPATH=$CLASSPATH:$basedir/build/classes diff --git a/bin/_jdnssec-keygen b/bin/_jdnssec-keygen index facc581..a18a647 100755 --- a/bin/_jdnssec-keygen +++ b/bin/_jdnssec-keygen @@ -3,7 +3,10 @@ thisdir=`dirname $0` basedir=`cd $thisdir/..; pwd` -ulimit -n `ulimit -H -n` +ulimit_max=`ulimit -H -n` +if [ $ulimit_max != "unlimited" ]; then + ulimit -n $ulimit_max +fi # set the classpath CLASSPATH=$CLASSPATH:$basedir/build/classes diff --git a/bin/_jdnssec-keyinfo b/bin/_jdnssec-keyinfo index 24c301c..6a01bea 100755 --- a/bin/_jdnssec-keyinfo +++ b/bin/_jdnssec-keyinfo @@ -3,7 +3,10 @@ thisdir=`dirname $0` basedir=`cd $thisdir/..; pwd` -ulimit -n `ulimit -H -n` +ulimit_max=`ulimit -H -n` +if [ $ulimit_max != "unlimited" ]; then + ulimit -n $ulimit_max +fi # set the classpath CLASSPATH=$CLASSPATH:$basedir/build/classes diff --git a/bin/_jdnssec-signkeyset b/bin/_jdnssec-signkeyset new file mode 100755 index 0000000..02c7001 --- /dev/null +++ b/bin/_jdnssec-signkeyset @@ -0,0 +1,19 @@ +#! /bin/sh + +thisdir=`dirname $0` +basedir=`cd $thisdir/..; pwd` + +ulimit_max=`ulimit -H -n` +if [ $ulimit_max != "unlimited" ]; then + ulimit -n $ulimit_max +fi + +# set the classpath +CLASSPATH=$CLASSPATH:$basedir/build/classes + +for i in $basedir/lib/*.jar $basedir/lib/*.zip; do + CLASSPATH="$CLASSPATH":"$i" +done +export CLASSPATH + +exec java com.verisignlabs.dnssec.cl.SignKeyset "$@" diff --git a/bin/_jdnssec-signzone b/bin/_jdnssec-signzone index 9a90add..9ffacce 100755 --- a/bin/_jdnssec-signzone +++ b/bin/_jdnssec-signzone @@ -3,7 +3,10 @@ thisdir=`dirname $0` basedir=`cd $thisdir/..; pwd` -ulimit -n `ulimit -H -n` +ulimit_max=`ulimit -H -n` +if [ $ulimit_max != "unlimited" ]; then + ulimit -n $ulimit_max +fi # set the classpath CLASSPATH=$CLASSPATH:$basedir/build/classes diff --git a/bin/_jdnssec-verifyzone b/bin/_jdnssec-verifyzone index 79b1127..21fe4c3 100755 --- a/bin/_jdnssec-verifyzone +++ b/bin/_jdnssec-verifyzone @@ -3,7 +3,10 @@ thisdir=`dirname $0` basedir=`cd $thisdir/..; pwd` -ulimit -n `ulimit -H -n` +ulimit_max=`ulimit -H -n` +if [ $ulimit_max != "unlimited" ]; then + ulimit -n $ulimit_max +fi # set the classpath CLASSPATH=$CLASSPATH:$basedir/build/classes diff --git a/bin/_jdnssec-zoneformat b/bin/_jdnssec-zoneformat index f81fb60..974efa0 100755 --- a/bin/_jdnssec-zoneformat +++ b/bin/_jdnssec-zoneformat @@ -3,7 +3,10 @@ thisdir=`dirname $0` basedir=`cd $thisdir/..; pwd` -ulimit -n `ulimit -H -n` +ulimit_max=`ulimit -H -n` +if [ $ulimit_max != "unlimited" ]; then + ulimit -n $ulimit_max +fi # set the classpath CLASSPATH=$CLASSPATH:$basedir/build/classes diff --git a/bin/jdnssec-signkeyset b/bin/jdnssec-signkeyset new file mode 100755 index 0000000..4d00ca3 --- /dev/null +++ b/bin/jdnssec-signkeyset @@ -0,0 +1,12 @@ +#! /bin/sh + +thisdir=`dirname $0` +basedir=`cd $thisdir/..; pwd` + +# set the classpath +for i in $basedir/lib/*.jar $basedir/lib/*.zip $basedir/build/lib/*.jar; do + CLASSPATH="$CLASSPATH":"$i" +done +export CLASSPATH + +exec java com.verisignlabs.dnssec.cl.SignKeyset "$@" diff --git a/lib/dnsjava-2.0.6-vrsn-2.jar b/lib/dnsjava-2.0.6-vrsn-2.jar deleted file mode 100644 index 85a506e..0000000 Binary files a/lib/dnsjava-2.0.6-vrsn-2.jar and /dev/null differ diff --git a/lib/dnsjava-2.0.6-vrsn-4.jar b/lib/dnsjava-2.0.6-vrsn-4.jar new file mode 100644 index 0000000..b7e17a4 Binary files /dev/null and b/lib/dnsjava-2.0.6-vrsn-4.jar differ diff --git a/src/com/verisignlabs/dnssec/cl/SignKeyset.java b/src/com/verisignlabs/dnssec/cl/SignKeyset.java new file mode 100644 index 0000000..12f52b9 --- /dev/null +++ b/src/com/verisignlabs/dnssec/cl/SignKeyset.java @@ -0,0 +1,500 @@ +// $Id: SignZone.java 2235 2009-02-07 20:37:29Z davidb $ +// +// Copyright (C) 2001-2003, 2009 VeriSign, Inc. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +// USA + +package com.verisignlabs.dnssec.cl; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.TimeZone; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.cli.AlreadySelectedException; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.OptionBuilder; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.PosixParser; +import org.apache.commons.cli.UnrecognizedOptionException; +import org.xbill.DNS.DNSSEC; +import org.xbill.DNS.Name; +import org.xbill.DNS.RRset; +import org.xbill.DNS.Record; +import org.xbill.DNS.Type; + +import com.verisignlabs.dnssec.security.BINDKeyUtils; +import com.verisignlabs.dnssec.security.DnsKeyPair; +import com.verisignlabs.dnssec.security.DnsSecVerifier; +import com.verisignlabs.dnssec.security.JCEDnsSecSigner; +import com.verisignlabs.dnssec.security.SignUtils; +import com.verisignlabs.dnssec.security.ZoneUtils; + +/** + * This class forms the command line implementation of a DNSSEC keyset signer. + * Instead of being able to sign an entire zone, it will just sign a given + * DNSKEY RRset. + * + * @author David Blacka (original) + * @author $Author: davidb $ + * @version $Revision: 2235 $ + */ +public class SignKeyset { + private static Logger log; + + /** + * This is an inner class used to hold all of the command line option state. + */ + private static class CLIState { + private Options opts; + private File keyDirectory = null; + public String[] keyFiles = null; + public String keysetFile = null; + public Date start = null; + public Date expire = null; + public String inputfile = null; + public String outputfile = null; + public boolean verifySigs = false; + + public CLIState() { + setupCLI(); + } + + /** + * Set up the command line options. + * + * @return a set of command line options. + */ + private void setupCLI() { + opts = new Options(); + + // boolean options + opts.addOption("h", "help", false, "Print this message."); + opts.addOption("a", "verify", false, "verify generated signatures>"); + + OptionBuilder.hasOptionalArg(); + OptionBuilder.withLongOpt("verbose"); + OptionBuilder.withArgName("level"); + OptionBuilder.withDescription("verbosity level."); + // Argument options + opts.addOption(OptionBuilder.create('v')); + + OptionBuilder.hasArg(); + OptionBuilder.withArgName("dir"); + OptionBuilder.withLongOpt("key-directory"); + OptionBuilder.withDescription("directory to find key files (default '.')."); + opts.addOption(OptionBuilder.create('D')); + + OptionBuilder.hasArg(); + OptionBuilder.withArgName("time/offset"); + OptionBuilder.withLongOpt("start-time"); + OptionBuilder.withDescription("signature starting time (default is now - 1 hour)"); + opts.addOption(OptionBuilder.create('s')); + + OptionBuilder.hasArg(); + OptionBuilder.withArgName("time/offset"); + OptionBuilder.withLongOpt("expire-time"); + OptionBuilder.withDescription("signature expiration time (default is start-time + 30 days)."); + opts.addOption(OptionBuilder.create('e')); + + OptionBuilder.hasArg(); + OptionBuilder.withArgName("outfile"); + OptionBuilder.withDescription("file the signed keyset is written to."); + opts.addOption(OptionBuilder.create('f')); + } + + public void parseCommandLine(String[] args) + throws org.apache.commons.cli.ParseException, ParseException, + IOException { + CommandLineParser cli_parser = new PosixParser(); + CommandLine cli = cli_parser.parse(opts, args); + + String optstr = null; + if (cli.hasOption('h')) usage(); + + if (cli.hasOption('v')) { + int value = parseInt(cli.getOptionValue('v'), 5); + Logger rootLogger = Logger.getLogger(""); + + switch (value) { + case 0: + rootLogger.setLevel(Level.OFF); + break; + case 4: + default: + rootLogger.setLevel(Level.INFO); + break; + case 5: + rootLogger.setLevel(Level.FINE); + break; + case 6: + rootLogger.setLevel(Level.ALL); + break; + } + Handler[] handlers = rootLogger.getHandlers(); + for (int i = 0; i < handlers.length; i++) + handlers[i].setLevel(rootLogger.getLevel()); + } + + if (cli.hasOption('a')) verifySigs = true; + + if ((optstr = cli.getOptionValue('D')) != null) { + keyDirectory = new File(optstr); + if (!keyDirectory.isDirectory()) { + System.err.println("error: " + optstr + + " is not a directory"); + usage(); + } + } + + if ((optstr = cli.getOptionValue('s')) != null) { + start = convertDuration(null, optstr); + } else { + // default is now - 1 hour. + start = new Date(System.currentTimeMillis() - (3600 * 1000)); + } + + if ((optstr = cli.getOptionValue('e')) != null) { + expire = convertDuration(start, optstr); + } else { + expire = convertDuration(start, "+2592000"); // 30 days + } + + outputfile = cli.getOptionValue('f'); + + String[] files = cli.getArgs(); + + if (files.length < 1) { + System.err.println("error: missing zone file and/or key files"); + usage(); + } + + inputfile = files[0]; + if (files.length > 1) { + keyFiles = new String[files.length - 1]; + System.arraycopy(files, 1, keyFiles, 0, files.length - 1); + } + } + + /** Print out the usage and help statements, then quit. */ + private void usage() { + HelpFormatter f = new HelpFormatter(); + + PrintWriter out = new PrintWriter(System.err); + + // print our own usage statement: + out.println("usage: jdnssec-signkeyset [..options..] " + + "dnskeyset_file [key_file ...] "); + f.printHelp(out, 75, "signZone.sh", null, opts, + HelpFormatter.DEFAULT_LEFT_PAD, + HelpFormatter.DEFAULT_DESC_PAD, + "\ntime/offset = YYYYMMDDHHmmss|+offset|\"now\"+offset\n"); + + out.flush(); + System.exit(64); + } + } + + /** + * This is just a convenience method for parsing integers from strings. + * + * @param s + * the string to parse. + * @param def + * the default value, if the string doesn't parse. + * @return the parsed integer, or the default. + */ + private static int parseInt(String s, int def) { + try { + int v = Integer.parseInt(s); + return v; + } catch (NumberFormatException e) { + return def; + } + } + + /** + * Verify the generated signatures. + * + * @param zonename + * the origin name of the zone. + * @param records + * a list of {@link org.xbill.DNS.Record}s. + * @param keypairs + * a list of keypairs used the sign the zone. + * @return true if all of the signatures validated. + */ + private static boolean verifySigs(Name zonename, List records, List keypairs) { + boolean secure = true; + + DnsSecVerifier verifier = new DnsSecVerifier(); + + for (Iterator i = keypairs.iterator(); i.hasNext();) { + verifier.addTrustedKey((DnsKeyPair) i.next()); + } + + verifier.setVerifyAllSigs(true); + + List rrsets = SignUtils.assembleIntoRRsets(records); + + for (Iterator i = rrsets.iterator(); i.hasNext();) { + RRset rrset = (RRset) i.next(); + + // skip unsigned rrsets. + if (!rrset.sigs().hasNext()) continue; + + int result = verifier.verify(rrset, null); + + if (result != DNSSEC.Secure) { + log.fine("Signatures did not verify for RRset: (" + result + + "): " + rrset); + secure = false; + } + } + + return secure; + } + + /** + * Load the key pairs from the key files. + * + * @param keyfiles + * a string array containing the base names or paths of the keys + * to be loaded. + * @param start_index + * the starting index of keyfiles string array to use. This + * allows us to use the straight command line argument array. + * @param inDirectory + * the directory to look in (may be null). + * @return a list of keypair objects. + */ + private static List getKeys(String[] keyfiles, int start_index, + File inDirectory) throws IOException { + if (keyfiles == null) return null; + + int len = keyfiles.length - start_index; + if (len <= 0) return null; + + ArrayList keys = new ArrayList(len); + + for (int i = start_index; i < keyfiles.length; i++) { + DnsKeyPair k = BINDKeyUtils.loadKeyPair(keyfiles[i], inDirectory); + if (k != null) keys.add(k); + } + + return keys; + } + + private static class KeyFileFilter implements FileFilter { + private String prefix; + + public KeyFileFilter(Name origin) { + prefix = "K" + origin.toString(); + } + + public boolean accept(File pathname) { + if (!pathname.isFile()) return false; + String name = pathname.getName(); + if (name.startsWith(prefix) && name.endsWith(".private")) + return true; + return false; + } + } + + private static List findZoneKeys(File inDirectory, Name zonename) + throws IOException { + if (inDirectory == null) { + inDirectory = new File("."); + } + + // get the list of "K.*.private files. + FileFilter filter = new KeyFileFilter(zonename); + File[] files = inDirectory.listFiles(filter); + + // read in all of the records + ArrayList keys = new ArrayList(); + for (int i = 0; i < files.length; i++) { + DnsKeyPair p = BINDKeyUtils.loadKeyPair(files[i].getName(), + inDirectory); + keys.add(p); + } + + if (keys.size() > 0) return keys; + return null; + } + + /** + * Calculate a date/time from a command line time/offset duration string. + * + * @param start + * the start time to calculate offsets from. + * @param duration + * the time/offset string to parse. + * @return the calculated time. + */ + private static Date convertDuration(Date start, String duration) + throws ParseException { + if (start == null) start = new Date(); + if (duration.startsWith("now")) { + start = new Date(); + if (duration.indexOf("+") < 0) return start; + + duration = duration.substring(3); + } + + if (duration.startsWith("+")) { + long offset = (long) parseInt(duration.substring(1), 0) * 1000; + return new Date(start.getTime() + offset); + } + + SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMddHHmmss"); + dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT")); + return dateFormatter.parse(duration); + } + + public static void execute(CLIState state) throws Exception { + // Read in the zone + List records = ZoneUtils.readZoneFile(state.inputfile, null); + if (records == null || records.size() == 0) { + System.err.println("error: empty keyset file"); + state.usage(); + } + + // Make sure that all records are DNSKEYs with the same name. + Name keysetName = null; + RRset keyset = new RRset(); + for (Iterator i = records.iterator(); i.hasNext();) { + Record r = (Record) i.next(); + if (r.getType() != Type.DNSKEY) { + System.err.println("error: Non DNSKEY RR found in keyset: " + r); + continue; + } + if (keysetName == null) { + keysetName = r.getName(); + } + if (!r.getName().equals(keysetName)) { + System.err.println("error: DNSKEY with a different name found!"); + state.usage(); + } + keyset.addRR(r); + } + + if (keyset.size() == 0) { + System.err.println("error: No DNSKEYs found in keyset file"); + state.usage(); + } + + // Load the key pairs. + List keypairs = getKeys(state.keyFiles, 0, state.keyDirectory); + + // If we *still* don't have any key pairs, look for keys the key + // directory + // that match + if (keypairs == null) { + keypairs = findZoneKeys(state.keyDirectory, keysetName); + } + + // If there *still* aren't any ZSKs defined, bail. + if (keypairs == null || keypairs.size() == 0) { + System.err.println("error: No signing keys could be determined."); + state.usage(); + } + + // default the output file, if not set. + if (state.outputfile == null) { + if (keysetName.isAbsolute()) { + state.outputfile = keysetName + "signed_keyset"; + } else { + state.outputfile = keysetName + ".signed_keyset"; + } + } + + JCEDnsSecSigner signer = new JCEDnsSecSigner(); + + List sigs = signer.signRRset(keyset, keypairs, state.start, + state.expire); + for (Iterator i = sigs.iterator(); i.hasNext();) { + keyset.addRR((Record) i.next()); + } + + // write out the signed RRset + List signed_records = new ArrayList(); + for (Iterator i = keyset.rrs(); i.hasNext();) { + signed_records.add(i.next()); + } + for (Iterator i = keyset.sigs(); i.hasNext();) { + signed_records.add(i.next()); + } + + // write out the signed zone + // force multiline mode for now + org.xbill.DNS.Options.set("multiline"); + ZoneUtils.writeZoneFile(signed_records, state.outputfile); + + if (state.verifySigs) { + log.fine("verifying generated signatures"); + boolean res = verifySigs(keysetName, signed_records, keypairs); + + if (res) { + System.out.println("Generated signatures verified"); + // log.info("Generated signatures verified"); + } else { + System.out.println("Generated signatures did not verify."); + // log.warn("Generated signatures did not verify."); + } + } + + } + + public static void main(String[] args) { + CLIState state = new CLIState(); + try { + state.parseCommandLine(args); + } catch (UnrecognizedOptionException e) { + System.err.println("error: unknown option encountered: " + + e.getMessage()); + state.usage(); + } catch (AlreadySelectedException e) { + System.err.println("error: mutually exclusive options have " + + "been selected:\n " + e.getMessage()); + state.usage(); + } catch (Exception e) { + System.err.println("error: unknown command line parsing exception:"); + e.printStackTrace(); + state.usage(); + } + + log = Logger.getLogger(SignKeyset.class.toString()); + + try { + execute(state); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/com/verisignlabs/dnssec/cl/VerifyZone.java b/src/com/verisignlabs/dnssec/cl/VerifyZone.java index ee9aa80..e002787 100644 --- a/src/com/verisignlabs/dnssec/cl/VerifyZone.java +++ b/src/com/verisignlabs/dnssec/cl/VerifyZone.java @@ -75,8 +75,10 @@ public class VerifyZone // boolean options opts.addOption("h", "help", false, "Print this message."); opts.addOption("s", "strict", false, - "Zone will only be considered valid if all " - + "signatures could be cryptographically verified"); + "Zone will only be considered valid if all " + + "signatures could be cryptographically verified"); + opts.addOption("m", "multiline", false, + "log DNS records using 'multiline' format"); // Argument options OptionBuilder.hasArg(); @@ -112,25 +114,33 @@ public class VerifyZone if (cli.hasOption('v')) { - int value = parseInt(cli.getOptionValue('v'), 5); + int value = parseInt(cli.getOptionValue('v'), 1); Logger rootLogger = Logger.getLogger(""); switch (value) { - case 0: - rootLogger.setLevel(Level.OFF); - break; - case 5: - default: - rootLogger.setLevel(Level.FINE); - break; - case 6: - rootLogger.setLevel(Level.ALL); - break; + case 0: + rootLogger.setLevel(Level.OFF); + break; + case 1: + rootLogger.setLevel(Level.INFO); + break; + case 5: + default: + rootLogger.setLevel(Level.FINE); + break; + case 6: + rootLogger.setLevel(Level.ALL); + break; } } if (cli.hasOption('s')) strict = true; + if (cli.hasOption('m')) + { + org.xbill.DNS.Options.set("multiline"); + } + if ((optstr = cli.getOptionValue('d')) != null) { keydir = new File(optstr); @@ -191,8 +201,7 @@ public class VerifyZone // print our own usage statement: f.printHelp(out, 75, "jdnssec-verifyzone [..options..] zonefile " + "[keyfile [keyfile...]]", null, opts, - HelpFormatter.DEFAULT_LEFT_PAD, - HelpFormatter.DEFAULT_DESC_PAD, null); + HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, null); out.flush(); System.exit(64); @@ -223,6 +232,19 @@ public class VerifyZone } + private static String reasonListToString(List reasons) + { + if (reasons == null) return ""; + StringBuffer out = new StringBuffer(); + for (Iterator i = reasons.iterator(); i.hasNext();) + { + out.append("Reason: "); + out.append((String) i.next()); + if (i.hasNext()) out.append("\n"); + } + return out.toString(); + } + private static byte verifyZoneSignatures(List records, List keypairs) { // Zone is secure until proven otherwise. @@ -234,18 +256,24 @@ public class VerifyZone { DnsKeyPair pair = (DnsKeyPair) i.next(); if (pair.getPublic() == null) continue; + log.info("Adding trusted key: " + pair.getDNSKEYRecord() + " ; keytag = " + + pair.getDNSKEYFootprint()); verifier.addTrustedKey(pair); } List rrsets = SignUtils.assembleIntoRRsets(records); + List reasons = new ArrayList(); for (Iterator i = rrsets.iterator(); i.hasNext();) { RRset rrset = (RRset) i.next(); // We verify each signature separately so that we can report // which exact signature failed. - for (Iterator j = rrset.sigs(); j.hasNext();) + Iterator j = rrset.sigs(); + // Set the default result based on whether or not this was a signed RRset. + byte rrset_result = (byte) (j.hasNext() ? DNSSEC.Failed : DNSSEC.Secure); + while (j.hasNext()) { Object o = j.next(); if (!(o instanceof RRSIGRecord)) @@ -255,14 +283,18 @@ public class VerifyZone } RRSIGRecord sigrec = (RRSIGRecord) o; - byte res = verifier.verifySignature(rrset, sigrec, null); + reasons.clear(); + byte res = verifier.verifySignature(rrset, sigrec, null, reasons); if (res != DNSSEC.Secure) { - log.info("Signature failed to verify RRset: " + rrset + "\nsig: " - + sigrec); + log.info("Signature failed to verify RRset:\n rr: " + + ZoneUtils.rrsetToString(rrset, false) + "\n sig: " + sigrec + + "\n" + reasonListToString(reasons)); } - if (res < result) result = res; + if (res > rrset_result) rrset_result = res; } + if (rrset_result != DNSSEC.Secure) result = DNSSEC.Failed; + } return result; @@ -327,19 +359,19 @@ public class VerifyZone switch (result) { - case DNSSEC.Failed: + case DNSSEC.Failed: + System.out.println("zone did not verify."); + System.exit(1); + break; + case DNSSEC.Insecure: + if (state.strict) + { System.out.println("zone did not verify."); System.exit(1); - break; - case DNSSEC.Insecure: - if (state.strict) - { - System.out.println("zone did not verify."); - System.exit(1); - } - case DNSSEC.Secure: - System.out.println("zone verified."); - break; + } + case DNSSEC.Secure: + System.out.println("zone verified."); + break; } System.exit(0); } @@ -354,7 +386,8 @@ public class VerifyZone } catch (UnrecognizedOptionException e) { - System.err.println("error: unknown option encountered: " + e.getMessage()); + System.err + .println("error: unknown option encountered: " + e.getMessage()); state.usage(); } catch (AlreadySelectedException e) diff --git a/src/com/verisignlabs/dnssec/security/DnsSecVerifier.java b/src/com/verisignlabs/dnssec/security/DnsSecVerifier.java index 03fcf72..3495878 100644 --- a/src/com/verisignlabs/dnssec/security/DnsSecVerifier.java +++ b/src/com/verisignlabs/dnssec/security/DnsSecVerifier.java @@ -95,7 +95,10 @@ public class DnsSecVerifier implements Verifier { DnsKeyPair p = (DnsKeyPair) i.next(); if (p.getDNSKEYAlgorithm() == algorithm - && p.getDNSKEYFootprint() == keyid) { return p; } + && p.getDNSKEYFootprint() == keyid) + { + return p; + } } return null; } @@ -164,8 +167,10 @@ public class DnsSecVerifier implements Verifier if (!(o instanceof DNSKEYRecord)) continue; DNSKEYRecord keyrec = (DNSKEYRecord) o; if (keyrec.getAlgorithm() == algorithm - && keyrec.getFootprint() == footprint) { return new DnsKeyPair( - keyrec, (PrivateKey) null); } + && keyrec.getFootprint() == footprint) + { + return new DnsKeyPair(keyrec, (PrivateKey) null); + } } return null; @@ -183,17 +188,21 @@ public class DnsSecVerifier implements Verifier return pair; } - private byte validateSignature(RRset rrset, RRSIGRecord sigrec) + private byte validateSignature(RRset rrset, RRSIGRecord sigrec, List reasons) { if (rrset == null || sigrec == null) return DNSSEC.Failed; if (!rrset.getName().equals(sigrec.getName())) { - log.info("Signature name does not match RRset name"); + log.fine("Signature name does not match RRset name"); + if (reasons != null) + reasons.add("Signature name does not match RRset name"); return DNSSEC.Failed; } if (rrset.getType() != sigrec.getTypeCovered()) { - log.info("Signature type does not match RRset type"); + log.fine("Signature type does not match RRset type"); + if (reasons != null) + reasons.add("Signature type does not match RRset type"); } Date now = new Date(); @@ -208,7 +217,8 @@ public class DnsSecVerifier implements Verifier } if (now.before(start)) { - log.info("Signature is not yet valid"); + log.fine("Signature is not yet valid"); + if (reasons != null) reasons.add("Signature not yet valid"); return DNSSEC.Failed; } } @@ -221,8 +231,9 @@ public class DnsSecVerifier implements Verifier } if (now.after(expire)) { - log.info("Signature has expired (now = " + now + ", sig expires = " + log.fine("Signature has expired (now = " + now + ", sig expires = " + expire); + if (reasons != null) reasons.add("Signature has expired."); return DNSSEC.Failed; } } @@ -230,25 +241,32 @@ public class DnsSecVerifier implements Verifier return DNSSEC.Secure; } + public byte verifySignature(RRset rrset, RRSIGRecord sigrec, Cache cache) + { + return verifySignature(rrset, sigrec, cache, null); + } + /** * Verify an RRset against a particular signature. * - * @return DNSSEC.Secure if the signature verfied, DNSSEC.Failed if it did not - * verify (for any reason), and DNSSEC.Insecure if verification could - * not be completed (usually because the public key was not + * @return DNSSEC.Secure if the signature verified, DNSSEC.Failed if it did + * not verify (for any reason), and DNSSEC.Insecure if verification + * could not be completed (usually because the public key was not * available). */ - public byte verifySignature(RRset rrset, RRSIGRecord sigrec, Cache cache) + public byte verifySignature(RRset rrset, RRSIGRecord sigrec, Cache cache, + List reasons) { - byte result = validateSignature(rrset, sigrec); + byte result = validateSignature(rrset, sigrec, reasons); if (result != DNSSEC.Secure) return result; - DnsKeyPair keypair = findKey(cache, sigrec.getSigner(), - sigrec.getAlgorithm(), sigrec.getFootprint()); + DnsKeyPair keypair = findKey(cache, sigrec.getSigner(), sigrec + .getAlgorithm(), sigrec.getFootprint()); if (keypair == null) { - log.info("could not find appropriate key"); + if (reasons != null) reasons.add("Could not find matching trusted key"); + log.fine("could not find matching trusted key"); return DNSSEC.Insecure; } @@ -270,7 +288,9 @@ public class DnsSecVerifier implements Verifier if (!signer.verify(sig)) { - log.info("Signature failed to verify cryptographically"); + if (reasons != null) + reasons.add("Signature failed to verify cryptographically"); + log.fine("Signature failed to verify cryptographically"); return DNSSEC.Failed; } @@ -284,7 +304,8 @@ public class DnsSecVerifier implements Verifier { log.severe("Security error: " + e); } - + if (reasons != null) + reasons.add("Signature failed to verify due to exception"); log.fine("Signature failed to verify due to exception"); return DNSSEC.Insecure; } @@ -303,7 +324,7 @@ public class DnsSecVerifier implements Verifier if (!i.hasNext()) { - log.info("RRset failed to verify due to lack of signatures"); + log.fine("RRset failed to verify due to lack of signatures"); return DNSSEC.Insecure; } diff --git a/src/com/verisignlabs/dnssec/security/JCEDnsSecSigner.java b/src/com/verisignlabs/dnssec/security/JCEDnsSecSigner.java index 000193e..03b8a3e 100644 --- a/src/com/verisignlabs/dnssec/security/JCEDnsSecSigner.java +++ b/src/com/verisignlabs/dnssec/security/JCEDnsSecSigner.java @@ -128,7 +128,7 @@ public class JCEDnsSecSigner if (keypairs.size() == 0) return null; // first, pre-calculate the RRset bytes. - byte[] rrset_data = SignUtils.generateCanonicalRRsetData(rrset); + byte[] rrset_data = SignUtils.generateCanonicalRRsetData(rrset, 0, 0); ArrayList sigs = new ArrayList(keypairs.size()); diff --git a/src/com/verisignlabs/dnssec/security/SignUtils.java b/src/com/verisignlabs/dnssec/security/SignUtils.java index 58d90f6..f6905ba 100644 --- a/src/com/verisignlabs/dnssec/security/SignUtils.java +++ b/src/com/verisignlabs/dnssec/security/SignUtils.java @@ -149,24 +149,61 @@ public class SignUtils return image.toByteArray(); } + /** * Calculate the canonical wire line format of the RRset. * * @param rrset - * the RRset to convert. - * @return the canonical wire line format of the RRset. This is the second + * the RRset to convert. + * @param ttl + * the TTL to use when canonicalizing -- this is generally the + * TTL of the signature if there is a pre-existing signature. If + * not it is just the ttl of the rrset itself. + * @param labels + * the labels field of the signature, or 0. + * @return the canonical wire line format of the rrset. This is the second * part of data to be signed. */ - public static byte[] generateCanonicalRRsetData(RRset rrset) + public static byte[] generateCanonicalRRsetData(RRset rrset, long ttl, + int labels) { DNSOutput image = new DNSOutput(); - // now convert load the wire format records in the RRset into a + if (ttl == 0) ttl = rrset.getTTL(); + Name n = rrset.getName(); + if (labels == 0) + { + labels = n.labels(); + } + else + { + // correct for Name()'s conception of label count. + labels++; + } + boolean wildcardName = false; + if (n.labels() != labels) + { + n = n.wild(n.labels() - labels); + wildcardName = true; + log.fine("Detected wildcard expansion: " + rrset.getName() + + " changed to " + n); + } + + // now convert the wire format records in the RRset into a // list of byte arrays. ArrayList canonical_rrs = new ArrayList(); for (Iterator i = rrset.rrs(); i.hasNext();) { Record r = (Record) i.next(); + if (r.getTTL() != ttl || wildcardName) + { + // If necessary, we need to create a new record with a new ttl + // or ownername. + // In the TTL case, this avoids changing the ttl in the + // response. + r = Record.newRecord(n, r.getType(), r.getDClass(), ttl, r + .rdataToWireCanonical()); + } byte[] wire_fmt = r.toWireCanonical(); canonical_rrs.add(wire_fmt); } @@ -202,7 +239,9 @@ public class SignUtils public static byte[] generateSigData(RRset rrset, RRSIGRecord presig) throws IOException { - byte[] rrset_data = generateCanonicalRRsetData(rrset); + byte[] rrset_data = generateCanonicalRRsetData(rrset, + presig.getOrigTTL(), + presig.getLabels()); return generateSigData(rrset_data, presig); } diff --git a/src/com/verisignlabs/dnssec/security/ZoneUtils.java b/src/com/verisignlabs/dnssec/security/ZoneUtils.java index 48ad4ea..32d5e52 100644 --- a/src/com/verisignlabs/dnssec/security/ZoneUtils.java +++ b/src/com/verisignlabs/dnssec/security/ZoneUtils.java @@ -138,23 +138,23 @@ public class ZoneUtils return null; } - + public static List findRRs(List records, Name name, int type) { List res = new ArrayList(); for (Iterator i = records.iterator(); i.hasNext();) { Object o = i.next(); - + if (o instanceof Record) { Record r = (Record) o; - if (r.getName().equals(name) && r.getType() == type) + if (r.getName().equals(name) && r.getType() == type) { res.add(r); } - } - else if (o instanceof RRset) + } + else if (o instanceof RRset) { RRset r = (RRset) o; if (r.getName().equals(name) && r.getType() == type) @@ -166,9 +166,30 @@ public class ZoneUtils } } } - + return res; } - -} + /** This is an alternate way to format an RRset into a string */ + public static String rrsetToString(RRset rrset, boolean includeSigs) + { + StringBuffer out = new StringBuffer(); + + for (Iterator i = rrset.rrs(false); i.hasNext();) + { + Record r = (Record) i.next(); + out.append(r.toString()); + out.append("\n"); + } + if (includeSigs) + { + for (Iterator i = rrset.sigs(); i.hasNext();) + { + Record r = (Record) i.next(); + out.append(r.toString()); + out.append("\n"); + } + } + return out.toString(); + } +}