#!/usr/bin/perl
#
# Copyright 2007-2014 SPARTA, Inc.  All rights reserved.  See the COPYING
# file distributed with this software for details.
#
#
# keyarch
#
#	This script archives old KSK and ZSK keys.
#

#
# If we're executing from a packed environment, make sure we've got the
# library path for the packed modules.
#
BEGIN
{
	if($ENV{'PAR_TEMP'})
	{
		unshift @INC, ("$ENV{'PAR_TEMP'}/inc/lib");
	}
}

use strict;

use Getopt::Long qw(:config no_ignore_case_always);
use Net::DNS::SEC::Tools::conf;
use Net::DNS::SEC::Tools::dnssectools;
use Net::DNS::SEC::Tools::keyrec;
use Net::DNS::SEC::Tools::rollrec;

#
# Version information.
#
my $NAME   = "keyarch";
my $VERS   = "$NAME version: 2.1.0";
my $DTVERS = "DNSSEC-Tools Version: 2.1";

##########################################
#
# Data required for command line options.
#

my $fnarg;				# Rollrec file to be managed.

my %dtconf = ();			# DNSSEC-Tools config file values.

my %opts = ();				# Filled option array.
my @opts =
(
	"zone=s",			# Zone to archive.
	"kskonly",			# Only archives KSKs.
	"zskonly",			# Only archives ZSKs.
	"dtconfig=s",			# Execution-specific config file.
	"help",				# Give a usage message and exit.
	"quiet",			# Quiet output.
	"verbose",			# Verbose output.
	"Version",			# Display the version number.
);

#
# Flag values for the various options.  Variable/option connection should
# be obvious.
#
my $zone;				# Zone to archive.
my $kskonly = 1;			# KSK-only flag.
my $zskonly = 1;			# ZSK-only flag.
my $verbose = 0;			# Verbose option.
my $quiet   = 0;			# Quiet option.

#
# Count of archived keys.
#
my $keycount = 0;

#
# Command paths.
#
my $MV = "/bin/mv";

#######################################################################

#
# Do Everything.
#
my $ret = main();
exit($ret);

#-----------------------------------------------------------------------------
# Routine:	main()
#
# Purpose:	Do Everything.
#
sub main
{
	my $ftype;					# Type of file argument.

	#
	# Set up how to handle module errors.
	#
	erraction(ERR_MSG);

	#
	# Use a local config file if we're running as part of a packed
	# configuration.
	#
	if(runpacked())
	{
		setconffile("$ENV{'PAR_TEMP'}/inc/dnssec-tools.conf");
	}

	#
	# Check our options and arguments.
	#
	optsandargs();

	#
	# Ensure we have a valid file argument.
	#
	$ftype = dt_filetype($fnarg);
	if(($ftype eq "mixed") || ($ftype eq "unknown"))
	{
		print STDERR "file argument must be either a keyrec file OR a rollrec file\n";
		exit(-2);
	}

	#
	# If we were given a rollrec file, we'll handle a single zone (if
	# -zone was given) or all the file's zones (if -zone wasn't given.)
	#
	# If we were given a keyrec file, we'll handle a single zone (if
	# -zone was given) or all the file's zones (if -zone wasn't given.)
	#
	if($ftype eq "rollrec")
	{
		#
		# Read the rollrec file.
		#
		rollrec_read($fnarg);

		#
		# Check the zone (if specified) or the whole rollrec file.
		#
		if(defined($zone))
		{
			chkzone($zone,0);
		}
		else
		{
			#
			# Check all the file's zones.
			#
			foreach my $rrn (sort(rollrec_names()))
			{
				chkzone($rrn,0);
			}
		}
		rollrec_close();
	}
	else
	{
		#
		# Read the keyrec file.
		#
		keyrec_read($fnarg);

		#
		# Check the zone (if specified) or the whole keyrec file.
		#
		if(defined($zone))
		{
			chkzone($zone,1);
		}
		else
		{
			#
			# Check each zone in the keyrec file.
			#
			foreach my $krn (sort(keyrec_names()))
			{
				my $kt;				# Keyrec's type.

				$kt = keyrec_recval($krn,'keyrec_type');

				next if($kt ne 'zone');
				chkzone($krn,1);
			}
		}
	}

	#
	# Close up shop.
	#
	vprint("$keycount keys archived");
	return($keycount);
}

#-----------------------------------------------------------------------------
# Routine:	chkzone()
#
# Purpose:	Check this zone for obsolete signing sets.  If we find
#		any of the requested types, its keys will be moved to the
#		proper archive directory.
#
sub chkzone
{
	my $zone    = shift;				# Zone to check.
	my $krfflag = shift;				# Keyrec-read flag.

	my $krf;					# Zone's keyrec file.
	my $archdir;					# Zone's archive dir.
	my $saved = 0;					# Saved-keys flag.

	#
	# Read the zone's keyrec file.
	#
	if(!$krfflag)
	{
		$krf = rollrec_recval($zone,'keyrec');
		keyrec_read($krf);
	}

	#
	# Get the zone's archive directory.
	#
	#	This check is performed here (instead of with other options)
	#	so that each zone can have its own personal archive directory.
	#
	$archdir = keyrec_recval($zone,'archivedir') || $dtconf{'archivedir'};
	return if(!checkdir($zone,$archdir));
	vprint("archive directory: $archdir\t\t($zone)");

	#
	# Check this zone for obsolete signing sets.  If we find any of
	# the requested types, we'll archive its keys.
	#
	foreach my $krn (sort(keyrec_names()))
	{
		my $keytype;				# Key's type.
		my $keyprv;				# Private key file.
		my $keypub;				# Public key file.

		$keytype = keyrec_recval($krn,'keyrec_type');

		#
		# Skip non-obsolete and non-revoked keys.
		#
		next if($keytype !~ /obs/);

		#
		# Skip KSKs if we're only archiving ZSKs.
		#
		next if(($keytype =~ /ksk/) && !$kskonly);

		#
		# Skip ZSKs if we're only archiving KSKs.
		#
		next if(($keytype =~ /zsk/) && !$zskonly);

		#
		# Build the key file names.
		#
		$keyprv = "$krn.private";
		$keypub	= "$krn.key";

		#
		# Save the keys.
		#
		archit($zone,$krn,$keyprv,$archdir,0);
		archit($zone,$krn,$keypub,$archdir,1);
		$saved++;
	}

	#
	# Close up the keyrec file.
	#
	keyrec_write() if($saved);
	keyrec_close();
}

#-----------------------------------------------------------------------------
# Routine:	archit()
#
# Purpose:	Archive the actual key file.
#
sub archit
{
	my $zone    = shift;				# Key's zone.
	my $keyname = shift;				# Key's name
	my $keyfile = shift;				# Key to archive.
	my $archdir = shift;				# Archive directory.
	my $pubflag = shift;				# Public-key flag.

	my $kronos = time;				# Timestamp.
	my $newname;					# New key path.

	#
	# Go home if the key file doesn't exist.
	#
	return if(!-e $keyfile);

	#
	# Build the new name.
	#
	$newname = "$archdir/$kronos.$keyfile";

	#
	# Move the key and maybe give a message.
	#
	system("$MV $keyfile $newname");
	if($verbose)
	{
		print("archived $keyfile\t\t($zone)\n");
	}
	else
	{
		nqprint("archived $keyfile");
	}

	#
	# If this is a public key, we'll reset the key's path in the keyrec.
	#
	keyrec_setval('key',$keyname,'keypath',$newname) if($pubflag);

	#
	# Bump our count of archived keys.
	#
	$keycount++;
}

#-----------------------------------------------------------------------------
# Routine:	optsandargs()
#
# Purpose:	Parse our options and arguments.
#
sub optsandargs
{
	my $argc = @ARGV;				# Number of arguments.
	my $dir;					# Execution directory.

	#
	# Check our options.
	#
	GetOptions(\%opts,@opts) || usage();
	$verbose = $opts{'verbose'};
	$quiet	 = $opts{'quiet'};
	$zone	 = $opts{'zone'};
	$kskonly = $opts{'kskonly'};
	$zskonly = $opts{'zskonly'};

	#
	# Show the usage or version number if requested.
	#
	usage() if(defined($opts{'help'}));
	version() if(defined($opts{'version'}));

	#
	# Check for a rollrec file name.
	#
	$fnarg = $ARGV[0] || rollrec_default();
	if(($fnarg eq "") || !defined($fnarg))
	{
		print STDERR "no rollrec file specified\n";
		exit(-1);
	}

	#
	# Ensure we weren't given both -quiet and -verbose.
	#
	if($quiet && $verbose)
	{
		print STDERR "-quiet and -verbose are mutually exclusive\n";
		exit(-1);
	}

	#
	# Ensure we weren't given both -kskonly and -zskonly.
	#
	if($kskonly && $zskonly)
	{
		print STDERR "-kskonly and -zskonly are mutually exclusive\n";
		exit(-1);
	}

	#
	# Ensure we weren't given both -kskonly and -zskonly.
	#
	$zskonly = 0 if($kskonly);
	$kskonly = 0 if($zskonly);

	#
	# If -kskonly and -zskonly weren't given, turn 'em both on.
	#
	if(!$kskonly && !$zskonly)
	{
		$kskonly = 1;
		$zskonly = 1;
	}

	#
	# If there's a -dtconfig command line option, we'll use that,
	# if we're running packed.
	#
	if(exists($opts{'dtconfig'}))
	{
		setconffile($opts{'dtconfig'});
	}

	#
	# Check for a rollrec file name.
	#
	%dtconf = parseconfig();
}

#----------------------------------------------------------------------
# Routine:	checkdir()
#
# Purpose:	Ensures archive directory exists and is a writable directory.
#
sub checkdir
{
	my $zone    = shift;			# Zone name.
	my $archdir = shift;			# Zone's archive directory.

	#
	# Check for directory existence.
	#
	if(!-e $archdir)
	{
		print STDERR "$zone: archive directory \"$archdir\" does not exist\n";
		return(0);
	}

	#
	# Check that the directory is really a directory.
	#
	if(!-d $archdir)
	{
		print STDERR "$zone: archive directory \"$archdir\" is not a directory\n";
		return(0);
	}

	#
	# Check that the directory is writable.
	#
	if(!-w $archdir)
	{
		print STDERR "$zone: archive directory \"$archdir\" is not writable\n";
		return(0);
	}

	return(1);
}

#----------------------------------------------------------------------
# Routine:	vprint()
#
# Purpose:	Verbose printing.
#
sub vprint
{
	my $str = shift;

	return if(!$verbose);
	print "$str\n";
}

#----------------------------------------------------------------------
# Routine:	nqprint()
#
# Purpose:	Non-quiet printing.
#
sub nqprint
{
	my $str = shift;

	return if($quiet);
	print "$str\n";
}

#----------------------------------------------------------------------
# Routine:	version()
#
# Purpose:	Print the version number(s) and exit.
#
sub version
{
	print STDERR "$VERS\n";
	print STDERR "$DTVERS\n";

	exit(0);
}

#-----------------------------------------------------------------------------
# Routine:	usage()
#
# Purpose:	Print a usage message and exit.
#
sub usage
{
	print STDERR "usage:  keyarch [options] <keyrec-file | rollrec-file>\n";
	print STDERR "\toptions:\n";
	print STDERR "\t\t-zone <zonename>\n";
	print STDERR "\t\t-kskonly\n";
	print STDERR "\t\t-zskonly\n";
	print STDERR "\t\t-dtconfig <config_file>\n";
	print STDERR "\t\t-quiet\n";
	print STDERR "\t\t-verbose\n";
	print STDERR "\t\t-Version\n";
	print STDERR "\t\t-help\n";
	exit(0);
}

1;

##############################################################################
#

=pod

=head1 NAME

keyarch - DNSSEC-Tools daemon to archive old KSK and ZSK keys

=head1 SYNOPSIS

  keyarch [options] <keyrec_file | rollrec_file>

=head1 DESCRIPTION

The B<keyarch> program archives old KSK and ZSK keys.  Keys are considered old
if they are revoked or obsolete.  Keys marked as either I<kskrev> or I<zskrev>
are revoked; keys marked as either I<kskobs> or I<zskobs> are obsolete.
Archived keys are prefixed with the seconds-since-epoch as a means of
distinguishing a zone's keys that have the same five digit number.

If the required file argument is a I<keyrec> file, then expired keys listed
in that file are archived.  If the file argument is a I<rollrec> file, the
I<keyrec> files of the zones in that file are checked for expired keys.

If the B<-zone> option is given, then only revoked and obsolete keys belonging
to the specified zone will be archived.

The archive directory is either zone-specific (listed in the zone's I<keyrec>
record in the zone's I<keyrec> file) or the default archive directory given
in the DNSSEC-Tools configuration file.

The count of archived keys is given as the program's exit code.  Error exit
codes are negative. 

=head1 OPTIONS

The following options are recognized:

=over 4

=item B<-zone zone_file>

Name of the zone whose KSKs will be archived.  If this is not given, then
all the zones defined in the I<rollrec> file will be checked.

=item B<-kskonly>

Only archive KSK keys.

=item B<-zskonly>

Only archive ZSK keys.

=item B<-dtconfig config_file>

Name of an alternate DNSSEC-Tools configuration file to be processed.
If specified, this configuration file is used I<in place> of the normal
DNSSEC-Tools configuration file B<not> in addition to it.  Also, it will be
handled prior to I<keyrec> files, I<rollrec> files, and command-line options.

=item B<-quiet>

No output will be given.

=item B<-verbose>

Verbose output will be given.

=item B<-help>

Display a usage message.

=item B<-Version>

Displays the version information for B<keyarch> and the DNSSEC-Tools package.

=back

=head1 EXIT VALUES

On success, B<keyarch>'s exit code is the number of keys archived.

B<keyarch> has a 0 exit code if the help message is given.

B<keyarch> has a negative exit code if an error is encountered.

=head1 COPYRIGHT

Copyright 2007-2014 SPARTA, Inc.  All rights reserved.
See the COPYING file included with the DNSSEC-Tools package for details.

=head1 AUTHOR

Wayne Morrison, tewok@tislabs.com

=head1 SEE ALSO

B<rollerd(8)>,
B<zonesigner(8)>

B<Net::DNS::SEC::Tools::conf.pm(3)>,
B<Net::DNS::SEC::Tools::dnssectools.pm(3)>,
B<Net::DNS::SEC::Tools::defaults.pm(3)>,
B<Net::DNS::SEC::Tools::keyrec.pm(3)>,
B<Net::DNS::SEC::Tools::rollrec.pm(3)>

B<keyrec(5)>,
B<rollrec(5)>

=cut
