#!/bin/sh

# dnsseczonetool : Simple DNSSEC key management and zone signing tool
#
# $Id: dnsseczonetool.20100917,v 1.1 2011/02/13 07:06:25 fujiwara Exp $
#
# http://member.wide.ad.jp/~fujiwara/dnssec/dnsseczonetool
#
# Installation and Configuration:
#   1. Copy dnsseczonetool into some directory.
#   2. Create dnsseczonetool.conf into the same directory of dnsseczonetool.
#
#   dnsseczonetool.conf:
#     MASTERDIR: Zone file directory
#                Default: MASTERDIR="/etc/namedb/master"
#     KSK_PARAM: Default dnssec-keygen's options for KSK
#                Default: KSK_PARAM_DEFAULT="-n zone -a RSASHA1 -b 2048 -f ksk"
#     KSK_PARAM_$zone: dnssec-keygen's options for zone's KSK
#                Default: KSK_PARAM
#     ZSK_PARAM: Default dnssec-keygen's options for ZSK
#                Default: ZSK_PARAM_DEFAULT="-n zone -a RSASHA1 -b 1024"
#     ZSK_PARAM_$zone: dnssec-keygen's options for zone's ZSK
#                Default: ZSK_PARAM
#     SIGN_PARAM: Default dnssec-signzone options
#                Default: SIGN_PARAM_DEFAULT="-N unixtime"
#     SIGN_PARAM_$zone: dnssec-signzone options for zone
#                Default: SIGN_PARAM
#     DS_PARAM:  Default dsfromkey options for zone
#                Default: SIGN_PARAM_DEFAULT="-2"
#     DS_PARAM_$zone: dsfromkey options for zone
#                Default: SIGN_PARAM
#     keygen:    dnssec-keygen path
#                Default: keygen="/usr/local/sbin/dnssec-keygen"
#     signzone:  dnssec-signzone path
#                Default: signzone="/usr/local/sbin/dnssec-signzone"
#     dsfromkey: dnssec-dsfromkey path
#                Default: dsfromkey="/usr/local/sbin/dnssec-dsfromkey"
#     rndc:      rndc path
#                Default: rndc="/usr/local/sbin/rndc"
#     CONFIGDIR: directory where dnsseczonetool uses.
#                Default: CONFIGDIR="$MASTERDIR/config"
#     KEYDIR:    directory where dnsseczonetool puts zone keys.
#                Default: KEYDIR="$MASTERDIR/config/keydir"
#     KEYBACKUPDIR: directory where dnsseczonetool puts old keys.
#                Default: KEYBACKUPDIR="$MASTERDIR/config/backup"
#     RNDC_OPTION: rndc options or OFF
#                Default: RNDC_OPTION="-k $MASTERDIR/rndc.key"
#     ZONE_PREPROCESS: zone preprocess command
#                Default: cat
#     RELOADALL_COMMAND:  reload all command
#                Default: none
#     PRESERVE_REMOVED_KEY: NO|YES
#                Default: YES
#
#     caution: $zone is zone name
#              whose '.' and '-' characters are replaced by '_'.
#		 All zone name must be lowercase.
#
# Usage:
#        
#   1. Generate KSK and ZSK
#       dnsseczonetool keygen zone(s)
#
#   2. Sign zone using keys generated in step 1
#       dnsseczonetool sign zone(s)
#
#   3. Add next ZSK for ZSK rollover (generate new ZSK and sign with old key)
#       dnsseczonetool add-next-zsk zone(s)
#
#   4. ZSK Rollover (Change current ZSK as unused previous ZSK,
#                    and sign new ZSK generated by step.3)
#       dnsseczonetool zsk-rollover zone(s)
#
#   4'. ZSK Rollover 2 (Change current ZSK as unused previous ZSK,
#                              stand-by ZSK as current ZSK,
#			Generate new ZSK as a stand-by ZSK,
#                       and sign the zone by new ZSK.)
#       dnsseczonetool zskroll zone(s)
#
#   5. Add next KSK for KSK rollover (generate new KSK and sign with both keys)
#       dnsseczonetool add-next-ksk zone(s)
#
#   6. KSK Rollover (Remove old KSK and sign new KSK generated by step.5)
#       dnsseczonetool ksk-rollover zone(s)
#
#   7. Zone key status
#       dnsseczonetool status zone(s)
#
#   8. Zone key status-dnskey (Show KSK DNSKEY for DLV registration)
#       dnsseczonetool status-dnskey zone(s)

# Copyright (c) 2009 Kazunori Fujiwara <fujiwara@wide.ad.jp>.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

PROG=$0

DIR=`dirname $0`
CONFIGFILE="$DIR/dnsseczonetool.conf"

keygen="/usr/local/sbin/dnssec-keygen"
signzone="/usr/local/sbin/dnssec-signzone"
dsfromkey="/usr/local/sbin/dnssec-dsfromkey"
rndc="/usr/local/sbin/rndc"
MASTERDIR="/etc/namedb/master"
RNDC_OPTION="-k $MASTERDIR/rndc.key"

KSK_PARAM="-n zone -a RSASHA1 -b 2048 -f ksk"
ZSK_PARAM="-n zone -a RSASHA1 -b 1024"
SIGN_PARAM="-N unixtime"
DS_PARAM="-2"
ZONE_PREPROCESS="cat"
PRESERVE_REMOVED_KEY="YES"

NOW=`date +%Y%m%d%H%M%S`

if [ -f $CONFIGFILE ]; then
	. $CONFIGFILE
fi

if [ "$1" = "" -a "$DEFAULT_ACTION" != "" ]; then
	echo "Doing $DEFAULT_ACTION"
	set $DEFAULT_ACTION
	echo $1 $2 $3
fi
CMD="$1"
shift

if [ "$CONFIGDIR" = "" ]; then
	CONFIGDIR="$MASTERDIR/config"
fi
if [ "$KEYDIR" = "" ]; then
	KEYDIR="$CONFIGDIR/keydir"
fi
if [ "$KEYBACKUPDIR" = "" ]; then
	KEYBACKUPDIR="$CONFIGDIR/backup"
fi

LOCKF=""

HEAD_ZSKNAME="zsk-"
HEAD_KSKNAME="ksk-"
HEAD_ZSSNAME="zss-"
HEAD_ZSRNAME="zsr-"            # Removed ZSK
HEAD_KSSNAME="kss-"

NEED_RELOAD="NO"

# setup
if [ ! -d $CONFIGDIR ]; then
	mkdir -p $CONFIGDIR
fi
if [ ! -d $KEYBACKUPDIR ]; then
	mkdir -p $KEYBACKUPDIR
fi
if [ ! -d $KEYDIR ]; then
	mkdir -p $KEYDIR
fi

cd $MASTERDIR

_check_file()
{
	while [ "$1" != "" ]; do
		if [ ! -f "$1" ]; then
			echo "$1 does not exist."
			_usage
		fi
		shift
	done
}

_check_nofile()
{
	while [ "$1" != "" ]; do
		if [ -f "$1" ]; then
			echo "$1 exist."
			_usage
		fi
		shift
	done
}

_usage()
{
	if [ "$LOCKF" != "" ]; then
		rm $LOCKF
	fi
	cat <<EOF
$PROG: Simple DNSSEC key management and zone signing tool
Usage:  $PROG command zone(s)
  Available commands:
     keygen       : Generate KSK, ZSK for the zone.
     sign         : (Re-)Sign the zone.
     add-next-ksk : Add next KSK for the zone and sign the zone.
     add-next-zsk : Add next ZSK for the zone and sign the zone.
     zsk-rollover : Change the next ZSK as a new ZSK and sign the zone.
     zskroll      : Change the next ZSK as a new ZSK, add next ZSK, and sign the zone.
     remove-previouskey: Remove previous ZSK
     ksk-rollover : Remove the previous KSK and sign the zone.
     status       : Show key status for the zone.
     status-dnskey: Show KSK DNSKEY for the zone. (for DLV)

     removekeys   : Remove all keys to redo/fix status for the zone.

   Comatibility:
     keygen2      : Generate KSK, ZSK and next ZSK for the zone.
	 standby-ksk-keygen = add-next-ksk
	 standby-zsk-keygen = add-next-zsk
EOF
	exit 1
}

sign()
{
	_check_file $ZONEFILE $KSK_FILE $ZSK_FILE
	KSK=`head -1 $KSK_FILE`
	ZSK=`head -1 $ZSK_FILE`
	KSS=""
	if [ -f $KSK_S_FILE ]; then
		KSS=`head -1 $KSK_S_FILE`
	fi
	_check_file "$KEYDIR/$KSK.private" "$KEYDIR/$ZSK.private"
	$ZONE_PREPROCESS $ZONEFILE > $ZONEFILE.tmp
	cat $KSK_FILE $ZSK_FILE | while read keyfile
	do
		_check_file "$KEYDIR/$keyfile.key"
		cat "$KEYDIR/$keyfile.key" >> $ZONEFILE.tmp
	done
	for i in $KSK_S_FILE $ZSK_S_FILE $ZSK_R_FILE
	do
		if [ -f $i ]; then
			keyfile=`head -1 $i`
			_check_file "$KEYDIR/$keyfile.key"
			cat "$KEYDIR/$keyfile.key" >> $ZONEFILE.tmp
		fi
	done
	cmdname=`basename $signzone`
	if [ "$cmdname" = "ldns-signzone" ]; then
		if [ "$KSS" != "" ]; then
			KSS="$KEYDIR/$KSS"
		fi
		echo $signzone $_SIGN_PARAM -o $ZONE -f "$ZONEFILE.signed" $ZONEFILE.tmp $KEYDIR/$ZSK $KEYDIR/$KSK $KSS
		$signzone $_SIGN_PARAM -o $ZONE -f "$ZONEFILE.signed" $ZONEFILE.tmp $KEYDIR/$ZSK $KEYDIR/$KSK $KSS
	else
		if [ "$KSS" != "" ]; then
			KSS="-k $KEYDIR/$KSS.private"
		fi
		echo $signzone $_SIGN_PARAM -o $ZONE -k $KEYDIR/$KSK.private $KSS -f "$ZONEFILE.signed" $ZONEFILE.tmp $KEYDIR/$ZSK.private 2>&1
		$signzone $_SIGN_PARAM -o $ZONE -k $KEYDIR/$KSK.private $KSS -f "$ZONEFILE.signed" $ZONEFILE.tmp $KEYDIR/$ZSK.private 2>&1
	fi
	rm $ZONEFILE.tmp

	echo "signzone returns $?"

	if [ "$RNDC_OPTION" != "OFF" ]; then
		$rndc $RNDC_OPTION reload $ZONE
	fi
	NEED_RELOAD="YES"
}

status()
{
	if [ -f $KSK_FILE ]; then
		echo -n "$ZONE's KSK = "
		cat $KSK_FILE;
		$dsfromkey $_DS_PARAM $KEYDIR/`cat $KSK_FILE`.key
	fi
	if [ -f $KSK_S_FILE ]; then
		echo -n "$ZONE's next KSK = "
		cat $KSK_S_FILE;
		$dsfromkey $_DS_PARAM $KEYDIR/`cat $KSK_S_FILE`.key
	fi
	if [ -f $ZSK_FILE ]; then
		echo -n "$ZONE's ZSK = "
		cat $ZSK_FILE;
	fi
	if [ -f $ZSK_S_FILE ]; then
		echo -n "$ZONE's next ZSK = "
		cat $ZSK_S_FILE;
	fi
	if [ -f $ZSK_R_FILE ]; then
		echo -n "$ZONE's previous ZSK = "
		cat $ZSK_R_FILE;
	fi
}

status_dnskey()
{
	if [ -f $KSK_FILE ]; then
		echo -n "$ZONE's KSK = "
		cat $KSK_FILE;
		grep DNSKEY $KEYDIR/`cat $KSK_FILE`.key
	fi
	if [ -f $KSK_S_FILE ]; then
		echo -n "$ZONE's next KSK = "
		cat $KSK_S_FILE;
		grep DNSKEY $KEYDIR/`cat $KSK_S_FILE`.key
	fi
}

keygensub()
{
	(
	cd $KEYDIR;
	echo "$keygen $1 $2"
	$keygen $1 $2 > $3;
	_FILE=`cat $3`
	if [ -f $_FILE.ds ]; then
		rm $_FILE.ds
	fi
	)
}

removekeys_sub()
{
	if [ -f $1 ]; then
		KEY=`head -1 $1`
		if [ -f $KEYDIR/$KEY.key ]; then
			mv $KEYDIR/$KEY.key $KEYDIR/$KEY.private $KEYBACKUPDIR/
		fi
	fi
}

remove_previouskey()
{
	if [ -f $ZSK_R_FILE ]; then
		removekeys_sub $ZSK_R_FILE
		mv $ZSK_R_FILE "$KEYBACKUPDIR/removed-ZSK-$NOW-$ZONE"
	fi
}

if [ "$CMD" = "" ]; then
	_usage
fi

if [ "$1" = "" -a "$ZONELIST" != "" ]; then
	set $ZONELIST
fi

for ZONE in $*
do
	LOCKF="$CONFIGDIR/$ZONE.lock"
	TMPF="$CONFIGDIR/$ZONE.$$"
	OUTF="$ZONE.signed"
	KSK_FILE="$CONFIGDIR/$HEAD_KSKNAME$ZONE"
	ZSK_FILE="$CONFIGDIR/$HEAD_ZSKNAME$ZONE"
	KSK_S_FILE="$CONFIGDIR/$HEAD_KSSNAME$ZONE"
	ZSK_S_FILE="$CONFIGDIR/$HEAD_ZSSNAME$ZONE"
	ZSK_R_FILE="$CONFIGDIR/$HEAD_ZSRNAME$ZONE"
	if [ $ZONE = "." ]; then
		ZONEFILE="root"
	else
		ZONEFILE=$ZONE
	fi

	ZONE_=`echo $ZONE | tr .- __`
	eval _SIGN_PARAM=\${SIGN_PARAM_$ZONE_:-$SIGN_PARAM}
	eval _KSK_PARAM=\${KSK_PARAM_$ZONE_:-$KSK_PARAM}
	eval _ZSK_PARAM=\${ZSK_PARAM_$ZONE_:-$ZSK_PARAM}
	eval _DS_PARAM=\${DS_PARAM_$ZONE_:-$DS_PARAM}

	touch $TMPF
	if ln $TMPF $LOCKF; then
		:
	else
		rm $TMPF
		echo "zone $ZONE locked"
		continue
	fi
	rm $TMPF
	case $CMD in
	removekeys)
		removekeys_sub $KSK_FILE
		removekeys_sub $ZSK_FILE
		removekeys_sub $KSK_S_FILE
		removekeys_sub $ZSK_S_FILE
		rm $KSK_FILE $ZSK_FILE $KSK_S_FILE $ZSK_S_FILE
		;;
	keygen)
		_check_nofile $KSK_FILE $ZSK_FILE
		keygensub "$_KSK_PARAM" $ZONE $KSK_FILE
		keygensub "$_ZSK_PARAM" $ZONE $ZSK_FILE
		status
		;;
	keygen2)
		_check_nofile $KSK_FILE $ZSK_FILE $ZSK_S_FILE
		keygensub "$_KSK_PARAM" $ZONE $KSK_FILE
		keygensub "$_ZSK_PARAM" $ZONE $ZSK_FILE
		keygensub "$_ZSK_PARAM" $ZONE $ZSK_S_FILE
		status
		;;
	standby-zsk-keygen)
		_check_nofile $ZSK_S_FILE
		keygensub "$_ZSK_PARAM" $ZONE $ZSK_S_FILE
		status
		;;
	add-next-ksk|standby-ksk-keygen)
		_check_nofile $KSK_S_FILE
		keygensub "$_KSK_PARAM" $ZONE $KSK_S_FILE
		sign
		status
		;;
	add-next-zsk|standby-zsk-keygen)
		_check_nofile $ZSK_S_FILE
		keygensub "$_ZSK_PARAM" $ZONE $ZSK_S_FILE
		sign
		status
		;;
	ksk-rollover)
		_check_file $ZONE $KSK_FILE $KSK_S_FILE
		KSK=`head -1 $KSK_FILE`
		KSS=`head -1 $KSK_S_FILE`
		_check_file $KEYDIR/$KSK.key $KEYDIR/$KSS.key $KEYDIR/$KSK.private $KEYDIR/$KSS.private
		mv $KEYDIR/$KSK.key $KEYDIR/$KSK.private $KEYBACKUPDIR/
		mv $KSK_S_FILE $KSK_FILE
		OLDKSK="$KSK"
		KSK="$KSS"
		KSS=""
		echo "$ZONE 's KSK: valid -> removed: $OLDKSK"
		echo "$ZONE 's KSK: next  -> current: $KSK"
		sign
		status
		;;
	zsk-rollover)
		_check_file $ZONE $ZSK_FILE $ZSK_S_FILE
		ZSK=`head -1 $ZSK_FILE`
		ZSS=`head -1 $ZSK_S_FILE`
		_check_file $KEYDIR/$ZSK.key $KEYDIR/$ZSS.key $KEYDIR/$ZSK.private $KEYDIR/$ZSS.private
		remove_previouskey
		mv $ZSK_FILE $ZSK_R_FILE
		mv $ZSK_S_FILE $ZSK_FILE
		OLDZSK="$ZSK"
		ZSK="$ZSS"
		ZSS=""
		if [ "$PRESERVE_REMOVED_KEY" = "NO" ]; then
			remove_previouskey
		fi
		echo "$ZONE 's ZSK: valid -> previous: $OLDZSK"
		echo "$ZONE 's ZSK: next -> current: $ZSK"
		sign
		status
		;;
	zskroll)
		_check_file $ZONEFILE $ZSK_FILE $ZSK_S_FILE
		ZSK=`head -1 $ZSK_FILE`
		ZSS=`head -1 $ZSK_S_FILE`
		_check_file $KEYDIR/$ZSK.key $KEYDIR/$ZSS.key $KEYDIR/$ZSK.private $KEYDIR/$ZSS.private
		keygensub "$_ZSK_PARAM" $ZONE $ZSK_S_FILE.new
		remove_previouskey
		mv $ZSK_FILE $ZSK_R_FILE
		mv $ZSK_S_FILE $ZSK_FILE
		mv $ZSK_S_FILE.new $ZSK_S_FILE
		OLDZSK="$ZSK"
		ZSK="$ZSS"
		ZSS=`head -1 $ZSK_S_FILE`
		if [ "$PRESERVE_REMOVED_KEY" = "NO" ]; then
			remove_previouskey
		fi
		echo "$ZONE 's ZSK: valid -> previous: $OLDZSK"
		echo "$ZONE 's ZSK: next -> valid: $ZSK"
		echo "$ZONE 's ZSK:      new next key: $ZSS"
		sign
		;;
	sign)
		sign
		;;
	status)
		status
		;;
	status-dnskey)
		status_dnskey
		;;
	remove-previouskey)
		_check_file $ZSK_R_FILE
		remove_previouskey
		sign
		;;
	*)
		echo "unknown command: $CMD"
		_usage
		;;
	esac
	rm $LOCKF
done

if [ "$NEED_RELOAD" = "YES" -a "$RELOADALL_COMMAND" != "" ]; then
	eval $RELOADALL_COMMAND
fi

exit 0
