Java tutorial
// Copyright (C) 2001-2003, 2011 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.BufferedReader; import java.io.File; import java.io.FileFilter; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Random; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.xbill.DNS.DNSKEYRecord; import org.xbill.DNS.DSRecord; import org.xbill.DNS.Name; import org.xbill.DNS.RRset; import org.xbill.DNS.Record; import org.xbill.DNS.TextParseException; import org.xbill.DNS.Type; import org.xbill.DNS.utils.base16; 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 zone signer. * * @author David Blacka */ public class SignZone extends CLBase { private CLIState state; /** * This is an inner class used to hold all of the command line option state. */ private static class CLIState extends CLIStateBase { public File keyDirectory = null; public File keysetDirectory = null; public String[] kskFiles = null; public String[] keyFiles = null; public String zonefile = null; public Date start = null; public Date expire = null; public String outputfile = null; public boolean verifySigs = false; public boolean useOptOut = false; public boolean fullySignKeyset = false; public List<Name> includeNames = null; public boolean useNsec3 = false; public byte[] salt = null; public int iterations = 0; public int digest_id = DSRecord.SHA1_DIGEST_ID; public long nsec3paramttl = -1; public boolean verboseSigning = false; public CLIState() { super("jdnssec-signzone [..options..] zone_file [key_file ...]"); } protected void setupOptions(Options opts) { // boolean options opts.addOption("a", "verify", false, "verify generated signatures>"); opts.addOption("F", "fully-sign-keyset", false, "sign the zone apex keyset with all available keys."); opts.addOption("V", "verbose-signing", false, "Display verbose signing activity."); // Argument options OptionBuilder.hasArg(); OptionBuilder.withArgName("dir"); OptionBuilder.withLongOpt("keyset-directory"); OptionBuilder.withDescription("directory to find keyset files (default '.')."); opts.addOption(OptionBuilder.create('d')); 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 zone is written to (default is <origin>.signed)."); opts.addOption(OptionBuilder.create('f')); OptionBuilder.hasArgs(); OptionBuilder.withArgName("KSK file"); OptionBuilder.withLongOpt("ksk-file"); OptionBuilder.withDescription("this key is a key signing key (may repeat)."); opts.addOption(OptionBuilder.create('k')); OptionBuilder.hasArg(); OptionBuilder.withArgName("file"); OptionBuilder.withLongOpt("include-file"); OptionBuilder.withDescription("include names in this file in the NSEC/NSEC3 chain."); opts.addOption(OptionBuilder.create('I')); // NSEC3 options opts.addOption("3", "use-nsec3", false, "use NSEC3 instead of NSEC"); opts.addOption("O", "use-opt-out", false, "generate a fully Opt-Out zone (only valid with NSEC3)."); OptionBuilder.hasArg(); OptionBuilder.withLongOpt("salt"); OptionBuilder.withArgName("hex value"); OptionBuilder.withDescription("supply a salt value."); opts.addOption(OptionBuilder.create('S')); OptionBuilder.hasArg(); OptionBuilder.withLongOpt("random-salt"); OptionBuilder.withArgName("length"); OptionBuilder.withDescription("generate a random salt."); opts.addOption(OptionBuilder.create('R')); OptionBuilder.hasArg(); OptionBuilder.withLongOpt("iterations"); OptionBuilder.withArgName("value"); OptionBuilder.withDescription("use this value for the iterations in NSEC3."); opts.addOption(OptionBuilder.create()); OptionBuilder.hasArg(); OptionBuilder.withLongOpt("nsec3paramttl"); OptionBuilder.withArgName("ttl"); OptionBuilder.withDescription("use this value for the NSEC3PARAM RR ttl"); opts.addOption(OptionBuilder.create()); OptionBuilder.hasArg(); OptionBuilder.withArgName("id"); OptionBuilder.withLongOpt("ds-digest"); OptionBuilder.withDescription("Digest algorithm to use for generated DSs"); opts.addOption(OptionBuilder.create()); } protected void processOptions(CommandLine cli) throws ParseException { String optstr = null; if (cli.hasOption('a')) verifySigs = true; if (cli.hasOption('3')) useNsec3 = true; if (cli.hasOption('O')) useOptOut = true; if (cli.hasOption('V')) verboseSigning = true; if (useOptOut && !useNsec3) { System.err.println("Opt-Out not supported without NSEC3 -- ignored."); useOptOut = false; } if (cli.hasOption('F')) fullySignKeyset = true; if ((optstr = cli.getOptionValue('d')) != null) { keysetDirectory = new File(optstr); if (!keysetDirectory.isDirectory()) { System.err.println("error: " + optstr + " is not a directory"); usage(); } } 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 = CLBase.convertDuration(null, optstr); } else { // default is now - 1 hour. start = new Date(System.currentTimeMillis() - (3600 * 1000)); } if ((optstr = cli.getOptionValue('e')) != null) { expire = CLBase.convertDuration(start, optstr); } else { expire = CLBase.convertDuration(start, "+2592000"); // 30 days } outputfile = cli.getOptionValue('f'); kskFiles = cli.getOptionValues('k'); if ((optstr = cli.getOptionValue('I')) != null) { File includeNamesFile = new File(optstr); try { includeNames = getNameList(includeNamesFile); } catch (IOException e) { throw new ParseException(e.getMessage()); } } if ((optstr = cli.getOptionValue('S')) != null) { salt = base16.fromString(optstr); if (salt == null && !optstr.equals("-")) { System.err.println("error: salt is not valid hexidecimal."); usage(); } } if ((optstr = cli.getOptionValue('R')) != null) { int length = parseInt(optstr, 0); if (length > 0 && length <= 255) { Random random = new Random(); salt = new byte[length]; random.nextBytes(salt); } } if ((optstr = cli.getOptionValue("iterations")) != null) { iterations = parseInt(optstr, iterations); if (iterations < 0 || iterations > 8388607) { System.err.println("error: iterations value is invalid"); usage(); } } if ((optstr = cli.getOptionValue("ds-digest")) != null) { digest_id = parseInt(optstr, -1); if (digest_id < 0) { System.err.println("error: DS digest ID is not a valid identifier"); usage(); } } if ((optstr = cli.getOptionValue("nsec3paramttl")) != null) { nsec3paramttl = parseInt(optstr, -1); } String[] files = cli.getArgs(); if (files.length < 1) { System.err.println("error: missing zone file and/or key files"); usage(); } zonefile = files[0]; if (files.length > 1) { keyFiles = new String[files.length - 1]; System.arraycopy(files, 1, keyFiles, 0, files.length - 1); } } } /** * 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 verifyZoneSigs(Name zonename, List<Record> records, List<DnsKeyPair> keypairs) { boolean secure = true; DnsSecVerifier verifier = new DnsSecVerifier(); for (DnsKeyPair pair : keypairs) { verifier.addTrustedKey(pair); } verifier.setVerifyAllSigs(true); List<RRset> rrsets = SignUtils.assembleIntoRRsets(records); for (RRset rrset : rrsets) { // skip unsigned rrsets. if (!rrset.sigs().hasNext()) continue; boolean result = verifier.verify(rrset); if (!result) { log.fine("Signatures did not verify for RRset: " + 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<DnsKeyPair> 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<DnsKeyPair> keys = new ArrayList<DnsKeyPair>(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 List<DnsKeyPair> getKeys(List<Record> dnskeyrrs, File inDirectory) throws IOException { List<DnsKeyPair> res = new ArrayList<DnsKeyPair>(); for (Record r : dnskeyrrs) { if (r.getType() != Type.DNSKEY) continue; // Construct a public-key-only DnsKeyPair just so we can calculate the // base name. DnsKeyPair pub = new DnsKeyPair((DNSKEYRecord) r); DnsKeyPair pair = BINDKeyUtils.loadKeyPair(BINDKeyUtils.keyFileBase(pub), inDirectory); if (pair != null) { res.add(pair); } } if (res.size() > 0) return res; return null; } 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<DnsKeyPair> findZoneKeys(File inDirectory, Name zonename) throws IOException { if (inDirectory == null) { inDirectory = new File("."); } // get the list of "K<zone>.*.private files. FileFilter filter = new KeyFileFilter(zonename); File[] files = inDirectory.listFiles(filter); // read in all of the records ArrayList<DnsKeyPair> keys = new ArrayList<DnsKeyPair>(); 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; } /** * This is an implementation of a file filter used for finding BIND 9-style * keyset-* files. */ private static class KeysetFileFilter implements FileFilter { public boolean accept(File pathname) { if (!pathname.isFile()) return false; String name = pathname.getName(); if (name.startsWith("keyset-")) return true; return false; } } /** * Load keysets (which contain delegation point security info). * * @param inDirectory * the directory to look for the keyset files (may be null, in * which * case it defaults to looking in the current working directory). * @param zonename * the name of the zone we are signing, so we can ignore keysets * that * do not belong in the zone. * @return a list of {@link org.xbill.DNS.Record}s found in the keyset * files. */ private static List<Record> getKeysets(File inDirectory, Name zonename) throws IOException { if (inDirectory == null) { inDirectory = new File("."); } // get the list of "keyset-" files. FileFilter filter = new KeysetFileFilter(); File[] files = inDirectory.listFiles(filter); // read in all of the records ArrayList<Record> keysetRecords = new ArrayList<Record>(); for (int i = 0; i < files.length; i++) { List<Record> l = ZoneUtils.readZoneFile(files[i].getAbsolutePath(), zonename); keysetRecords.addAll(l); } // discard records that do not belong to the zone in question. for (Iterator<Record> i = keysetRecords.iterator(); i.hasNext();) { Record r = i.next(); if (!r.getName().subdomain(zonename)) { i.remove(); } } return keysetRecords; } /** * Load a list of DNS names from a file. * * @param nameListFile * the path of a file containing a bare list of DNS names. * @return a list of {@link org.xbill.DNS.Name} objects. */ private static List<Name> getNameList(File nameListFile) throws IOException { BufferedReader br = new BufferedReader(new FileReader(nameListFile)); List<Name> res = new ArrayList<Name>(); String line = null; while ((line = br.readLine()) != null) { try { Name n = Name.fromString(line); // force the name to be absolute. // FIXME: we should probably get some fancy logic here to // detect if the name needs the origin appended, or just the // root. if (!n.isAbsolute()) n = Name.concatenate(n, Name.root); res.add(n); } catch (TextParseException e) { log.severe("DNS Name parsing error:" + e); } } if (res.size() == 0) return null; return res; } /** * Determine if the given keypairs can be used to sign the zone. * * @param zonename * the zone origin. * @param keypairs * a list of {@link DnsKeyPair} objects that will be used to sign * the * zone. * @return true if the keypairs valid. */ private static boolean keyPairsValidForZone(Name zonename, List<DnsKeyPair> keypairs) { if (keypairs == null) return true; // technically true, I guess. for (DnsKeyPair kp : keypairs) { Name keyname = kp.getDNSKEYRecord().getName(); if (!keyname.equals(zonename)) { return false; } } return true; } public void execute() throws Exception { // Read in the zone List<Record> records = ZoneUtils.readZoneFile(state.zonefile, null); if (records == null || records.size() == 0) { System.err.println("error: empty zone file"); state.usage(); } // calculate the zone name. Name zonename = ZoneUtils.findZoneName(records); if (zonename == null) { System.err.println("error: invalid zone file - no SOA"); state.usage(); } // Load the key pairs. List<DnsKeyPair> keypairs = getKeys(state.keyFiles, 0, state.keyDirectory); List<DnsKeyPair> kskpairs = getKeys(state.kskFiles, 0, state.keyDirectory); // If we didn't get any keys on the command line, look at the zone apex for // any public keys. if (keypairs == null && kskpairs == null) { List<Record> dnskeys = ZoneUtils.findRRs(records, zonename, Type.DNSKEY); keypairs = getKeys(dnskeys, state.keyDirectory); } // If we *still* don't have any key pairs, look for keys the key directory // that match if (keypairs == null && kskpairs == null) { keypairs = findZoneKeys(state.keyDirectory, zonename); } // If we don't have any KSKs, but we do have more than one zone // signing key (presumably), presume that the zone signing keys // are just not differentiated and try to figure out which keys // are actually ksks by looking at the SEP flag. if ((kskpairs == null || kskpairs.size() == 0) && keypairs != null && keypairs.size() > 1) { for (Iterator<DnsKeyPair> i = keypairs.iterator(); i.hasNext();) { DnsKeyPair pair = i.next(); DNSKEYRecord kr = pair.getDNSKEYRecord(); if ((kr.getFlags() & DNSKEYRecord.Flags.SEP_KEY) != 0) { if (kskpairs == null) kskpairs = new ArrayList<DnsKeyPair>(); kskpairs.add(pair); i.remove(); } } } // If there are no ZSKs defined at this point (yet there are KSKs // provided), all KSKs will be treated as ZSKs, as well. if (keypairs == null || keypairs.size() == 0) { keypairs = kskpairs; } // If there *still* aren't any ZSKs defined, bail. if (keypairs == null || keypairs.size() == 0) { System.err.println("No zone signing keys could be determined."); state.usage(); } // default the output file, if not set. if (state.outputfile == null && !state.zonefile.equals("-")) { if (zonename.isAbsolute()) { state.outputfile = zonename + "signed"; } else { state.outputfile = zonename + ".signed"; } } // Verify that the keys can be in the zone. if (!keyPairsValidForZone(zonename, keypairs) || !keyPairsValidForZone(zonename, kskpairs)) { System.err.println("error: specified keypairs are not valid for the zone."); state.usage(); } // We force the signing keys to be in the zone by just appending // them to the zone here. Currently JCEDnsSecSigner.signZone // removes duplicate records. if (kskpairs != null) { for (DnsKeyPair pair : kskpairs) { records.add(pair.getDNSKEYRecord()); } } if (keypairs != null) { for (DnsKeyPair pair : keypairs) { records.add(pair.getDNSKEYRecord()); } } // read in the keysets, if any. List<Record> keysetrecs = getKeysets(state.keysetDirectory, zonename); if (keysetrecs != null) { records.addAll(keysetrecs); } JCEDnsSecSigner signer = new JCEDnsSecSigner(state.verboseSigning); // Sign the zone. List<Record> signed_records; if (state.useNsec3) { signed_records = signer.signZoneNSEC3(zonename, records, kskpairs, keypairs, state.start, state.expire, state.fullySignKeyset, state.useOptOut, state.includeNames, state.salt, state.iterations, state.digest_id, state.nsec3paramttl); } else { signed_records = signer.signZone(zonename, records, kskpairs, keypairs, state.start, state.expire, state.fullySignKeyset, state.digest_id); } // write out the signed zone ZoneUtils.writeZoneFile(signed_records, state.outputfile); if (state.verifySigs) { // FIXME: ugh. if (kskpairs != null) { keypairs.addAll(kskpairs); } log.fine("verifying generated signatures"); boolean res = verifyZoneSigs(zonename, 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) { SignZone tool = new SignZone(); tool.state = new CLIState(); tool.run(tool.state, args); } }