#!/usr/bin/perl
#
# Copyright 2004-2014 SPARTA, Inc.  All rights reserved.  See the COPYING
# file distributed with this software for details.
#
#
# krfcheck
#
#	This script checks a keyrec file for problems, potential problems,
#	and inconsistencies.
#
#	Problems include:
#
#		- no zones defined
#		- no sets defined
#		- no keys defined
#		- unknown zone keyrecs  (key keyrec points to a non-existent
#					 zone keyrec)
#		- mislabeled keys (key thinks it's KSK/ZSK, zone
#				   thinks it's a ZSK/KSK)
#		- missing key from zone keyrec
#		- invalid zone data values
#			- existence of zone file
#			- existence of KSK file
#			- endtime > 1 day
#			- seconds count and date string match
#		- invalid set data values
#			- listed sets exist
#			- listed keys don't exist
#			- seconds count and date string match
#		- invalid key data values
#			- bad encryption algorithm
#			- existence of ZSK file
#			- out-of-range bit values
#			- non-existent random device
#			- seconds count and date string match
#		- expired zone keyrecs
#
#	Potential problems include:
#
#		- soon-to-expire zone keyrecs
#		- odd zone-signing date
#		- orphaned key keyrecs  (not referenced by any zone keyrecs)
#		- non-existent key directories
#
#	Inconsistencies include:
#
#		- zone fields in a key keyrec
#		- key fields in a zone keyrec
#

use strict;

use Getopt::Long qw(:config no_ignore_case_always);

use Net::DNS::SEC::Tools::conf;
use Net::DNS::SEC::Tools::keyrec;
use Net::DNS::SEC::Tools::tooloptions;

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

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

#
# Time "constants".
#
my $DAY	  = (24 * 60 * 60);		# Seconds in a day.
my $WEEK  = (7 * $DAY);			# Seconds in a week.

my $DAYSWARN = 7;			# Days before an expiration warning.


#
# List of valid encryption algorithms and length ranges.
#
my @algorithms = ("rsasha1", "rsamd5", "dsa", "dh", "hmac-md5", "nsec3rsasha1");

my %length_low =
(
	"rsasha1"	=> 512,
	"rsamd5"	=> 512,
	"dsa"		=> 512,
	"dh"		=> 128,
	"hmac-md5"	=> 1,
	"nsec3rsasha1"	=> 512,

);

my %length_high =
(
	"rsasha1"	=> 2048,
	"rsamd5"  	=> 2048,
	"dsa"		=> 1024,
	"dh"		=> 4096,
	"hmac-md5"	=> 512,
	"nsec3rsasha1"  => 4096
);

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

#
# Data required for command line options.
#
my $count   = 0;			# Error-count flag.
my $quiet   = 0;			# Quiet flag.
my $verbose = 0;			# Verbose flag.
my $keychk  = 0;			# Keys-only flag.
my $setchk  = 0;			# Sets-only flag.
my $zonechk = 0;			# Zones-only flag.
my %options = ();			# Filled option array.
my @opts =
(
	"zone",				# Only check zone keyrecs.
	"set",				# Only check set keyrecs.
	"key",				# Only check key keyrecs.
	"count+",			# Give final error count.
	"quiet+",			# Don't give any output.
	"verbose+",			# Give lotsa output.
	"Version",			# Display the version number.
	"help",				# Give a usage message and exit.
);


my $zoneerrs = 0;			# Count of errors in zones.
my $seterrs = 0;			# Count of errors in sets.
my $keyerrs = 0;			# Count of errors in keys.

my @krnames;				# List of keyrecs in the file.

my %zones = ();				# Names of zone keyrecs.
my %sets = ();				# Names of set keyrecs.
my %kskkeys = ();			# Names of KSK keyrecs.
my %zskkeys = ();			# Names of ZSK keyrecs.

my $curtime;				# Current time.

my $ret;				# Return code from main().

$ret = main();
exit($ret);

#-----------------------------------------------------------------------------
# Routine:	main()
#
sub main()
{
	my $errors = 0;			# Total error count.

	#
	# Parse the command line.
	#
	erraction(ERR_EXIT);
	optsandargs();

	#
	# Read the keyrec file.
	#
	getkeyrecs($ARGV[0]);

	#
	# Check our watch.
	#
	$curtime = time();

	#
	# Run the zone checks.
	#
	zonechecks() if($zonechk);

	#
	# Run the set checks.
	#
	setchecks() if($setchk);

	#
	# Run the key checks.
	#
	keychecks() if($keychk);

	#
	# Calculate and report the number of errors we found.
	#
	$errors = $zoneerrs + $seterrs + $keyerrs;
	print("$errors errors\n") if($count);
	return($errors);
}

#-----------------------------------------------------------------------------
# Routine:	optsandargs()
#
# Purpose:	Parse our command line.
#
sub optsandargs
{
	my $argc = @ARGV;		# Number of command line arguments.

	#
	# Check our options.
	#
	GetOptions(\%options,@opts) || usage();
	$count	 = $options{'count'};
	$quiet	 = $options{'quiet'};
	$verbose = $options{'verbose'};
	$keychk	 = $options{'key'};
	$setchk	 = $options{'set'};
	$zonechk = $options{'zone'};

	#
	# Check a few immediate-handle options.
	#
	version() if(defined($options{'Version'}));
	usage()   if(defined($options{'help'}));

	#
	# Check that exclusive options really are exclusive.
	# 
	if(($keychk && $zonechk)	||
	   ($keychk && $setchk)		||
	   ($setchk && $zonechk))
	{
		usage();
	}

	#
	# If no record-specific options weren't given, we'll do everything.
	# 
	if(!$zonechk && !$setchk && !$keychk)
	{
		$zonechk = 1;
		$setchk	 = 1;
		$keychk	 = 1;
	}

	#
	# Ensure we were given a keyrec file to check.
	#
	usage() if($argc == 0);
}

#-----------------------------------------------------------------------------
# Routine:	zonechecks()
#
# Purpose:	Run some checks on the zones.
#
sub zonechecks
{
	my $zkr;				# Zone keyrec reference.
	my %zonerec;				# Zone keyrec.

	my $zonename;				# Zone name for looping.
	my $keyname;				# Key name.

	my @fields = keyrec_keyfields();	# Valid key fields.

	#
	# Make sure some zones were defined.
	#
	if(length(%zones) == 0)
	{
		vprint("no zones defined\n");
		$zoneerrs++;
		return;
	}

	#
	# Run a bunch of checks on the zones.
	#
	foreach my $zonename (sort(keys(%zones)))
	{
		vprint("checking zone $zonename\n");

		$zkr = $zones{$zonename};

		zonedata($zonename,$zkr);

		zonesets($zonename,$zkr);

		expiredzones($zonename,$zkr);

		crosshkeys("zone","key",$zonename,$zkr,@fields);
	}

	vprint("\n");
}

#-----------------------------------------------------------------------------
# Routine:	setchecks()
#
# Purpose:	Run some checks on the signing sets.
#
sub setchecks
{
	my $skr;				# Set keyrec reference.
	my %setrec;				# Set keyrec.

	my $setname;				# Set name for looping.
	my $keyname;				# Key name.

	my @sets = keyrec_signsets();		# Valid signing sets.

	#
	# Make sure the keyrec file has some set keyrecs.
	#
	if(length(%sets) == 0)
	{
		vprint("no set keyrecs defined\n");
		$seterrs++;
		return;
	}

	#
	# Go through the sets and verify that each set's keys exist
	# and that the set timestamp is valid.
	#
	foreach my $setname (sort(keys(%sets)))
	{
		vprint("checking set $setname\n");

		setdata($setname);

	}

	vprint("\n");
}

#-----------------------------------------------------------------------------
# Routine:	keychecks()
#
# Purpose:	Run some checks on the KSKs and ZSKs.
#
sub keychecks
{
	my $kr;					# Keyrec reference.
	my %keyrec;				# Keyrec.

	my @fields = keyrec_zonefields();	# Valid zone fields.

	#
	# Run several checks on the KSKs.
	#
	if(length(%kskkeys) > 0)
	{
		foreach my $keyname (sort(keys(%kskkeys)))
		{
			vprint("checking KSK key $keyname\n");

			$kr = $kskkeys{$keyname};
			%keyrec = %$kr;

			keydata("KSK",$keyname,$kr);

			keyzones("KSK",$keyname,\%keyrec);

			orphankeys("KSK",$keyname);

			keytypes("KSK",$keyname);

			crosshkeys("KSK","zone",$keyname,$kr,@fields);
		}
	}
	else
	{
		vprint("no KSK keys defined\n");
		$keyerrs++;
	}
	vprint("\n");

	#
	# Run several checks on the ZSKs.
	#
	if(length(%zskkeys) > 0)
	{
		foreach my $keyname (sort(keys(%zskkeys)))
		{
			vprint("checking ZSK key $keyname\n");

			$kr = $zskkeys{$keyname};
			%keyrec = %$kr;

			keydata("ZSK",$keyname,$kr);

			keyzones("ZSK",$keyname,\%keyrec);

			orphankeys("ZSK",$keyname);

			keytypes("ZSK",$keyname);

			crosshkeys("ZSK","zone",$keyname,$kr,@fields);
		}
	}
	else
	{
		vprint("no ZSK keys defined\n");
		$keyerrs++;
	}

}

#-----------------------------------------------------------------------------
# Routine:	getkeyrecs()
#
# Purpose:	Load the keyrec file contents.  The keyrecs are segregated
#		according to type.
#
sub getkeyrecs
{
	my $krfile = shift;			# Keyrec file.

	#
	# Read the specified keyrec file.
	#
	keyrec_read($krfile);
	@krnames = keyrec_names();

	#
	# Add the keyrec to one of our global type hashes.
	#
	foreach my $krn (sort(@krnames))
	{
		my $kr;				# Reference to keyrec.
		my $type;			# Keyrec's type.
		my $stype;			# Keyrec's set type.

		next if(!$krn);

		#
		# Get the keyrec, its type, and its set type.
		#
		$kr	= keyrec_fullrec($krn);
		$type	= keyrec_recval($krn,'keyrec_type');
		$stype	= keyrec_recval($krn,'set_type');

		#
		# Add the keyrec to the appropriate hash.
		#
		if($type eq 'zone')
		{
			$zones{$krn} = $kr;
		}
		elsif(($type eq 'set') ||
		      (($stype eq 'kskobs') || ($stype eq 'kskrev')))
		{
			$sets{$krn} = $kr;
		}
		elsif(($type eq 'kskcur') || ($type eq 'kskpub') ||
		      ($type eq 'kskobs') || ($type eq 'kskrev'))
		{
			$kskkeys{$krn} = $kr;
		}
		elsif(($type eq 'zskcur') || ($type eq 'zsknew') ||
		      ($type eq 'zskpub') || ($type eq 'zskobs'))
		{
			$zskkeys{$krn} = $kr;
		}
		else
		{
			qprint("keyrec $krn has unknown type - \"$type\"\n");
		}
	}
}

##############################################################################
#									     #
# 				Zone checks				     #
#									     #
##############################################################################

#-----------------------------------------------------------------------------
# Routine:	zonesets()
#
# Purpose:	Verify that the signing sets of the KSKs and ZSKs exist.
#
sub zonesets
{
	my $zonename = shift;			# Zone name.
	my $zkr	     = shift;			# Zone keyrec reference.
	my %zonerec  = %$zkr;			# Zone keyrec.

	my $setname;				# Set name.

	#
	# Ensure that the zone's Current KSK signing set is present and exists.
	#
	$setname = $zonerec{'kskcur'};
	if(!defined($setname))
	{
		qprint("missing set:  KSKCUR (zone $zonename)\n");
		$zoneerrs++;
	}
	elsif(!defined($sets{$setname}))
	{
		qprint("no such key:  KSKCUR set $setname (zone $zonename)\n");
		$zoneerrs++;
	}

	#
	# Ensure that if the zone's Published KSK signing set is present that
	# it actually exists.
	#
	$setname = $zonerec{'kskpub'};
	if(defined($setname))
	{
		if(!defined($sets{$setname}))
		{
			qprint("no such key:  KSKPUB set $setname (zone $zonename)\n");
			$zoneerrs++;
		}
	}

	#
	# Ensure that the zone's Current ZSK signing set is present and exists.
	#
	$setname = $zonerec{'zskcur'};
	if(!defined($setname))
	{
		qprint("missing set:  ZSKCUR set $setname (zone $zonename)\n");
		$zoneerrs++;
	}
	elsif(!defined($sets{$setname}))
	{
		qprint("no such set:  ZSKCUR set $setname (zone $zonename)\n");
		$zoneerrs++;
	}

	#
	# Ensure that the zone's Published ZSK signing set is present
	# and exists.
	#
	$setname = $zonerec{'zskpub'};
	if(!defined($setname))
	{
		qprint("missing set:  ZSKPUB set $setname (zone $zonename)\n");
		$zoneerrs++;
	}
	elsif(!defined($sets{$setname}))
	{
		qprint("no such set:  ZSKPUB set $setname (zone $zonename)\n");
		$zoneerrs++;
	}
}

#-----------------------------------------------------------------------------
# Routine:	expiredzones()
#
# Purpose:	Check for expired zones.
#
sub expiredzones
{
	my $zonename = shift;				# Zone name to check.
	my $zkr	     = shift;				# Keyrec reference.
	my %zonerec  = %$zkr;				# Zone keyrec.

	my $endtime  = $zonerec{'endtime'};		# Zone's endtime.
	my $signsecs = $zonerec{'keyrec_signsecs'};	# Zone's sign date.

	my $errflag = 0;				# Error flag.

	if(!defined($signsecs))
	{
		qprint("missing keyrec_signsecs; unable to verify expiration time for zone $zonename\n");
		$errflag = 1;
		$zoneerrs++;
	}

	if(!defined($endtime))
	{
		qprint("missing endtime; unable to verify expiration time for zone $zonename\n");
		$errflag = 1;
		$zoneerrs++;
	}

	return if($errflag);

	$ret = checkexpire('zone',$zonename,$endtime,$signsecs);
	$zoneerrs += $ret;
}

#-----------------------------------------------------------------------------
# Routine:	zonedata()
#
# Purpose:	Check the data in the zone keyrec.
#
sub zonedata
{

	my $zonename = shift;			# Zone's name.
	my $kr	     = shift;			# Keyrec reference.
	my %keyrec   = %$kr;			# Zone table.

	my $zonefile = $keyrec{'zonefile'};
	my $kskdir   = $keyrec{'kskdirectory'};
	my $kskfile  = $keyrec{'kskpath'};
	my $zskdir   = $keyrec{'zskdirectory'};
	my $endtime  = $keyrec{'endtime'};
	my $signsecs = $keyrec{'keyrec_signsecs'};
	my $signdate = $keyrec{'keyrec_signdate'};

	my $datestr;				# Converted keyrec_signsecs.
	my $dateerr = 0;			# Date error flag.

	#
	# Check the existence of the zone file.
	#
	if(defined($zonefile) && (! -e $zonefile))
	{
		qprint("missing file:  zone file $zonefile (zone $zonename)\n");
		$zoneerrs++;
	}

	#
	# Check the existence of the KSK key file.
	#
	if(defined($kskfile) && (! -e $kskfile))
	{
		qprint("missing file:  KSK $kskfile (zone $zonename)\n");
		$zoneerrs++;
	}

	#
	# Check the existence of the KSK key directory.
	#
	if(defined($kskdir) && (! -e $kskdir))
	{
		qprint("missing KSK directory:  $kskdir (zone $zonename)\n");
		$zoneerrs++;
	}

	#
	# Check the existence of the ZSK key directory.
	#
	if(defined($zskdir) && (! -e $zskdir))
	{
		qprint("missing ZSK directory:  $zskdir (zone $zonename)\n");
		$zoneerrs++;
	}

	#
	# Make sure the zone's endtime is more than a day.
	#
	if(defined($endtime) && ($endtime =~ /^\+[0-9]+/))
	{
		my $secs;			# Second-count in endtime.

		$endtime =~ /^\+([0-9]+)/;
		$secs = $1;
		if($secs < $DAY)
		{
			qprint("strange endtime:  $secs < one day (zone $zonename)\n");
			$zoneerrs++;
		}
	}

	#
	# Verify that the zone's timestamp data exists.
	#
	if(!defined($signsecs))
	{
		$dateerr = 1;
		qprint("no keyrec_signsecs defined for zone $zonename\n");
		$zoneerrs++;
	}
	if(!defined($signdate))
	{
		$dateerr = 1;
		qprint("no keyrec_signdate defined for zone $zonename\n");
		$zoneerrs++;
	}

	#
	# Ensure that the zone's seconds count converts into the date
	# stored in its date string.
	#
	if(!$dateerr)
	{
		$datestr = gmtime($signsecs);
		if(defined($signdate) && ($datestr ne $signdate))
		{
			qprint("mismatched timestamps:  $signsecs invalid for \"$signdate\" (zone $zonename)\n");
			$zoneerrs++;
		}
	}
}

##############################################################################
#									     #
# 				Set checks				     #
#									     #
##############################################################################

#-----------------------------------------------------------------------------
# Routine:	setdata()
#
# Purpose:	Check the data in a set keyrec.
#
sub setdata
{
	my $setname = shift;				# Set to check.
	my $keys;					# Set's key list.
	my @keys;					# Set's key array.
	my $dateerr = 0;				# Date error flag.
	my $datestr;					# Date string.
	my $setsecs;					# Set's seconds count.
	my $setdate;					# Set's date string.
	my $settype;					# Set's type.
	my $zone;					# Set's owner zone.

	#
	# Verify the set's zone.
	#
	$zone = keyrec_recval($setname,'zonename');
	if(!defined($zone))
	{
		qprint("no zone defined for set $setname\n");
		$seterrs++;
	}
	elsif(!defined($zones{$zone}))
	{
		qprint("no such zone keyrec:  set $setname does not have a valid zone ($zone)\n");
		$seterrs++;
	}

	#
	# Get the set's type.
	#
	$settype = keyrec_recval($setname,'set_type');

	#
	# Get the set's list of keys.
	#
	$keys = keyrec_recval($setname,'keys');
	@keys = split / /, $keys;

	if(!defined $keys)
	{
		qprint("no keys defined for set $setname\n");
		$seterrs++;
	}

	#
	# Make sure all of the set's keys have a key keyrec and are of the
	# expected type.
	#
	foreach my $key (sort(@keys))
	{
		my $keytype;				# Key's type.
		my $kstype;				# Key's set type.

		# Get some info about this keyrec.
		#
		$keytype = keyrec_recval($key,'keyrec_type');
		$kstype  = keyrec_recval($key,'set_type');

		#
		# Subordinate set keyrecs may only be kskrevs and kskobses.
		#
		next if(($keytype eq 'set') && ($kstype eq 'kskrev'));
		next if(($keytype eq 'set') && ($kstype eq 'kskobs'));
		next if(defined($kstype) &&
			(($keytype eq 'kskrev') || ($keytype eq 'kskobs')));

		#
		# Ensure the key exists as a KSK key or a ZSK key.
		#
		if(!defined($kskkeys{$key}) && !defined($zskkeys{$key}))
		{
			qprint("no such key keyrec:  key $key (set $setname)\n");
#			qprint("\t\t(keytype $keytype) (kstype $kstype)\n");
			$seterrs++;
		}

		#
		# Compare the key's type to the set's type.
		#
		if($keytype ne $settype)
		{
			qprint("key type (key $key, type $keytype) does not match set's type (set $setname, type $settype)\n");
			$seterrs++;
		}
	}

	#
	# Get the set's timestamp data.
	#
	$setsecs = keyrec_recval($setname,'keyrec_setsecs');
	$setdate = keyrec_recval($setname,'keyrec_setdate');
	if(!defined($setsecs))
	{
		$dateerr = 1;
		qprint("no keyrec_setsecs defined for set $setname\n");
		$seterrs++;
	}
	if(!defined($setdate))
	{
		$dateerr = 1;
		qprint("no keyrec_setdate defined for set $setname\n");
		$seterrs++;
	}

	#
	# Ensure that the set's seconds count converts into the date
	# stored in its date string.
	#
	if($dateerr == 0)
	{
		$datestr = gmtime($setsecs);
		if(defined($setdate) && ($datestr ne $setdate))
		{
			qprint("mismatched timestamps:  $setsecs invalid for \"$setdate\" (set $setname)\n");
			$seterrs++;
		}
	}
}

##############################################################################
#									     #
# 				Key checks				     #
#									     #
##############################################################################

#-----------------------------------------------------------------------------
# Routine:	keydata()
#
# Purpose:	Check the data in the key keyrec.
#
sub keydata
{
	my $keytype = shift;			# Keyrec's type.
	my $keyname = shift;			# Keyrec's name.
	my $kr	    = shift;			# Keyrec reference.
	my %keyrec  = %$kr;			# Key table.

	my $found = 0;				# Algorithm-found flag.
	my $length;				# Key length.

	my $algorithm	= lc($keyrec{'algorithm'});
	my $keypath	= $keyrec{'keypath'};
	my $random	= $keyrec{'random'};
	my $gensecs	= $keyrec{'keyrec_gensecs'};
	my $gendate	= $keyrec{'keyrec_gendate'};

	my $datestr;				# Converted keyrec_gensecs.
	my $dateerr = 0;			# Date error flag.

	#
	# Get the key's length.
	#
	if($keytype eq "KSK")
	{
		$length = $keyrec{'ksklength'};
	}
	else
	{
		$length = $keyrec{'zsklength'};
	}

	#
	# Search the algorithm list for this key's algorithm.  If we find
	# it, we'll mark that it's good.
	#
	if(defined($algorithm))
	{
		foreach my $alg (@algorithms)
		{
			if($alg eq $algorithm)
			{
				$found = 1;
				last;
			}
		}
	}

	#
	# If the key's encryption algorithm is bad, we'll give an error
	# message.  If it's okay, we'll make sure the length is in the
	# range of valid lengths for the key type.
	#
	if(!$found)
	{
		qprint("invalid algorithm:  \"$algorithm\" ($keytype key $keyname)\n");
		$keyerrs++;
	}
	else
	{
		my $lowval  = $length_low{$algorithm};
		my $highval = $length_high{$algorithm};

		if($lowval > $length)
		{
			qprint("invalid key length:  length ($length) < minimum ($lowval) for $algorithm ($keytype key $keyname)\n");
			$keyerrs++;
		}
		if($highval < $length)
		{
			qprint("invalid key length:  length ($length) > maximum ($lowval) for $algorithm ($keytype key $keyname)\n");
			$keyerrs++;
		}

		#
		# Check the existence of the ZSK key file.
		#
		if(defined($keypath) && (! -e $keypath))
		{
			if($keyrec{'keyrec_type'} ne 'zskobs')
			{
				qprint("missing file:  ZSK $keypath\n");
				$keyerrs++;
			}
		}
	}

	#
	# Check the existence of the random number file.
	#
	if(defined($random))
	{
		if(! -e $random)
		{
			qprint("missing file:  random generator $random ($keytype key $keyname)\n");
			$keyerrs++;
		}
	}

	#
	# Verify that the key's timestamp data exists.
	#
	if(!defined($gensecs))
	{
		$dateerr = 1;
		qprint("no keyrec_gensecs defined for key $keyname\n");
		$keyerrs++;
	}
	if(!defined($gendate))
	{
		$dateerr = 1;
		qprint("no keyrec_gendate defined for key $keyname\n");
		$keyerrs++;
	}

	#
	# Ensure that the key's seconds count converts into the date
	# stored in its date string.
	#
	$datestr = gmtime($gensecs);
	if(defined($gendate) && ($datestr ne $gendate))
	{
		qprint("mismatched timestamps:  $gensecs invalid for \"$gendate\" ($keytype key $keyname)\n");
		$keyerrs++;
	}

}

#-----------------------------------------------------------------------------
# Routine:	keyzones()
#
# Purpose:	Reverse check the key's zone.
#
sub keyzones
{
	my $keytype = shift;			# Keyrec's type.
	my $keyname = shift;			# Keyrec's name.
	my $kr	    = shift;			# Keyrec reference.
	my %keyrec  = %$kr;			# Key table.

	my $zonename;				# Zone name to check.

	$zonename = $keyrec{'zonename'};

	if($zonename eq "")
	{
		qprint("no zone given:  $keytype key $keyname\n");
		$keyerrs++;
		return;
	}

	if(!defined($zones{$zonename}))
	{
		qprint("no such zone:  $zonename ($keytype key $keyname)\n");
		$keyerrs++;
	}
}

#-----------------------------------------------------------------------------
# Routine:	orphankeys()
#
# Purpose:	Look for orphaned keys -- keys which don't belong to any
#		signing set.
#
sub orphankeys
{
	my $keytype = shift;			# Keyrec's type.
	my $keyname = shift;			# Keyrec's name.

	my $found = 0;				# Key-found flag.

	#
	# Go through our signing sets to see if one holds the key.
	#
	foreach my $set (keyrec_signsets())
	{
		if(keyrec_signset_haskey($set,$keyname))
		{
			$found = 1;
			last;
		}
	}

	#
	# Go through our signing sets to see if one holds the key.
	#
	foreach my $set (keyrec_signsets())
	{
		my $kt;				# Keyrec's type.
		my $keys;			# Keyrec's key string.
		my @keys;			# Keyrec's key array.

		last if($found);

		$kt	= keyrec_recval($set,'keyrec_type');
		$keys	= keyrec_recval($set,'keys');

		#
		# Return failure if it it's a zone or set keyrec.
		#
		next if(($kt eq 'zone') || ($kt eq 'set'));

		#
		# Return failure if it's a zone or set keyrec.
		#
		next if($keys eq '');

		#
		# Put the keyrec's keys into an array of names.
		#
		@keys = split / /, $keys;

		#
		# Check the keys to see if one holds the key.
		#
		for(my $ind = 0;$ind < @keys; $ind++)
		{
			if($keys[$ind] eq $keyname)
			{
				$found = 1;
				last;
			}
		}
	}

	#
	# Register an error if no signing set holds the key.
	#
	if(!$found)
	{
		my $tstr = keyrec_recval($keyname,'keyrec_type');
		qprint("orphaned key:  $keytype key $keyname\t($tstr)\n");
		$keyerrs++;
		return;
	}

}

#-----------------------------------------------------------------------------
# Routine:	keytypes()
#
# Purpose:	Ensure that keys have the right types.
#
sub keytypes
{
	my $keytype = shift;			# Keyrec's type.
	my $keyname = shift;			# Keyrec's name.

	my $set;				# Signing set.
	my $ktype;				# Key's actual type.

	my $zonename;				# Key's zone's name.

	#
	# Look through the zone list to ensure the key and the zone agree
	# about the key's type.
	#
	if($keytype eq "KSK")
	{
		#
		# Gather our needed data.
		#
		$zonename = $kskkeys{$keyname}{'zonename'};
		$ktype	  = $kskkeys{$keyname}{'keyrec_type'};
		$set	  = $zones{$zonename}{$ktype};
	}
	else
	{
		#
		# Gather our needed data.
		#
		$zonename = $zskkeys{$keyname}{'zonename'};
		$ktype	  = $zskkeys{$keyname}{'keyrec_type'};
		$set	  = $zones{$zonename}{$ktype};
	}

	#
	# Obsolete and revoked keys aren't listed in the zone keyrec.
	#
	return if(($ktype eq "zskobs") ||
		  ($ktype eq "kskobs") ||
		  ($ktype eq "kskrev"));

	#
	# Register an error if the key isn't in the appropriate signing set.
	#
	if(!keyrec_signset_haskey($set,$keyname))
	{
		qprint("mislabeled key type:  $keytype key $keyname not in set $set\n");
		$keyerrs++;
	}
}

##############################################################################
#									     #
# 				Utilities				     #
#									     #
##############################################################################

#-----------------------------------------------------------------------------
# Routine:	checkexpire()
#
sub checkexpire
{
	my $type     = shift;			# Zone name for looping.
	my $name     = shift;			# Zone name for looping.
	my $endtime  = shift;			# Zone's endtime.
	my $signsecs = shift;			# Zone's sign date.

	my $errs = 0;				# Error count.

	my $secs;				# Seconds in "+nnn" endtime.
	my $days;				# Days until zone expiration.
	my $daystr = "days";			# Days string.
	my $finaltime;				# Time zone expires.

	#
	# Ensure that the keyrec's sign date is older than the current time.
	#
	if($signsecs > $curtime)
	{
		qprint("bizarre time:  sign date more recent than current date ($type $name)\n");
		$errs++;
	}

	#
	# Get the number of seconds until the keyrec's end time.
	#
	if($endtime =~ /^+/)
	{
		$endtime =~ /\+([0-9]+)/;
		$secs = $1;
	}

	#
	# Calculate the keyrec's expiration date.
	#
	$finaltime = $signsecs + $secs;

	#
	# Make sure the keyrec hasn't expired yet.  If it hasn't, we'll
	# check to see if it's close to expiration.
	#
	if($finaltime <= $curtime)
	{
		$secs = $curtime - $finaltime;
		$days = int($secs / $DAY);
		$daystr = "day" if ($days == 1);
		qprint("signtime:  $type expired $days $daystr ago ($type $name)\n");
		$errs++;
	}
	else
	{
		#
		# Figure out how many days are left until expiration.
		#
		$secs = $finaltime - $curtime;
		$days = int($secs / $DAY);
		$daystr = "day" if ($days == 1);

		#
		# Check if the keyrec will soon expire.
		#
		if($days <= $DAYSWARN)
		{
			qprint("signtime:  $type expires in $days $daystr ($type $name)\n");
			$errs++;
		}
	}

	return($errs);
}

#-----------------------------------------------------------------------------
# Routine:	crosshkeys()
#
# Purpose:	Ensure that all the keyrec's fields are valid for that
#		type of keyrec.
#
sub crosshkeys
{
	my $krtype  = shift;			# Type of keyrec.
	my $fldtype = shift;			# Type of @fields' keys.
	my $krname  = shift;			# Keyrec's name.
	my $kr	    = shift;			# Reference to keyrec.
	my @fields  = shift;			# Hash keys to check for.

	my %keyrec = %$kr;			# Caller's keyrec.
	my $errs   = 0;				# Error count.

	#
	# None of the entries in @fields should be hash keys in the
	# keyrec.  We'll check to ensure all's well.
	#
	foreach my $key (@fields)
	{
		#
		# Skip internal-use fields.
		#
		if($key eq "keyrec_type")
		{
			next;
		}

		if(exists($keyrec{$key}))
		{
			qprint("bad entry:  $fldtype hash-key $key in a $krtype record  ($krtype $krname)\n");
			$errs++;
		}
	}

	return($errs);
}

#-----------------------------------------------------------------------------
# Routine:	dumpdata()
#
# Purpose:	Display a bunch of data.  This is only for debugging.
#
sub dumpdata
{
	my $keyname;				# Key name for looping.
	my $zonename;				# Zone name for looping.

	return if(!$verbose);

	print "zones:\n";
	foreach $zonename (sort(keys(%zones)))
	{
		print "\t$zonename\n";
	}
	print "\n";

	print "ksks:\n";
	foreach $keyname (sort(keys(%kskkeys)))
	{
		print "\t$keyname\n";
	}
	print "\n";

	print "zsks:\n";
	foreach $keyname (sort(keys(%zskkeys)))
	{
		print "\t$keyname\n";
	}
	print "\n";
}

#-----------------------------------------------------------------------------
# Routine:	qprint()
#
# Purpose:	Only print if -quiet wasn't given.
#
sub qprint
{
	my $line = shift;

	if(!$quiet)
	{
		print $line;
	}
}

#-----------------------------------------------------------------------------
# Routine:	vprint()
#
# Purpose:	Only print if -verbose was given.
#
sub vprint
{
	my $line = shift;

	if($verbose)
	{
		qprint($line);
	}
}

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

	exit(0);
}

#-----------------------------------------------------------------------------
# Routine:	usage()
#
sub usage
{
	print STDERR "usage:  krfcheck [options] <keyrec-file>\n";
	print STDERR "             options:\n";
	print STDERR "                  -zone      only check zone keyrecs\n";
	print STDERR "                  -set       only check set keyrecs\n";
	print STDERR "                  -key       only check key keyrecs\n";
	print STDERR "                  -count     display an error count\n";
	print STDERR "                  -quiet     give no output\n";
	print STDERR "                  -verbose   give verbose output\n";
	print STDERR "                  -Version   print version numbers\n";
	print STDERR "                  -help      print this message\n";
	exit(0);
}

1;

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

=pod

=head1 NAME

krfcheck - Check a DNSSEC-Tools I<keyrec> file for problems and
inconsistencies

=head1 SYNOPSIS

  krfcheck [-zone | -set | -key] [-count] [-quiet]
	   [-verbose] [-Version] [-help] keyrec-file

=head1 DESCRIPTION

This script checks a I<keyrec> file for problems, potential problems,
and inconsistencies.

Recognized problems include:

=over 4

=item * no zones defined

The I<keyrec> file does not contain any zone I<keyrec>s.

=item * no sets defined

The I<keyrec> file does not contain any set I<keyrec>s.

=item * no keys defined

The I<keyrec> file does not contain any key I<keyrec>s.

=item * unknown zone I<keyrec>s

A set I<keyrec> or a key I<keyrec> references a non-existent zone I<keyrec>.

=item * missing key from zone I<keyrec>

A zone I<keyrec> does not have both a KSK key and a ZSK key.

=item * missing key from set I<keyrec>

A key listed in a set I<keyrec> does not have a key I<keyrec>.

=item * expired zone I<keyrec>s

A zone has expired.

=item * mislabeled key

A key is labeled as a KSK (or ZSK) and its owner zone has it labeled as the
opposite.

=item * invalid zone data values

A zone's I<keyrec> data are checked to ensure that they are valid.  The
following conditions are checked:  existence of the zone file, existence of
the KSK file, existence of the KSK and ZSK directories, the end-time is
greater than one day, and the seconds-count and date string match.

=item * invalid key data values

A key's I<keyrec> data are checked to ensure that they are valid.  The
following conditions are checked:  valid encryption algorithm, key length
falls within algorithm's size range, random generator file exists, and the
seconds-count and date string match.

=back

Recognized potential problems include:

=over 4

=item * imminent zone expiration

A zone will expire within one week. 

=item * odd zone-signing date

A zone's recorded signing date is later than the current system clock.

=item * orphaned keys

A key I<keyrec> is unreferenced by any set I<keyrec>.

=item * missing key directories

A zone I<keyrec>'s key directories (I<kskdirectory> or I<zskdirectory>) does
not exist.

=back

Recognized inconsistencies include:

=over 4

=item * key-specific fields in a zone I<keyrec>

A zone I<keyrec> contains key-specific entries.  To allow for site-specific
extensibility, B<krfcheck> does not check for undefined I<keyrec> fields.

=item * zone-specific fields in a key I<keyrec>

A key I<keyrec> contains zone-specific entries.  To allow for site-specific
extensibility, B<krfcheck> does not check for undefined I<keyrec> fields.

=item * mismatched zone timestamp

A zone's seconds-count timestamp does not match its textual timestamp.

=item * mismatched set timestamp

A set's seconds-count timestamp does not match its textual timestamp.

=item * mismatched key timestamp

A key's seconds-count timestamp does not match its textual timestamp.

=back

=head1 OPTIONS

=over 4

=item B<-zone>

Only perform checks of zone I<keyrec>s.  This option may not be combined with
the B<-set> or B<-key> options.

=item B<-set>

Only perform checks of set I<keyrec>s.  This option may not be combined with
the B<-zone> or B<-key> options.

=item B<-key>

Only perform checks of key I<keyrec>s.  This option may not be combined with
the B<-set> or B<-zone> options.

=item B<-count>

Display a final count of errors.

=item B<-quiet>

Do not display messages.  This option supersedes the setting of the B<-verbose>
option.

=item B<-verbose>

Display many messages.  This option is subordinate to the B<-quiet> option.

=item B<-Version>

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

=item B<-help>

Display a usage message.

=back

=head1 COPYRIGHT

Copyright 2004-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<cleankrf(8)>,
B<fixkrf(8)>,
B<lskrf(1)>,
B<zonesigner(8)>

B<Net::DNS::SEC::Tools::keyrec.pm(3)>

B<file-keyrec(5)>

=cut
