Cli improvements (#17)

* add config file processing, refactor CLIBase some

* fix algorithm aliases with key generation

* Refactor to remove CLIState et al, move CLI common statics to new Utils

* only use usage() for help, otherwise fail()

* add a universal command line client, build a one-jar to use it.

* bump the version

* update ChangeLog, README, README.TODO, minor fixes

* undo overzealous find/replace. sigh.

* fix use_large_exponent logic in KeyGen

* more fixes, minor improvements
This commit is contained in:
David Blacka 2024-04-07 21:12:56 -04:00 committed by GitHub
parent 2876649a4e
commit 1727d7c7d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1364 additions and 1152 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
build
bin/main
dist/
.classpath
.project
.gradle

View File

@ -1,3 +1,18 @@
2024-04-07 David Blacka <david@blacka.com>
* Released version 0.20
* Removed support for Gradle builds since the gradle config was
out-of-date
* Requires Java 15 or later for EdDSA support
* Supports DNSSEC algorithm 16 using the SunEC provider
* Supports a java properties-formatted config file. See
jdnssec-tools.properties.example for an example
* Updated to dnsjava 3.5.3
* Updated to commons-cli 1.6.0
* Added a "one-jar" distribution method, and a "univeral" CLI to
use with it.
* Formatting and linter suggestions
2024-03-25 David Blacka <davidb@verisign.com>
* Released version 0.19

View File

@ -11,5 +11,3 @@ This bit of code has been around since approximately 2005, and has been in "mini
* allowing for an external sort once the data is shown to be larger than X, and/or
* allowing for a memory-constrained internal sort that uses disk, and/or,
* figuring out how to let the JVM use *a lot* of memory.
* Add support for algorithm 16, perhaps refactor algorithm 15 support using bouncycastle.
* Note that our current dnsjava version, 3.5.1 has some support, although it isn't clear if it has sign/verify support.

View File

@ -8,9 +8,13 @@ This is a collection of DNSSEC tools written in Java. They are intended to be a
These tools depend upon DNSjava (<https://github.com/dnsjava/dnsjava>), the Jakarta Commons CLI and Logging libraries (<https://commons.apache.org/proper/commons-cli>), slf4j (<https://www.slf4j.org>), and Sun's Java Cryptography extensions. A copy of each of these libraries is included in the distribution.
See the "licenses" directory for the licensing information of this package and the other packages that are distributed with it.
See the "[licenses](https://github.com/dblacka/jdnssec-tools/tree/master/licenses)" directory for the licensing information of this package and the other packages that are distributed with it.
Getting started:
## Getting Started
### Using the binary distribution
The binary distributions can be downloaded from the [releases](https://github.com/dblacka/jdnssec-tools/releases) page. To use it;
1. Unpack the binary distribution:
@ -21,9 +25,11 @@ Getting started:
cd java-dnssec-tools-x.x.x
./bin/jdnssec-signzone -h
Building from source:
### Building from source
1. Unpack the source distribution, preferably into the same directory that the binary distribution was unpacked.
There is a source distribution also downloadable from the [releases](https://github.com/dblacka/jdnssec-tools/releases) page, but this should work with a clone of this repo.
1. (If downloaded) Unpack the source distribution, preferably into the same directory that the binary distribution was unpacked.
tar zxvf java-dnssec-tools-x.x.x-src.tar.gz
@ -32,17 +38,19 @@ Building from source:
ant
4. You can build the distribution tarballs with 'ant dist'. You can run the tools directly from the build area (without building the jdnssec-tools.jar file) by using the ./bin/\_jdnssec_* wrappers.
5. Alternatively, build the project using gradle:
gradlew clean
gradlew assemble -i
The resulting jar file gets generated in build/libs.
4. You can build the distribution tarballs with 'ant dist', although the main `ant` build command will have built the primary jar file.
The source for this project is available in git on github: <https://github.com/dblacka/jdnssec-tools>
### Using the one-jar distribution
As of version 0.20, there is a one-jar (aka an executable jar) as part of the distribution. It can also be downloaded from the [releases](https://github.com/dblacka/jdnssec-tools/releases) page.
1. Fetch the one-jar distribution.
2. Invoke with `java -jar jdnssec-tools-x.x.x.jar`
java -jar jdnssec-tools-x.x.x.jar signzone -h
---
Questions or comments may be directed to the author (<mailto:davidb@verisign.com>), or by creating issues in the [github issue tracker](https://github.com/dblacka/jdnssec-tools/issues).

View File

@ -1 +1 @@
version=0.19
version=0.20

View File

@ -1,19 +0,0 @@
#! /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.DSTool "$@"

View File

@ -1,19 +0,0 @@
#! /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.KeyGen "$@"

View File

@ -1,19 +0,0 @@
#! /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.KeyInfoTool "$@"

View File

@ -1,19 +0,0 @@
#! /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 "$@"

View File

@ -1,19 +0,0 @@
#! /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.SignZone "$@"

View File

@ -1,19 +0,0 @@
#! /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.VerifyZone "$@"

View File

@ -1,19 +0,0 @@
#! /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.ZoneFormat "$@"

22
bin/jdnssec-tools Executable file
View File

@ -0,0 +1,22 @@
#! /bin/sh
thisdir=$(dirname $0)
basedir=$(cd $thisdir/.. || exit; pwd)
ulimit_max=$(ulimit -H -n)
if [ $ulimit_max != "unlimited" ]; then
ulimit -n $ulimit_max
fi
# set the classpath
for i in "$basedir"/lib/*.jar "$basedir"/lib/*.zip "$basedir"/build/libs/*.jar; do
if ! [ -f $i ]; then continue; fi
if [ -z "$CLASSPATH" ]; then
CLASSPATH=$i
else
CLASSPATH="$CLASSPATH":"$i"
fi
done
export CLASSPATH
exec java com.verisignlabs.dnssec.cl.CLI "$@"

View File

@ -10,18 +10,20 @@
-->
<project default="compile" basedir=".">
<project default="build" basedir=".">
<property file="build.properties" />
<property file="VERSION" />
<property name="sectools-distname" value="jdnssec-tools-${version}" />
<property name="build.dir" value="build" />
<property name="build.dest" value="${build.dir}/classes" />
<property name="build.lib.dest" value="${build.dir}/libs" />
<property name="build.src" value="src/main/java" />
<property name="dist.dir" value="dist"/>
<property name="dist.name" value="jdnssec-tools-${version}" />
<property name="packages" value="com.verisignlabs.dnssec.*" />
<property name="doc.dir" value="docs" />
<property name="javadoc.dest" value="${doc.dir}/javadoc" />
@ -33,6 +35,7 @@
<pathelement location="${build.dest}" />
<fileset dir="${lib.dir}" includes="*.jar,*.zip" />
</path>
<property name="project.classpath" refid="project.classpath" />
<target name="prepare-src">
@ -40,7 +43,7 @@
<mkdir dir="${build.lib.dest}" />
</target>
<target name="sectools" depends="prepare-src" >
<target name="compile" depends="prepare-src" >
<javac srcdir="${build.src}"
destdir="${build.dest}"
classpathref="project.classpath"
@ -51,14 +54,29 @@
release="${build.java_version}" />
</target>
<target name="sectools-jar" depends="usage,sectools">
<target name="build-jar" depends="usage, compile">
<jar jarfile="${build.lib.dest}/jdnssec-tools.jar"
basedir="${build.dest}"
includes="com/verisignlabs/dnssec/" />
</target>
<target name="compile"
depends="usage,sectools-jar">
<target name="build"
depends="usage,build-jar">
</target>
<target name="build-onejar" depends="compile">
<jar destfile="${dist.dir}/${dist.name}.jar">
<zipfileset dir="${build.dest}" includes="**/*.class" />
<zipfileset src="${lib.dir}/dnsjava-3.5.3.jar" />
<zipfileset src="${lib.dir}/commons-cli-1.6.0.jar" />
<zipfileset src="${lib.dir}/slf4j-api-1.7.36.jar" />
<zipfileset src="${lib.dir}/slf4j-simple-1.7.36.jar" />
<manifest>
<attribute name="Main-Class"
value="com.verisignlabs.dnssec.cl.CLI" />
</manifest>
</jar>
</target>
<target name="javadoc" depends="usage">
@ -75,16 +93,21 @@
</javadoc>
</target>
<target name="clean" depends="usage">
<delete dir="${build.dest}" />
<delete dir="${build.lib.dest}" />
<delete dir="${dist.dir}" />
</target>
<target name="sectools-dist-prepare" depends="usage, compile, javadoc">
<mkdir dir="${sectools-distname}" />
<target name="dist-clean" depends="usage">
<delete dir="${dist.name}" />
</target>
<copy todir="${sectools-distname}">
<target name="dist-prepare" depends="usage, build, javadoc">
<mkdir dir="${dist.dir}" />
<mkdir dir="${dist.name}" />
<copy todir="${dist.name}">
<fileset dir=".">
<include name="bin/jdnssec-*" />
<include name="lib/*.jar" />
@ -99,37 +122,33 @@
</fileset>
</copy>
<copy todir="${sectools-distname}/lib">
<copy todir="${dist.name}/lib">
<fileset dir="${build.lib.dest}">
<include name="*.jar" />
</fileset>
</copy>
</target>
<target name="sectools-dist-clean">
<delete dir="${sectools-distname}" />
</target>
<patternset id="exec.files">
<include name="${sectools-distname}/bin/jdnssec-*" />
<include name="${dist.name}/bin/jdnssec-*" />
</patternset>
<patternset id="src.files">
<include name="${sectools-distname}/src/" />
<include name="${sectools-distname}/build.xml" />
<include name="${sectools-distname}/build.properties" />
<include name="${dist.name}/src/" />
<include name="${dist.name}/build.xml" />
<include name="${dist.name}/build.properties" />
</patternset>
<patternset id="bin.files">
<include name="${sectools-distname}/doc/" />
<include name="${sectools-distname}/lib/" />
<include name="${sectools-distname}/licenses/" />
<include name="${sectools-distname}/VERSION" />
<include name="${sectools-distname}/README" />
<include name="${dist.name}/doc/" />
<include name="${dist.name}/lib/" />
<include name="${dist.name}/licenses/" />
<include name="${dist.name}/VERSION" />
<include name="${dist.name}/README" />
</patternset>
<target name="sectools-bin-dist" depends="sectools-dist-prepare">
<tar destfile="${sectools-distname}.tar.gz" compression="gzip">
<target name="bin-dist" depends="dist-prepare">
<tar destfile="${dist.dir}/${dist.name}.tar.gz" compression="gzip">
<tarfileset mode="755" dir=".">
<patternset refid="exec.files" />
</tarfileset>
@ -139,21 +158,16 @@
</tar>
</target>
<target name="sectools-src-dist" depends="sectools-dist-prepare">
<tar destfile="${sectools-distname}-src.tar.gz"
compression="gzip">
<target name="src-dist" depends="dist-prepare">
<tar destfile="${dist.dir}/${dist.name}-src.tar.gz" compression="gzip">
<tarfileset dir=".">
<patternset refid="src.files" />
</tarfileset>
</tar>
</target>
<target name="sectools-dist"
depends="sectools-bin-dist,sectools-src-dist, sectools-dist-clean">
</target>
<target name="dist" depends="sectools-dist">
<target name="dist"
depends="bin-dist, src-dist, build-onejar, dist-clean">
</target>
<target name="usage">
@ -161,10 +175,11 @@
<echo message="jdnssec-tools v. ${version} Build System" />
<echo message="--------------------------------" />
<echo message="Available Targets:" />
<echo message=" compile (default) - compiles the source code, creates jar" />
<echo message=" build (default) - compiles the source code, creates main jar" />
<echo message=" javadoc - create javadoc from source" />
<echo message=" clean - delete class files" />
<echo message=" dist - package it up" />
<echo message=" onejar - build the executable jar" />
<echo message=" usage - this help message" />
<echo message=" " />
</target>

View File

@ -0,0 +1,93 @@
# An example properties file for jdnssec-tools
# Properties may be be scoped by the tool name, which is the name minus "jdnssec-"
# If unscoped, the same named property will be used by multiple tools
# Common properties
# log_level = warning
# verbose = true # same as log_level = fine (true) or log_level = warning (false)
# multiline = false
# algorithm aliasing is <scope>.alias.<new-mnemonic> = <orig-alg-id>:<alias-alg-id>
# alias.NEWALG = 8:100
# jdnssec-dstool properties
## These are all equivalent. Unscoped properties might apply to other tools
# dstool.digest_algorithm = 4
# digest_algorithm = 4 # applies to jdnssec-signzone, too
# dstool.digest_id = 4
# jdnssec-keygen properties
# keygen.use_large_exponent = true
# keygen.key_directory = .
# key_directory = /path/to/dnskey_files # applies to jdnssec-sign*
# keygen.algorithm = ED448
# keygen.keylength = 2048
# keygen.keylen = 2048 # same thing
# keygen.ttl = 3600
# jdnssec-keyinfotool
# no additional keys
# jdnssec-signkeyset
# signkeyset.verify = false
# signkeyset.key_directory = .
# signkeyset.start = -300
# signkeyset.inception = 1712424863
# signkeyset.expire = +604800
# jdnssec-signrrset
# signrrset.verify_signatures = false
# signrrset.verify = false # same thing
# signrrset.key_directory = .
# signrrset.start = now
# signrrset.inception = now # same thing
# signrrset.expire = now+3600
# jdnssec-signzone
# signzone.verify_signatures = false
# signzone.verify = false # same thing
# signzone.use_nsec3 = false
# signzone.nsec3 = false # same thing
# signzone.use_opt_out = false
# signzone.opt_out = false # same thing
# signzone.verbose_signing = false
# signzone.fully_sign_keyset = false
# signzone.fully_sign = false # same thing
# signzone.key_directory = .
# signzone.keydir = . # same thing
# signzone.start = now
# signzone.inception = now
# signzone.expire = now+3600
# signzone.nsec3_salt = DEADBEEF
# signzone.salt = DEADBEEF # same thing
# signzone.nsec3_random_salt_length = 6
# signzone.nsec3_salt_length = 6 # same thing
# signzone.random_salt_length = 6 # same thing
# signzone.nsec3_iterations = 0
# signzone.iterations = 0 # same thing
# signzone.digest_algorithm = 4
# signzone.digest_id = 4 # same thing
# signzone.nsec3param_ttl = 86400
# signzone.include_names_file = /path/to/include-names
# signzone.include_names = /path/to/include-names # same thing
# jdnssec-verifyzone
# verifyzone.ignore_time = false
# verifyzone.ignore_duplicate_rrs = false
# verifyzone.ignore_duplicates = false # same thing
# verifyzone.start_fudge = 0
# verifyzone.expire_fudge = 0
# verifyzone.current_time = now
# jdnssec-zoneformat
# zoneformat.assign_nsec3_owners = false
# zoneformat.assign_owners = false # same thing

View File

@ -17,11 +17,11 @@
package com.verisignlabs.dnssec.cl;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date;
import java.util.TimeZone;
import java.util.Properties;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
@ -48,8 +48,19 @@ import com.verisignlabs.dnssec.security.DnsKeyAlgorithm;
* subclass variant of the CLIState and call run().
*/
public abstract class CLBase {
protected static Logger staticLog = Logger.getLogger(CLBase.class.getName());
protected Logger log;
protected Logger log = Logger.getLogger(this.getClass().toString());
protected Options opts;
protected String name;
protected String usageStr;
protected Properties props;
protected CommandLine cli;
protected CLBase(String name, String usageStr) {
this.name = name;
this.usageStr = usageStr;
setup();
}
/**
* This is a very simple log formatter that simply outputs the log level and
@ -70,253 +81,319 @@ public abstract class CLBase {
}
}
/** This is the base set of command line options provided to all subclasses. */
private void setupCommonOptions() {
// Set up the standard set of options that all jdnssec command line tools will
// implement.
// boolean options
opts.addOption("h", "help", false, "Print this message.");
opts.addOption("m", "multiline", false,
"Output DNS records using 'multiline' format");
opts.addOption(Option.builder("l").longOpt("log-level").argName("level").hasArg()
.desc("set the logging level with either java.util.logging levels, or 0-6").build());
opts.addOption(Option.builder("v").longOpt("verbose").desc(
"set as verbose (log-level = fine)").build());
opts.addOption(Option.builder("c").longOpt("config").argName("file").hasArg()
.desc("configuration file (format: java properties)").build());
opts.addOption(Option.builder("A").hasArg().argName("alias:original:mnemonic").longOpt("alg-alias")
.desc("Define an alias for an algorithm").build());
}
/**
* This is a base class for command line parsing state. Subclasses should
* override setupOptions and processOptions.
* This is an overridable method for subclasses to add their own command line
* options.
*/
public static class CLIStateBase {
protected Options opts;
protected String usageStr;
protected abstract void setupOptions();
/**
* The base constructor. This will setup the command line options.
*
* @param usage
* The command line usage string (e.g.,
* "jdnssec-foo [..options..] zonefile")
*/
public CLIStateBase(String usage) {
usageStr = usage;
setup();
/**
* Initialize the command line options
*/
public void setup() {
opts = new Options();
setupCommonOptions();
setupOptions();
}
/**
* This is the main method for parsing the command line arguments. Subclasses
* generally override processOptions() rather than this method. This method
* creates the parsing objects and processes the common options.
*
* @param args The command line arguments.
*/
public void parseCommandLine(String[] args) {
String[] logLevelOptionKeys = { "log_level", "log-level" };
String[] multilineOptionKeys = { "multiline" };
CommandLineParser parser = new DefaultParser();
try {
cli = parser.parse(opts, args);
} catch (UnrecognizedOptionException e) {
fail("unknown option encountered: " + e.getMessage());
} catch (AlreadySelectedException e) {
fail("mutually exclusive options have been selected:\n " + e.getMessage());
} catch (ParseException e) {
fail("unable to parse command line: " + e);
}
/** This is the base set of command line options provided to all subclasses. */
private void setup() {
// Set up the standard set of options that all jdnssec command line tools will
// implement.
opts = new Options();
// boolean options
opts.addOption("h", "help", false, "Print this message.");
opts.addOption("m", "multiline", false,
"Output DNS records using 'multiline' format");
opts.addOption(Option.builder("v").longOpt("verbose").argName("level").hasArg().desc(
"verbosity level -- 0: silence, 1: error, 2: warning, 3: info, 4/5: fine, 6: finest; default: 2 (warning)")
.build());
opts.addOption(Option.builder("A").hasArg().argName("alias:original:mnemonic").longOpt("alg-alias")
.desc("Define an alias for an algorithm").build());
setupOptions(opts);
if (cli.hasOption('h')) {
usage();
}
/**
* This is an overridable method for subclasses to add their own command
* line options.
*
* @param opts
* the options object to add (via OptionBuilder, typically) new
* options to.
*/
protected void setupOptions(Options opts) {
// Subclasses generally override this.
String loadedConfig = loadConfig(cli.getOptionValue('c'));
Logger rootLogger = Logger.getLogger("");
// we set log level with both --log-level and -v/--verbose.
String logLevel = cliOption("log-level", logLevelOptionKeys, null);
if (logLevel == null) {
logLevel = cli.hasOption("v") ? "fine" : "warning";
}
setLogLevel(rootLogger, logLevel);
for (Handler h : rootLogger.getHandlers()) {
h.setLevel(rootLogger.getLevel());
h.setFormatter(new BareLogFormatter());
}
/**
* This is the main method for parsing the command line arguments.
* Subclasses generally override processOptions() rather than this method.
* This method create the parsing objects and processes the standard
* options.
*
* @param args
* The command line arguments.
* @throws ParseException
*/
public void parseCommandLine(String[] args) throws ParseException {
CommandLineParser parser = new DefaultParser();
CommandLine cli = parser.parse(opts, args);
if (loadedConfig != null) {
log.info("Loaded config file: " + loadedConfig);
}
if (cli.hasOption('h')) {
usage();
if (cliBooleanOption("m", multilineOptionKeys, false)) {
org.xbill.DNS.Options.set("multiline");
}
processAliasOptions();
processOptions();
}
/**
* Process additional tool-specific options. Subclasses generally override
* this.
*/
protected abstract void processOptions();
/**
* Load a configuration (java properties) file for jdnssec-tools. Returns
* the path of the loaded file.
*
* @param configFile a given path to a config file. This will be considered
* first.
* @return The path of the file that was actually loaded, or null if no config
* file was loaded.
*/
protected String loadConfig(String configFile) {
// Do not load config files twice
if (props != null) {
return null;
}
props = new Properties();
String[] configFiles = { configFile, "jdnssec-tools.properties", ".jdnssec-tools.properties",
System.getProperty("user.home") + "/.jdnssec-tools.properties" };
File f = null;
for (String fname : configFiles) {
if (fname == null) {
continue;
}
f = new File(fname);
if (!f.canRead()) {
continue;
}
Logger rootLogger = Logger.getLogger("");
int value = parseInt(cli.getOptionValue('v'), -1);
try (FileInputStream stream = new FileInputStream(f)) {
props.load(stream);
break; // load the first config file found in our list
} catch (IOException e) {
log.warning("Could not read config file " + f.getName() + ": " + e);
}
}
switch (value) {
if (f != null) {
return f.getPath();
}
return null;
}
protected void fail(String errorMessage) {
log.severe(errorMessage);
System.exit(64);
}
/** Print out the usage and help statements, then quit. */
public void usage() {
HelpFormatter f = new HelpFormatter();
PrintWriter out = new PrintWriter(System.err);
// print our own usage statement:
f.printHelp(out, 120, usageStr, null, opts, HelpFormatter.DEFAULT_LEFT_PAD,
HelpFormatter.DEFAULT_DESC_PAD, null);
out.flush();
System.exit(0);
}
/**
* Set the logging level based on a string value
*
* @param logger The logger to set -- usually the rootLogger
* @param levelStr A level string that is either an integer from 0 to 6, or a
* java.util.logging log level string (severe, warning, info,
* fine, finer,
* finest).
*/
private void setLogLevel(Logger logger, String levelStr) {
Level level;
int internalLogLevel = Utils.parseInt(levelStr, -1);
if (internalLogLevel != -1) {
switch (internalLogLevel) {
case 0:
rootLogger.setLevel(Level.OFF);
level = Level.OFF;
break;
case 1:
rootLogger.setLevel(Level.SEVERE);
level = Level.SEVERE;
break;
case 2:
default:
rootLogger.setLevel(Level.WARNING);
level = Level.WARNING;
break;
case 3:
rootLogger.setLevel(Level.INFO);
level = Level.INFO;
break;
case 4:
rootLogger.setLevel(Level.CONFIG);
level = Level.FINE;
break;
case 5:
rootLogger.setLevel(Level.FINE);
break;
case 6:
rootLogger.setLevel(Level.ALL);
break;
level = Level.ALL;
}
// I hate java.util.logging, btw.
for (Handler h : rootLogger.getHandlers()) {
h.setLevel(rootLogger.getLevel());
h.setFormatter(new BareLogFormatter());
} else {
try {
level = Level.parse(levelStr.toUpperCase());
} catch (IllegalArgumentException e) {
System.err.println("Verbosity level '" + levelStr + "' not recognized");
level = Level.WARNING;
}
}
logger.setLevel(level);
}
if (cli.hasOption('m')) {
org.xbill.DNS.Options.set("multiline");
}
/**
* Process both property file based alias definitions and command line alias
* definitions
*/
protected void processAliasOptions() {
DnsKeyAlgorithm algs = DnsKeyAlgorithm.getInstance();
// First parse any command line options
// those look like '-A <alias-num>:<orig-num>:<mnemonic>', e.g., '-A
// 21:13:ECDSAP256-NSEC6'
String[] optstrs = null;
if ((optstrs = cli.getOptionValues('A')) != null) {
for (String value : optstrs) {
String[] valueComponents = value.split(":");
int aliasAlg = Utils.parseInt(valueComponents[0], -1);
int origAlg = Utils.parseInt(valueComponents[1], -1);
String mnemonic = valueComponents[2];
String[] optstrs = null;
if ((optstrs = cli.getOptionValues('A')) != null) {
for (int i = 0; i < optstrs.length; i++) {
addArgAlias(optstrs[i]);
if (mnemonic != null && origAlg >= 0 && aliasAlg >= 0) {
algs.addAlias(aliasAlg, mnemonic, origAlg);
}
}
processOptions(cli);
}
/**
* Process additional tool-specific options. Subclasses generally override
* this.
*
* @param cli
* The {@link CommandLine} object containing the parsed command
* line state.
*/
protected void processOptions(CommandLine cli) throws ParseException {
// Subclasses generally override this.
}
// Next see if we have any alias options in properties
// Those look like 'signzone.alias.<alias-mnemonic> =
// <orig-alg-num>:<alias-alg-num>'
for (String key : props.stringPropertyNames()) {
if (key.startsWith(name + ".alias.") || key.startsWith("alias.")) {
String[] keyComponents = key.split("\\.");
String mnemonic = keyComponents[keyComponents.length - 1];
String[] valueComponents = props.getProperty(key).split(":");
int origAlg = Utils.parseInt(valueComponents[0], -1);
int aliasAlg = Utils.parseInt(valueComponents[1], -1);
/** Print out the usage and help statements, then quit. */
public void usage() {
HelpFormatter f = new HelpFormatter();
PrintWriter out = new PrintWriter(System.err);
// print our own usage statement:
f.printHelp(out, 75, usageStr, null, opts, HelpFormatter.DEFAULT_LEFT_PAD,
HelpFormatter.DEFAULT_DESC_PAD, null);
out.flush();
System.exit(64);
}
protected void addArgAlias(String s) {
if (s == null)
return;
DnsKeyAlgorithm algs = DnsKeyAlgorithm.getInstance();
String[] v = s.split(":");
if (v.length < 2)
return;
int alias = parseInt(v[0], -1);
if (alias <= 0)
return;
int orig = parseInt(v[1], -1);
if (orig <= 0)
return;
String mn = null;
if (v.length > 2)
mn = v[2];
algs.addAlias(alias, mn, orig);
}
}
public static int parseInt(String s, int def) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return def;
}
}
public static long parseLong(String s, long def) {
try {
return Long.parseLong(s);
} catch (NumberFormatException e) {
return def;
if (mnemonic != null && origAlg >= 0 && aliasAlg >= 0) {
algs.addAlias(aliasAlg, mnemonic, origAlg);
}
}
}
}
/**
* Calculate a date/time from a command line time/offset duration string.
* Given a parsed command line, option, and list of possible config
* properties, and a default value, determine value for the option
*
* @param start
* the start time to calculate offsets from.
* @param duration
* the time/offset string to parse.
* @return the calculated time.
* @param option The option name
* @param properties A list of configuration parameters that we would like
* to use for this option, from most preferred to least.
* @param defaultValue A default value to return if either the option or
* config value cannot be parsed, or neither are present.
* @return The found value, or the default value.
*/
public static Instant convertDuration(Instant start, String duration) throws ParseException {
if (start == null) {
start = Instant.now();
protected String cliOption(String option, String[] properties, String defaultValue) {
if (cli.hasOption(option)) {
return cli.getOptionValue(option);
}
if (duration.startsWith("now")) {
start = Instant.now();
if (duration.indexOf("+") < 0)
return start;
duration = duration.substring(3);
for (String property : properties) {
// first look up the scoped version of the property
String value = props.getProperty(name + "." + property);
if (value != null) {
return value;
}
value = props.getProperty(property);
if (value != null) {
return value;
}
}
return defaultValue;
}
if (duration.startsWith("+")) {
long offset = parseLong(duration.substring(1), 0);
return start.plusSeconds(offset);
}
/**
* Given a parsed command line, option, and list of possible config
* properties, determine the value for the option, converting the value to
* long.
*/
protected long cliLongOption(String option, String[] properties, long defaultValue) {
String value = cliOption(option, properties, Long.toString(defaultValue));
return Utils.parseLong(value, defaultValue);
}
// This is a heuristic to distinguish UNIX epoch times from the zone file
// format standard (which is length == 14)
if (duration.length() <= 10) {
long epoch = parseLong(duration, 0);
return Instant.ofEpochSecond(epoch);
}
/**
* Given a parsed command line, option, and list of possible config
* properties, determine the value for the option, converting the value to
* int.
*/
protected int cliIntOption(String option, String[] properties, int defaultValue) {
String value = cliOption(option, properties, Integer.toString(defaultValue));
return Utils.parseInt(value, defaultValue);
}
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMddHHmmss");
dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
try {
Date parsedDate = dateFormatter.parse(duration);
return parsedDate.toInstant();
} catch (java.text.ParseException e) {
throw new ParseException(e.getMessage());
/**
* Given a parsed command line, option, and list of possible config
* properties, determine the value for the option, converting the value to
* a boolean.
*/
protected boolean cliBooleanOption(String option, String[] properties, boolean defaultValue) {
if (cli.hasOption(option)) {
return true;
}
String value = cliOption(option, properties, Boolean.toString(defaultValue));
return Boolean.parseBoolean(value);
}
public abstract void execute() throws Exception;
public void run(CLIStateBase state, String[] args) {
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();
}
public void run(String[] args) {
parseCommandLine(args);
log = Logger.getLogger(this.getClass().toString());
try {

View File

@ -0,0 +1,84 @@
package com.verisignlabs.dnssec.cl;
public class CLI {
private SubCommandType subCommand = null;
private String commandSetStr = null;
enum SubCommandType {
DSTOOL, KEYGEN, KEYINFO, SIGNKEYSET, SIGNRRSET, SIGNZONE, VERIFYZONE, ZONEFORMAT;
}
public CLI(String name, String usageStr) {
StringBuilder sb = new StringBuilder();
for (SubCommandType type : SubCommandType.class.getEnumConstants()) {
sb.append(type.toString().toLowerCase());
sb.append(" ");
}
commandSetStr = sb.toString().trim();
}
private void fail(String errorMessage){
System.err.println("ERROR: " + errorMessage);
System.exit(2);
}
public void run(String[] args) {
String[] subCommandArgs = null;
if (args.length < 1) {
fail("missing command: must be one of: " + commandSetStr);
}
String command = args[0];
if (command.equals("-h")) {
System.err.println("usage: jdnssec-tools <command> <command args..>");
System.err.println(" <command> is one of: " + commandSetStr);
System.exit(0);
}
try {
subCommand = SubCommandType.valueOf(command.toUpperCase());
} catch (IllegalArgumentException e) {
fail("unrecognized command '" + command + "': must be one of: " + commandSetStr);
}
subCommandArgs = new String[args.length - 1];
System.arraycopy(args, 1, subCommandArgs, 0, args.length - 1);
CLBase cmd = null;
switch(subCommand) {
case DSTOOL:
cmd = new DSTool("dstool", "jdnssec-tools dstool [..options..] keyfile [keyfile..]");
break;
case KEYGEN:
cmd = new KeyGen("keygen", "jdnssec-tools keygen [..options..] zonename");
break;
case KEYINFO:
cmd = new KeyInfoTool("keyinfotool", "jdnssec-tools keyinfo [..options..] keyfile");
break;
case SIGNKEYSET:
cmd = new SignKeyset("signkeyset", "jdnssec-tools signkeyset [..options..] dnskeyset_file [key_file ...]");
break;
case SIGNRRSET:
cmd = new SignRRset("signrrset", "jdnssec-tools signrrset [..options..] rrset_file key_file [key_file ...]");
break;
case SIGNZONE:
cmd = new SignZone("signzone", "jdnssec-tools signzone [..options..] zone_file [key_file ...]");
break;
case VERIFYZONE:
cmd = new VerifyZone("verifyzone", "jdnssec-tools verifyzone [..options..] zonefile");
break;
case ZONEFORMAT:
cmd = new ZoneFormat("zoneformat", "jdnssec-tools zoneformat [..options..] zonefile");
break;
default:
fail("commmand " + command + " has not been implemented.");
break;
}
cmd.run(subCommandArgs);
}
public static void main(String[] args) {
CLI cli = new CLI("cli", "jdnssec-tools <command> [..args..]");
cli.run(args);
}
}

View File

@ -18,11 +18,10 @@
package com.verisignlabs.dnssec.cl;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.xbill.DNS.CDSRecord;
import org.xbill.DNS.DLVRecord;
import org.xbill.DNS.DNSKEYRecord;
@ -40,7 +39,15 @@ import com.verisignlabs.dnssec.security.SignUtils;
* @author David Blacka
*/
public class DSTool extends CLBase {
private CLIState state;
private dsType createType = dsType.DS;
private String outputfile = null;
private String[] keynames = null;
private int digestId = DNSSEC.Digest.SHA256;
private long dsTTL = -1;
public DSTool(String name, String usageStr) {
super(name, usageStr);
}
/** There are several records that are based on DS. */
protected enum dsType {
@ -52,80 +59,77 @@ public class DSTool extends CLBase {
* state.
*/
protected static class CLIState extends CLIStateBase {
public dsType createType = dsType.DS;
public String outputfile = null;
public String keyname = null;
public int digestId = DNSSEC.Digest.SHA256;
public CLIState() {
super("jdnssec-dstool [..options..] keyfile");
}
/**
* Set up the command line options.
*
* @return a set of command line options.
*/
@Override
protected void setupOptions(Options opts) {
opts.addOption(Option.builder("D").longOpt("dlv").desc("Generate a DLV record instead.").build());
opts.addOption(Option.builder("C").longOpt("cds").desc("Generate a CDS record instead").build());
opts.addOption(
Option.builder("d").hasArg().argName("id").longOpt("digest").desc("The digest algorithm to use").build());
opts.addOption(Option.builder("f").hasArg().argName("file").longOpt("output").desc("output to file").build());
}
@Override
protected void processOptions(CommandLine cli)
throws org.apache.commons.cli.ParseException {
outputfile = cli.getOptionValue('f');
if (cli.hasOption("dlv")) {
createType = dsType.DLV;
} else if (cli.hasOption("cds")) {
createType = dsType.CDS;
}
String optstr = cli.getOptionValue('d');
if (optstr != null)
digestId = DNSSEC.Digest.value(optstr);
String[] args = cli.getArgs();
if (args.length < 1) {
System.err.println("error: missing key file ");
usage();
}
keyname = args[0];
}
/**
* Set up the command line options.
*
* @return a set of command line options.
*/
protected void setupOptions() {
opts.addOption(Option.builder("D").longOpt("dlv").desc("Generate a DLV record instead.").build());
opts.addOption(Option.builder("C").longOpt("cds").desc("Generate a CDS record instead").build());
opts.addOption(
Option.builder("d").hasArg().argName("id").longOpt("digest").desc("The digest algorithm to use").build());
opts.addOption(Option.builder("f").hasArg().argName("file").longOpt("output").desc("output to file").build());
opts.addOption(Option.builder("T").longOpt("ttl").hasArg().desc("TTL to use for generated DS/CDS record").build());
}
public void execute() throws Exception {
DnsKeyPair key = BINDKeyUtils.loadKey(state.keyname, null);
protected void processOptions() {
String[] digestAlgOptionKeys = { "digest_algorithm", "digest_id" };
String[] dsTTLOptionKeys = { "ds_ttl", "ttl" };
outputfile = cli.getOptionValue('f');
if (cli.hasOption("dlv")) {
createType = dsType.DLV;
} else if (cli.hasOption("cds")) {
createType = dsType.CDS;
}
String digestValue = cliOption("d", digestAlgOptionKeys, Integer.toString(digestId));
digestId = DNSSEC.Digest.value(digestValue);
dsTTL = cliLongOption("ttl", dsTTLOptionKeys, dsTTL);
String[] args = cli.getArgs();
if (args.length < 1) {
fail("missing key file");
}
keynames = args;
}
public void createDS(String keyname) throws IOException {
DnsKeyPair key = BINDKeyUtils.loadKey(keyname, null);
DNSKEYRecord dnskey = key.getDNSKEYRecord();
if ((dnskey.getFlags() & DNSKEYRecord.Flags.SEP_KEY) == 0) {
log.warning("DNSKEY is not an SEP-flagged key.");
log.warning("DNSKEY " + keyname + " is not an SEP-flagged key.");
}
DSRecord ds = SignUtils.calculateDSRecord(dnskey, state.digestId, dnskey.getTTL());
Record res = ds;
long ttl = dsTTL < 0 ? dnskey.getTTL() : dsTTL;
DSRecord ds = SignUtils.calculateDSRecord(dnskey, digestId, ttl);
Record res;
if (state.createType == dsType.DLV) {
log.fine("creating DLV.");
DLVRecord dlv = new DLVRecord(ds.getName(), ds.getDClass(), ds.getTTL(), ds.getFootprint(), ds.getAlgorithm(),
ds.getDigestID(), ds.getDigest());
res = dlv;
} else if (state.createType == dsType.CDS) {
log.fine("creating CDS.");
CDSRecord cds = new CDSRecord(ds.getName(), ds.getDClass(), ds.getTTL(), ds.getFootprint(), ds.getAlgorithm(),
ds.getDClass(), ds.getDigest());
res = cds;
switch (createType) {