From beca0e48726196c368eec6717e0e98e02647c4e4 Mon Sep 17 00:00:00 2001 From: David Blacka Date: Wed, 21 Jul 2010 17:09:56 +0000 Subject: [PATCH] Add jdnssec-signrrset tool which will sign any single rrset with any key. git-svn-id: https://svn.verisignlabs.com/jdnssec/tools/trunk@208 4cbd57fe-54e5-0310-bd9a-f30fe5ea5e6e --- bin/jdnssec-signrrset | 12 + src/com/verisignlabs/dnssec/cl/SignRRset.java | 476 ++++++++++++++++++ 2 files changed, 488 insertions(+) create mode 100755 bin/jdnssec-signrrset create mode 100644 src/com/verisignlabs/dnssec/cl/SignRRset.java diff --git a/bin/jdnssec-signrrset b/bin/jdnssec-signrrset new file mode 100755 index 0000000..cfc3dfe --- /dev/null +++ b/bin/jdnssec-signrrset @@ -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.SignRRset "$@" diff --git a/src/com/verisignlabs/dnssec/cl/SignRRset.java b/src/com/verisignlabs/dnssec/cl/SignRRset.java new file mode 100644 index 0000000..684c93e --- /dev/null +++ b/src/com/verisignlabs/dnssec/cl/SignRRset.java @@ -0,0 +1,476 @@ +// $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.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 RRset signer. + * Instead of being able to sign an entire zone, it will just sign a given + * RRset. Note that it will sign any RRset with any private key without + * consideration of whether or not the RRset *should* be signed in the context + * of a zone. + * + * @author David Blacka (original) + * @author $Author: davidb $ + * @version $Revision: 2235 $ + */ +public class SignRRset { + 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 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 rrset 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-signrrset [..options..] " + + "rrset_file key_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; + } + + /** + * 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 RRset file"); + state.usage(); + } + // Construct the RRset. Complain if the records in the input file + // consist of more than one RRset. + RRset rrset = null; + for (Iterator i = records.iterator(); i.hasNext();) { + Record r = (Record) i.next(); + + // skip RRSIGs + if (r.getType() == Type.RRSIG || r.getType() == Type.SIG) { + continue; + } + + // Handle the first record. + if (rrset == null) { + rrset = new RRset(); + rrset.addRR(r); + continue; + } + // Ensure that the remaining records all belong to the same rrset. + if (rrset.getName().equals(r.getName()) + && rrset.getType() == r.getType() + && rrset.getDClass() == r.getDClass()) { + rrset.addRR(r); + } else { + System.err.println("Records do not all belong to the same RRset."); + state.usage(); + } + } + + if (rrset.size() == 0) { + System.err.println("No records found in inputfile."); + state.usage(); + } + + // Load the key pairs. + + if (state.keyFiles.length == 0) { + System.err.println("error: at least one keyfile must be specified"); + state.usage(); + } + + List keypairs = getKeys(state.keyFiles, 0, + state.keyDirectory); + + // Make sure that all the keypairs have the same name. + // This will be used as the zone name, too. + + Name keysetName = null; + for (DnsKeyPair pair : keypairs) { + if (keysetName == null) { + keysetName = pair.getDNSKEYName(); + continue; + } + if (!pair.getDNSKEYName().equals(keysetName)) { + System.err.println("Keys do not all have the same name."); + state.usage(); + } + } + + // default the output file, if not set. + if (state.outputfile == null) { + state.outputfile = state.inputfile + ".signed"; + } + + JCEDnsSecSigner signer = new JCEDnsSecSigner(); + + List sigs = signer.signRRset(rrset, keypairs, state.start, + state.expire); + for (Iterator i = sigs.iterator(); i.hasNext();) { + rrset.addRR((Record) i.next()); + } + + // write out the signed RRset + List signed_records = new ArrayList(); + for (Iterator i = rrset.rrs(); i.hasNext();) { + signed_records.add(i.next()); + } + for (Iterator i = rrset.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(SignRRset.class.toString()); + + try { + execute(state); + } catch (Exception e) { + e.printStackTrace(); + } + } +}