# HG changeset patch # User Heiko Schlittermann # Date 1295739015 -3600 # Node ID 0e1e5027e9c09113c3c9f540a4bcd9ad0b815ea2 # Parent 53c95f2ff0ac1233dacc8df303860f3413d5f6d4# Parent 6d624831079fd01f972fc49ab76797c9ac52e87b merged diff -r 53c95f2ff0ac -r 0e1e5027e9c0 .hgignore --- a/.hgignore Tue Dec 21 17:00:11 2010 +0100 +++ b/.hgignore Sun Jan 23 00:30:15 2011 +0100 @@ -1,6 +1,6 @@ -.hgignore -dnssec-keytool -update-serial -zone-ls -zone-mk -zone-rm +bind +blib +Build +_build +cover_db +pod2htm.\.tmp diff -r 53c95f2ff0ac -r 0e1e5027e9c0 .hgtags --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgtags Sun Jan 23 00:30:15 2011 +0100 @@ -0,0 +1,1 @@ +9c304b77cf02c77538ad7fc983f30f04fd50ebdd 0.1 diff -r 53c95f2ff0ac -r 0e1e5027e9c0 Build.PL --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Build.PL Sun Jan 23 00:30:15 2011 +0100 @@ -0,0 +1,22 @@ +#! /usr/bin/perl + +use strict; +use warnings; +use Module::Build; + +my $build = Module::Build->new( + module_name => "dnstools", + dist_author => "schlittermann.de", + dist_version => "0.1", + create_license => 1, + license => "gpl", + requires => { + perl => "5.10.0", + "Net::LibIDN" => "0", + "Template" => "0" + }, + build_requires => { "Pod::Coverage" => 0, "Test::Command" => "0.08" }, + script_files => [glob "bin/*"], # avoid .swp files +); + +$build->create_build_script; diff -r 53c95f2ff0ac -r 0e1e5027e9c0 MANIFEST.SKIP --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MANIFEST.SKIP Sun Jan 23 00:30:15 2011 +0100 @@ -0,0 +1,50 @@ + +#!start included /usr/share/perl/5.10/ExtUtils/MANIFEST.SKIP +# Avoid version control files. +^\.hg/ +^\.hg[^_] +.*\.sw?$ +\bRCS\b +\bCVS\b +\bSCCS\b +,v$ +\B\.svn\b +\B\.git\b +\B\.gitignore\b +\b_darcs\b + +# Avoid Makemaker generated and utility files. +\bMANIFEST\.bak +\bMakefile$ +\bblib/ +\bMakeMaker-\d +\bpm_to_blib\.ts$ +\bpm_to_blib$ +\bblibdirs\.ts$ # 6.18 through 6.25 generated this + +# Avoid Module::Build generated and utility files. +\bBuild$ +\b_build/ + +# Avoid temp and backup files. +~$ +\.old$ +\#$ +\b\.# +\.bak$ + +# Avoid Devel::Cover files. +\bcover_db\b +#!end included /usr/share/perl/5.10/ExtUtils/MANIFEST.SKIP + + +# Avoid Module::Build generated and utility files. +\bBuild$ +\bBuild.bat$ +\b_build +\bBuild.COM$ +\bBUILD.COM$ +\bbuild.com$ + +# Avoid archives of this distribution +\bdnstools-[\d\.\_]+ diff -r 53c95f2ff0ac -r 0e1e5027e9c0 Makefile --- a/Makefile Tue Dec 21 17:00:11 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,19 +0,0 @@ -ALL = zone-ls zone-mk zone-rm \ - update-serial \ - dnssec-keytool - -CLEANFILES = $(ALL) - -.PHONY: all clean distclean install - -all: $(ALL) - -clean: -distclean: - rm -f $(CLEANFILES) - - -%: %.pl - @perl -c $< - @cp -f $< $@ - @chmod a+x-w $@ diff -r 53c95f2ff0ac -r 0e1e5027e9c0 bin/.perltidyrc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/.perltidyrc Sun Jan 23 00:30:15 2011 +0100 @@ -0,0 +1,1 @@ +../.perltidyrc \ No newline at end of file diff -r 53c95f2ff0ac -r 0e1e5027e9c0 bin/dnssec-keytool --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/dnssec-keytool Sun Jan 23 00:30:15 2011 +0100 @@ -0,0 +1,349 @@ +#! /usr/bin/perl + +use v5.10; +use warnings; +use strict; +use File::Temp; +use Getopt::Long; +use Pod::Usage; +use File::Basename; +use if $ENV{DEBUG} => "Smart::Comments"; +use DNStools::Config qw(get_config); + +my $ME = basename $0; + +sub read_conf(@); +sub read_argv($); +sub rm_keys($@); +sub ck_zone($@); +sub create_ksk($@); +sub create_zsk($@); +sub post_create($@); + +MAIN: { + ### reading config + my %conf = get_config(); + + my ($cmd, @zones) = read_argv($conf{master_dir}); + + given ($cmd) { + when ("rm") { rm_keys($conf{master_dir}, @zones); exit } + when ("ck") { ck_zone($conf{master_dir}, @zones) } + when ("ksk") { create_ksk($conf{master_dir}, @zones) } + }; + + create_zsk($conf{master_dir}, @zones); + post_create($conf{master_dir}, @zones); +} + +sub read_argv ($) { + my ($master_dir) = @_; + my ($cmd, @zones); # return + + GetOptions( + "zsk" => sub { $cmd = "zsk" }, + "ksk" => sub { $cmd = "ksk" }, + "rm" => sub { $cmd = "rm" }, + "ck|check" => sub { $cmd = "ck" }, + "h|help" => sub { pod2usage(-exitvalue => 0, -verbose => 1) }, + "m|man" => sub { + pod2usage( + -exitvalue => 0, + -noperldoc => system("perldoc -V &>/dev/null"), + -verbose => 2 + ); + }, + ) + and @ARGV + or pod2usage; + + # checks the zones in argv if there are managed zones + foreach (@ARGV) { + chomp(my $zone = `idn --quiet "$_"`); + + die "zone $zone is not managed\n" + if not -f "$master_dir/$zone/$zone"; + + push @zones, $zone; + } + return ($cmd, @zones); +} + + +sub rm_keys ($@) { + + # deletes all the keys were handed over -rm in argv + my ($master_dir, @zone) = @_; + + for (@zone) { + my $zone = $_; + + my $zpf = "$master_dir/$zone"; + my $ep = 0; + + if (-e "$zpf/$zone.signed") { + unlink "$zpf/$zone.signed" and $ep = 1; + } + if (-e "$zpf/.keycounter") { + unlink "$zpf/.keycounter" and $ep = 1; + } + if (-e "$zpf/.index.ksk") { + unlink "$zpf/.index.ksk" and $ep = 1; + } + if (-e "$zpf/.index.zsk") { + unlink "$zpf/.index.zsk" and $ep = 1; + } + if (-e "$zpf/dsset-$zone.") { + unlink "$zpf/dsset-$zone." and $ep = 1; + } + if (-e "$zpf/keyset-$zone.") { + unlink "$zpf/keyset-$zone." and $ep = 1; + } + + for (glob("$zpf/K$zone*")) { + chomp($_); + unlink("$_"); + } + + if ($ep == 1) { + print " * $zone: removed key-set\n"; + } + + open(my $old, "$zpf/$zone") or die "$zpf/$zone: $!\n"; + my $fh = File::Temp->new(DIR => $zpf) or die "Can't create tmpfile: $!\n"; + print $fh grep { not /^\s*\$INCLUDE.*"K$zone.*\.key"/i } <$old>; + rename($fh->filename => "$zpf/$zone") + or die "Can't rename " . $fh->filename . " to $zpf/$zone: $!\n"; + } +} + +sub create_ksk ($@) { + my ($master_dir, @zone) = @_; + my @index; + my $keyname; + + for (@zone) { + my $zone = $_; + my $zpf = "$master_dir/$zone"; + + $keyname = + `cd $zpf && dnssec-keygen -a RSASHA1 -b 2048 -f KSK -n ZONE $zone`; + + unless (-f "$zpf/.index.ksk") { @index = (); } + else { + open(INDEX, "$zpf/.index.ksk") or die "$zpf/.index.ksk: $!\n"; + @index = ; + close(INDEX); + } + + push @index, $keyname; + if (@index > 2) { shift(@index); } + + { + my $fh = File::Temp->new(DIR => "$zpf") + or die "Can't create tmpdir: $!\n"; + print $fh join "" => @index, ""; + rename($fh->filename => "$zpf/.index.ksk") + or die "Can't rename " + . $fh->filename + . " to $zpf/.index.ksk: $!\n"; + } + + chomp($keyname); + print " * $zone: new KSK $keyname\n"; + print "!! THE KSK must be published !! \n"; + + } +} + +sub create_zsk ($@) { + my ($master_dir, @zone) = @_; + my @index; + my $keyname; + + for (@zone) { + my $zone = $_; + my $zpf = "$master_dir/$zone"; + + $keyname = `cd $zpf && dnssec-keygen -a RSASHA1 -b 512 -n ZONE $zone`; + + unless (-f "$zpf/.index.zsk") { + @index = (); + } + else { + open(INDEX, "$zpf/.index.zsk") or die "$zpf/.index.zsk: $!\n"; + @index = ; + close(INDEX); + } + + push @index, $keyname; + if (@index > 2) { shift(@index); } + + { + my $fh = File::Temp->new(DIR => "$zpf") + or die "Can't create tmpdir: $!\n"; + print $fh join "" => @index, ""; + rename($fh->filename => "$zpf/.index.zsk") + or die "Can't rename " + . $fh->filename + . " to $zpf/.index.zsk: $!\n"; + } + chomp($keyname); + print " * $zone: new ZSK $keyname\n"; + + open(KC, ">$zpf/.keycounter") or die "$zpf/keycounter: $!\n"; + print KC "0"; + close(KC); + } +} + +sub ck_zone ($@) { + my ($master_dir, @zone) = @_; + + for (@zone) { + my $zone = $_; + my $zpf = "$master_dir/$zone"; + my $keyfile; + my @content; + my @keylist; + + for (<$zpf/*>) { + if (m#(K$zone.*\.key)#) { + $keyfile = $1; + open(KEYFILE, "<", "$zpf/$keyfile") + or die "$zpf/$keyfile: $!\n"; + @content = ; + close(KEYFILE); + for (@content) { + if (m#DNSKEY.257#) { + push @keylist, $keyfile; + } + } + } + } + + open(INDEX, ">$zpf/.index.ksk") or die "$zpf/.index.ksk: $!\n"; + for (@keylist) { + s#\.key##; + print INDEX "$_\n"; + } + close(INDEX); + + print " * $zone: new .index.ksk created\n"; + if (-f "$zpf/.index.zsk") { + unlink("$zpf/.index.zsk") or die "$zpf/.index.zsk: $!\n"; + } + } +} + +sub post_create ($@) { + my ($master_dir, @zone) = @_; + for (@zone) { + my $zone = $_; + `touch $master_dir/$zone/$zone`; + &kill_useless_keys($zone, $master_dir); + &key_to_zonefile($zone, $master_dir); + } +} + +sub kill_useless_keys ($@) { + + # the function deletes all keys that are not available in the zone + + my $zone = $_[0]; + my $master_dir = $_[1]; + my @keylist = (); + my $zpf = "$master_dir/$zone"; + + open(INDEX, "<$zpf/.index.zsk") or die "$zpf/.index.zsk: $!\n"; + @keylist = ; + close(INDEX); + open(INDEX, "<$zpf/.index.ksk") or die "$zpf/.index.ksk: $!\n"; + push @keylist, ; + + # shortened the key name from the index file on the id in order to + # be able to compare + for (@keylist) { + chomp; + s#K.*\+.*\+(.*)#$1#; + } + + # reviewed every key file (KSK, ZSK), whether they are described in + # the respective index file. if not they will be deleted. + for (glob("$master_dir/$zone/K*")) { + chomp; + my $file = $_; + my $rm_count = 1; + my $keyname; + for (@keylist) { + if ($file =~ /$_/) { $rm_count = 0; } + } + if ($rm_count == 1) { + unlink "$file"; + if ($file =~ /$zpf\/(.*\.key)/) { + print " * $zone: Key $1 removed \n"; + } + } + } +} + +sub key_to_zonefile ($@) { + + # the function added all keys to the indexfile + my $zone = $_[0]; + my $master_dir = $_[1]; + my $zpf = "$master_dir/$zone"; + my @old_content; + my @new_content = (); + + open(ZONEFILE, "<$zpf/$zone"); + @old_content = ; + close(ZONEFILE); + + for (@old_content) { + unless (m#INCLUDE.*key#) { push @new_content, $_; } + } + + for (<$zpf/*>) { + if (m#(.*\/)(K.*\.key)#) { + push @new_content, "\$INCLUDE \"$2\"\n"; + } + } + open(ZONEFILE, ">$zpf/$zone") or die "$zpf/$zone: $!\n"; + print ZONEFILE @new_content; + close(ZONEFILE); +} + +__END__ + +=pod + +=head1 NAME + +dnssec-keytool + +=head1 SYNOPSIS + +dnssec-keytool {-z|-k|-r|-c} zone + +=head1 DESCRIPTION + +Blabla. + +=head1 OPTIONS + +=over + +=item B<-z> created a new ZSK + +=item B<-k> created a new ZSK and KSK + +=item B<-r> delete the key-set of a zone + +=item B<-c> created configuration files for the dnstools and a new ZSK for an existing KSK + +=back + +=cut + +# vim:sts=4 sw=4 aw ai sm: diff -r 53c95f2ff0ac -r 0e1e5027e9c0 bin/update-serial --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/update-serial Sun Jan 23 00:30:15 2011 +0100 @@ -0,0 +1,548 @@ +#!/usr/bin/perl + +use v5.10; +use strict; +use warnings; + +use File::Basename; +use Pod::Usage; +use Getopt::Long; +use File::Temp; +use IO::File; +use POSIX qw(strftime); +use if $ENV{DEBUG} => "Smart::Comments"; +use DNStools::Config qw(get_config); + +sub uniq(@); +sub zones(@); +sub changed_zones(); +sub update_index($); +sub signature_expired($); +sub need_rollover(); +sub done_rollover(); +sub begin_rollover(@); +sub end_rollover(@); +sub unlink_unused_keys($); +sub include_keys($); +sub sign($); +sub update_serial($); + +sub mk_zone_conf; +sub file_entry; +sub server_reload; + +my %config; +my %opt; + +MAIN: { + + GetOptions( + "sign-alert-time=i" => \$opt{sign_alert_time}, + "key-counter-end=i" => \$opt{key_counter_end}, + "h|help" => sub { pod2usage(-exit 0, -verbose => 1) }, + "m|man" => sub { + pod2usage( + -exit 0, + -verbose => 2, + -noperldoc => system("perldoc -v &>/dev/null") + ); + }, + ) or pod2usage; + + # merge the config and the defined options from commandline + %config = get_config("$ENV{DNSTOOLS_CONF}", "dnstools.conf", + "$ENV{HOME}/.dnstools.conf", "/etc/dnstools.conf", \%opt); + + our $bind_dir = $config{bind_dir}; + our $conf_dir = $config{zone_conf_dir}; + + my @candidates = @ARGV ? zones(@ARGV) : changed_zones; + push @candidates, update_index($config{indexzone}); + push @candidates, signature_expired($config{sign_alert_time}); + + my @need_rollover = need_rollover; + my @done_rollover = done_rollover; + + push @candidates, begin_rollover(@need_rollover); + push @candidates, end_rollover(@done_rollover); + + foreach my $zone (uniq(@candidates)) { + update_serial($zone); + sign($zone); + } + say "Need to ... file_entry, mk_zone_conf, server_reload"; + exit; + + file_entry; # bearbeitet die file-eintraege der konfigurations-datei + mk_zone_conf; # konfiguration zusammenfuegen + server_reload; # server neu laden + +} + +sub uniq(@) { + + # remove duplicate entries + my %all; + @all{@_} = (); + keys %all; +} + +sub zones(@) { + + # check whether the zones in argv are managed zones and + # insert them into the list new_serial + + my @r; + + foreach (@_) { + chomp(my $zone = `idn --quiet "$_"`); + die "$zone is not managed\n" + if not -e "$config{master_dir}/$zone/$zone"; + push @r, $zone; + } + + return @r; +} + +sub changed_zones() { + + # find candidates in our master dir + my @r; + + while (glob "$config{master_dir}/*") { + my $zone = basename($_); + + if (not -e "$_/.stamp") { + say " * $zone: no .stamp file found"; # NOCH IN NEW_SERIAL PUSHEN + push @r, $zone; + next; + } + + my $stamp_age = -M _; + my $file_age = -M "$_/$zone"; + + next if $stamp_age <= $file_age; # should be only < + + push @r, $zone; + say " * $zone: zone file modified"; + } + return @r; +} + +sub signature_expired($) { + my $sign_alert_time = shift; # the time between the end and the new signing + # (see external configuration) + my @r; + +# erzeugt $time (die zeit ab der neu signiert werden soll) +# ... warum eigentlich nur bis zu den Stunden und nicht auch Minuten und Sekunden? + my $time = strftime("%Y%m%d%H" => localtime time + 3600 * $sign_alert_time); + + ## vergleicht fuer alle zonen im ordner $config{master_dir} mit einer + ## .signed-datei den zeitpunkt in $time mit dem ablaufdatum der + ## signatur, welcher aus der datei .signed ausgelesen wird. + ZONE: while (my $dir = glob "$config{master_dir}/*") { + my $zone = basename $dir; + + next if not -e "$dir/$zone.signed"; + + open(my $fh, "$dir/$zone.signed") + or die "Can't open $dir/$zone.signed: $!\n"; + push @r, $zone + if /RRSIG\s+SOA[\d ]+(\d{10})\d{4}\s+\(/ ~~ [<$fh>] + and $1 < $time; + } + + return @r; +} + +sub sign($) { + + my $zone = shift; + my $dir = "$config{master_dir}/$zone"; + + my $pid = fork // die "Can't fork: $!"; + + if ($pid == 0) { + chdir $dir or die "Can't chdir to $dir: $!\n"; + exec "dnssec-signzone" => $zone; + die "Can't exec: $!\n"; + } + + wait == $pid or die "Child is lost: $!"; + die "Can't sign zone!" if $?; + + say " * $zone neu signiert"; + + open(my $fh, "+>>$dir/.keycounter") + or die "Can't open $dir/.keycounter for update: $!\n"; + seek($fh, 0, 0); + my $kc = <$fh>; + truncate($fh, 0); + say $fh ++$kc; +} + +sub update_serial($) { + + my $zone = shift; + + my $file = "$config{master_dir}/$zone/$zone"; + my $in = IO::File->new($file) or die "Can't open $file: $!\n"; + my $out = File::Temp->new(DIR => dirname $file) + or die "Can't open tmpfile: $!\n"; + my $_ = join "" => <$in>; + + my $serial; + s/^(\s+)(\d{10})(?=\s*;\s*serial)/$1 . ($serial = new_serial($2))/emi + or die "Serial number not found for replacement!"; + + print $out $_; + + close($in); + close($out); + + rename($out->filename => $file) + or die "Can't rename tmp to $file: $!\n"; + + $serial =~ s/\s*//g; + say " * $zone: serial incremented to $serial"; + + open(my $stamp, ">", dirname($file) . "/.stamp"); + print $stamp time() . " " . localtime() . "\n"; + + say " * $zone: stamp aktualisiert"; +} + +sub new_serial($) { + + my ($date, $cnt) = $_[0] =~ /(\d{8})(\d\d)/; + + state $now = strftime("%4Y%02m%02d", localtime); + + return $date eq $now + ? sprintf "%s%02d", $date, $cnt + 1 + : "${now}00"; + +} + +sub mk_zone_conf { + + # erzeugt eine named.conf-datei aus den entsprechenden vorlagen. + our $bind_dir; + our $conf_dir; + + open(TO, ">$bind_dir/named.conf.zones") + or die "$bind_dir/named.conf.zones: $!\n"; + while (<$conf_dir/*>) { + open(FROM, "$_") or die "$_: $! \n"; + print TO ; + close(FROM); + } + close(TO); + print "** zonekonfiguration erzeugt\n"; +} + +sub update_index($) { + my $indexzone = shift; + + my @iz; + + { + open(my $fh, "$config{master_dir}/$indexzone/$indexzone") + or die "$config{master_dir}/$indexzone/$indexzone: $!\n"; + chomp(@iz = grep !/ZONE::/ => <$fh>); + } + + for my $dir (glob "$config{master_dir}/*") { + my $zone = basename($dir); + my $info = -e ("$dir/.keycounter") ? "sec-on" : "sec-off"; + push @iz, join "::", "\t\tIN TXT\t\t\"ZONE", $zone, $info; + } + + { + my $fh = File::Temp->new(DIR => "$config{master_dir}/$indexzone") + or die "Can't create tmpdir: $!\n"; + print $fh join "\n" => @iz, ""; + rename($fh->filename => "$config{master_dir}/$indexzone/$indexzone") + or die "Can't rename " + . $fh->filename + . " to $config{master_dir}/$indexzone/$indexzone: $!\n"; + } + + say "** index-zone aktualisiert"; + return $indexzone; +} + +sub file_entry { + + # prueft jede domain, die ein verzeichnis in $config{master_dir} hat, ob sie + # dnssec nutzt. + # passt die eintraege in $config_file falls noetig an. + our $conf_dir; + + while (glob "$config{master_dir}/*") { + s#($config{master_dir}/)(.*)#$2#; + my $zone = $_; + my $zone_file = "$config{master_dir}/$zone/$zone"; + my $conf_file = "$conf_dir/$zone"; + my @c_content; + + unless (-f "$conf_file") { + die "$conf_file: $! \n"; + } + + if (-e "$config{master_dir}/$zone/.keycounter") { + open(FILE, "<$conf_file") or die "$conf_file: $!\n"; + @c_content = ; + close(FILE); + for (@c_content) { + if (m{(.*)($zone_file)(";)}) { + print + " * zonekonfiguration aktualisiert ($2 ==> $2.signed)\n"; + $_ = "$1$2.signed$3\n"; + } + } + open(FILE, ">$conf_file") or die "$conf_file: $!\n"; + print FILE @c_content; + close(FILE); + } + else { + open(FILE, "<$conf_file") or die "$conf_file: $!\n"; + @c_content = ; + close(FILE); + for (@c_content) { + if (m{(.*)($zone_file)\.signed(.*)}) { + print + " * zonekonfiguration aktualisiert ($2.signed ==> $2)\n"; + $_ = "$1$2$3\n"; + } + } + open(FILE, ">$conf_file") or die "$conf_file: $!\n"; + print FILE @c_content; + close(FILE); + } + } +} + +sub server_reload { + if (`rndc reload`) { print "** reload dns-server \n" } +} + +sub need_rollover() { + + # gibt alle zonen mit abgelaufenen keycounter + my @r; + + while (my $kc = glob "$config{master_dir}/*/.keycounter") { + my $zone = basename dirname $kc; + my $key; + + { + open(my $fh, $kc) or die "$kc: $!\n"; + chomp($key = <$fh>); + } + + push @r, $zone if $config{key_counter_end} <= $key; + } + + return @r; +} + +sub done_rollover() { + + # funktion ueberprueft ob ein keyrollover fertig ist + # die bedingung dafuer ist das: + # - eine datei .index.zsk vorhanden ist + # - die datei .index.zsk älter ist, als die rollover-Zeit + # - die datei .index.zsk ueber mehr als eine zeile gross ist + # (also mehr als einen Schlüssel enthält) + my @r; + my $now = time; + + while (my $dir = glob "$config{master_dir}/*") { + my $zone = basename $dir; + + my @index = (); + my $index_wc; + + # prueft nach der ".index.zsk"-datei und erstellt den zeitpunkt + # an dem das key-rollover endet. + # rollover is done when mtime of the .index.zsk + abl_zeit is + # in the past + next if not -e "$dir/.index.zsk"; + next if (stat _)[9] + 3600 * $config{abl_zeit} >= $now; + + # prueft die anzahl der schluessel in der .index.zsk + open(my $fh, "$dir/.index.zsk") or die "$dir/.index.zsk: $!\n"; + (<$fh>); + push @r, $zone if $. > 1; + } + + return @r; +} + +sub begin_rollover(@) { + my @zones = @_; + my @r; + + # anfang des key-rollovers + + foreach my $zone (@zones) { + + # erzeugt zsks + my $dir = "$config{master_dir}/$zone"; + my ($keyname, @keys); + + # create a new key + { # need to change the direcoty, thus some more effort + # alternativly: $keyname = `cd $dir && dnssec-keygen ...`; + # would do, but is more fragile on shell meta characters + + open(my $keygen, "-|") or do { + chdir $dir or die "Can't chdir to $dir: $!\n"; + exec "dnssec-keygen", + -a => "RSASHA1", + -b => 512, + -n => "ZONE", + $zone; + die "Can't exec: $!"; + }; + chomp($keyname = <$keygen>); + close($keygen) or die "dnssec-keygen failed: $@"; + } + + open(my $fh, "+>>$dir/.index.zsk") or die "$dir/.index.zsk: $!\n"; + seek($fh, 0, 0); + chomp(@keys = <$fh>); + + ### @keys + + push @keys, $keyname; + shift @keys if @keys > 2; + + truncate($fh, 0) or die "truncate"; + print $fh join "\n" => @keys; + + print " * $zone: neuer ZSK $keyname erstellt\n"; + + open($fh, ">$dir/.keycounter") or die "$dir/.keycounter: $!\n"; + say $fh 0; + close($fh); + + unlink_unused_keys($zone); + include_keys($zone); + push @r, $zone; + } + + return @r; +} + +sub include_keys($) { + + # die funktion fugt alle schluessel in eine zonedatei + my $zone = shift; + my $dir = "$config{master_dir}/$zone"; + + my $in = IO::File->new("$dir/$zone") or die "Can't open $dir/$zone: $!\n"; + my $out = File::Temp->new(DIR => $dir) or die "Can't open tmpfile: $!\n"; + + print $out grep { !/\$include\s+.*key/i } $in; + print $out map { "\$INCLUDE @{[basename $_]}\n" } glob "$dir/K*key"; + + close $in; + close $out; + rename($out->filename => "$dir/$zone") + or die "Can't rename tmp to $dir/$zone: $!\n"; + +} + +sub unlink_unused_keys($) { + + # die funktion loescht alle schluessel die nicht in der index.zsk + # der uebergebenen zone stehen + my $zone = shift; + + my @keys; + my $dir = "$config{master_dir}/$zone"; + + { + + # collect the keys and cut everything except the key id + # we cut the basenames (w/o the .private|.key suffix) + open(my $zsk, "<$dir/.index.zsk") or die "$dir/.index.zsk: $!\n"; + open(my $ksk, "<$dir/.index.ksk") or die "$dir/.index.ksk: $!\n"; + @keys = (<$zsk>, <$ksk>); + } + + # prueft alle schluesseldateien (ksk, zsk) ob sie in der jeweiligen + # indexdatei beschrieben sind. wenn nicht werden sie geloescht. + for my $file (glob "$dir/K*.key $dir/K*.private") { + unlink $file if basename($file, ".key", ".private") ~~ @keys; + } +} + +sub end_rollover(@) { + + my @zones = @_; + my @r; + + foreach my $zone (@zones) { + + my $dir = "$config{master_dir}/$zone"; + + open(my $fh, "+>>$dir/.index.zsk") + or die "Can't open $dir/.index.zsk: $!\n"; + seek($fh, 0, 0); + chomp(my @keys = <$fh>); + + if (@keys > 1) { + truncate($fh, 0); + say $fh $keys[-1]; + } + close($fh); + + unlink_unused_keys($zone); + include_keys($zone); + push @r => $zone; + } + + return @r; +} + +__END__ + +=head1 NAME + + update-serial - updates the serial numbers and re-signs the zone files + +=head1 SYNOPSIS + + update-serial [options] [zone...] + +=head1 DESCRIPTION + +B scans the configured directories for modified zone files. On any +file found it increments the serial number and signs the zone, if approbiate. + +=head1 OPTIONS + +=over + +=item B<--sign-alert-time> I + +=item B<--key-counter-end> I + +Maximum number if key usages. + + +=back + +The common options B<-h>|B<--help>|B<-m>|B<--man> are supported. + +=head1 AUTHOR + +L + +=cut + +# vim:sts=4 sw=4 aw ai sm: diff -r 53c95f2ff0ac -r 0e1e5027e9c0 bin/zone-ls --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/zone-ls Sun Jan 23 00:30:15 2011 +0100 @@ -0,0 +1,144 @@ +#! /usr/bin/perl + +use v5.10; +use strict; +use warnings; +use Pod::Usage; +use File::Basename; +use Time::Local; +use Getopt::Long; +use if $ENV{DEBUG} => "Smart::Comments"; +use DNStools::Config qw(get_config); + +my %config; +my $opt_expiry = undef; + +MAIN: { + my %info; # will hold the information we collected + + GetOptions( + "e|expiry" => \$opt_expiry, + "h|help" => sub { pod2usage(-exit => 0, -verbose => 1) }, + "m|man" => sub { + pod2usage( + -exit => 0, + -verbose => 2, + -noperldoc => system("perldoc -V &>/dev/null") + ); + }, + ) or pod2usage; + + %config = get_config(); + die "$config{master_dir}: $!\n" if not -d $config{master_dir}; + + foreach my $dir (grep { -d } glob "$config{master_dir}/*") { + + my $zone = basename($dir); + $info{$zone} = { status => "OK" }; + + if (not -f "$dir/.index.zsk") { + $info{$zone}{zsk} = 0; + $info{$zone}{ksk} = 0; + $info{$zone}{kc} = 0; + $info{$zone}{end} = "-"; + $info{$zone}{expiry} = undef; + next; + } + + # prueft wie viele zsks genutzt werden + { + open(my ($fh), $_ = "<$dir/.index.zsk") + or die "Can't open $_: $!\n"; + () = <$fh>; + $info{$zone}{zsk} = $. + } + + # prueft wie viele ksks genutzt werden + { + open(my ($fh), $_ = "<$dir/.index.ksk") + or die "Can't open $_: $!\n"; + () = <$fh>; + $info{$zone}{ksk} = $. + } + + # prueft wie oft die schluessel zum signieren genutzt wurden + { + open(my ($fh), $_ = "<$dir/.keycounter") + or die "Can't open $_: $!\n"; + chomp($info{$zone}{kc} = <$fh>); + } + + # prueft das ablaufdatum + if (!-f "$dir/$zone.signed") { + $info{$zone}{end} = "-"; + next; + } + + open(my ($fh), $_ = "<$dir/$zone.signed") or die "Can't open $_: $!\n"; + while (<$fh>) { + next if not /RSIG\s+SOA\s.*\s + (?\d\d\d\d) + (?\d\d) + (?\d\d) + (?\d\d) + (?\d\d)\d+\s\(/ix; + $info{$zone}{end} = "$+{day}.$+{mon}.$+{year} $+{hour}:$+{min}"; + $info{$zone}{expiry} = + timelocal(0, $+{min}, $+{hour}, $+{day}, $+{mon} - 1, $+{year}); + } + } + + { # output + + my $sort_by = + $opt_expiry + ? sub { ($info{$a}{expiry} // 2**64) <=> ($info{$b}{expiry} // 2**64) } + : sub { $a cmp $b }; + + my $format_h = "%-35s %-8s %1s/%1s %3s %7s\n"; + my $format_l = "%-35s %-8s %1d/%1d %5d %19s\n"; + + printf $format_h => qw(Domain Status ZSK KSK Used Sig-end); + + foreach my $zone (sort $sort_by keys %info) { + printf $format_l => $zone, + @{ $info{$zone} }{qw(status zsk ksk kc end)}; + } + } +} + +__END__ + +=head1 NAME + + zone-ls -- lists all zones + +=head1 SYNOPSIS + + zone-ls [-e|--expiry] + +=head1 DESCRIPTION + +This B lists all zones under control of our dnstools suite. The output is ordered by domain name. + +=head1 OPTIONS + +=over + +=item B<-e>|B<--expiry> + +Order the output by expiry date. The sooner the key expires, the more top the +domain is listed. + +=back + +Additionally the common B<-h>|B<--help>|B<-m>|B<--man> options, which should be +self explanatory. + +=head1 AUTHORS + +L + +=cut + +# vim:ts=4 sw=4 ai si aw: diff -r 53c95f2ff0ac -r 0e1e5027e9c0 bin/zone-mk --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/zone-mk Sun Jan 23 00:30:15 2011 +0100 @@ -0,0 +1,120 @@ +#!/usr/bin/perl + +use 5.010; +use warnings; +use strict; +use Pod::Usage; +use if $ENV{DEBUG} => "Smart::Comments"; +use Cwd qw(abs_path); +use File::Path qw(make_path); +use File::Basename; +use Getopt::Long; +use Net::LibIDN qw(:all); +use DNStools::Config qw(get_config); +use Template; + +my $CHARSET = "UTF-8"; + +my $opt_force = 0; + +MAIN: { + + my %cf = get_config; + + GetOptions( + "f|force" => \$opt_force, + "h|help" => sub { pod2usage(-verbose => 1, -exit => 0) }, + "m|man" => sub { + pod2usage( + -verbose => 2, + -noperldoc => system("perldoc -V &>/dev/null"), + -exit => 0 + ); + }, + ) + and @ARGV >= 2 + or pod2usage; + + my $customer = shift; + + die "$cf{master_dir}: $!" if not -d -r -x $cf{master_dir}; + die "$cf{zone_conf_dir}: $!" if not -d -r -x $cf{zone_conf_dir}; + + # legt fuer jede domain in @ARGV ein verzeichnis in $master_dir an. + # schreibt aus den angegebenen templates die dateien $zonefile und $config + # in die entsprechenden verzeichnisse. + for my $utf8domain (@ARGV) { + + my $domain = idn_to_ascii($utf8domain, $CHARSET); + my $zonefile = "$cf{master_dir}/$domain/$domain"; + my $configfile = "$cf{zone_conf_dir}/$domain"; + my $now = time; + + make_path dirname $zonefile; + + if (-f $zonefile and not $opt_force) { + say "skipping $utf8domain: zone file '$zonefile' exists."; + next; + } + + if (-f $configfile and not $opt_force) { + say "skipping $utf8domain: config file '$configfile' exists."; + next; + } + + say "domain $utf8domain ($domain) for $customer."; + + my %vars = ( + domain => $domain, + utf8domain => $utf8domain, + now => $now, + zonefile => abs_path($zonefile), + customer => $customer, + hostmaster => $cf{hostmaster}, + primary => $cf{primary}, + secondary => $cf{secondary}, + ); + + my $tt = Template->new(INCLUDE_PATH => $cf{template_dir}) + or die "$Template::ERROR\n"; + + $tt->process("named.zone", \%vars, $zonefile) || die $tt->error; + $tt->process("named.config", \%vars, $configfile) || die $tt->error; + + } + +} + +__END__ + +=head1 NAME + + zone-mk - create a new zone + +=head1 SYNOPSIS + + zone-mk [-f|--force] ... + +=head1 DESCRIPTION + +B creates a new DNS zone file and the config snipped. + +=head1 OPTIONS + +=over + +=item B<-f>|B<--force> + +Crate zone file and config even if they exist. (default: off) + +=item B<-h>|B<--help> + +=item B<-m>|B<--man> + +=back + +=head1 FILES + +The F is used. + +=cut diff -r 53c95f2ff0ac -r 0e1e5027e9c0 bin/zone-rm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/zone-rm Sun Jan 23 00:30:15 2011 +0100 @@ -0,0 +1,32 @@ +#!/usr/bin/perl + +use warnings; +use strict; +use File::Path; +use DNStools::Config qw(get_config); + +# liest die Konfiguration ein +my %config = get_config(); + +my $master_dir = $config{"master_dir"}; +my $conf_dir = $config{"zone_conf_dir"}; + +for (@ARGV) { + chomp(my $zone = `idn --quiet "$_"`); + + if (-d "$master_dir/$zone") { + rmtree "$master_dir/$zone/" + and print "zone-dir for $zone removed\n"; + } + else { + print "$master_dir/$zone: $!\n"; + } + + if (-e "$conf_dir/$zone") { + unlink "$conf_dir/$zone" + and print "configuration-file for $zone removed\n"; + } + else { + print "$conf_dir/$zone: $!\n"; + } +} diff -r 53c95f2ff0ac -r 0e1e5027e9c0 dnssec-keytool.pl --- a/dnssec-keytool.pl Tue Dec 21 17:00:11 2010 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,370 +0,0 @@ -#! /usr/bin/perl - -use warnings; -use strict; -use FindBin; - -sub del_double { - my %all; - grep { $all{$_} = 0 } @_; - return (keys %all); -} - -sub read_conf { - - # read configuration - my @configs = ("$FindBin::Bin/dnstools.conf", "/etc/dnstools.conf"); - our %config; - - for (grep { -f } @configs) { - open(CONFIG, $_) or die "Can't open $_: $!\n"; - } - unless (seek(CONFIG, 0, 0)) { - die "Can't open config (searched: @configs)\n"; - } - while () { - chomp; - s/#.*//; - s/\t//g; - s/\s//g; - - next unless length; - my ($cname, $ccont) = split(/\s*=\s*/, $_, 2); - $config{$cname} = $ccont; - } - close(CONFIG); -} - -sub read_argv { - # evaluate argv or print the help - my $arg = shift @ARGV; - my $zone; - our $do; - our @zones; - our $master_dir; - - if (!defined $arg) { - print " usage: dnssec-keytool