--- /dev/null
+#!/usr/bin/perl
+
+# Copyright (c) 2010 Peter Palfrader <peter@palfrader.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+use strict;
+use warnings;
+use English;
+use Net::DNS::Resolver;
+use Getopt::Long;
+
+$SIG{'__DIE__'} = sub { print @_; exit 4; };
+
+my $RES = Net::DNS::Resolver->new;
+my $DLV = 'dlv.isc.org';
+
+sub get_tag_generic {
+ my $zone = shift;
+ my $type = shift;
+
+ my @result;
+ my $pkt = $RES->send($zone, $type);
+ return () unless $pkt;
+ return () unless $pkt->answer;
+ for my $rr ($pkt->answer) {
+ next unless ($rr->type eq $type);
+ next unless ($rr->name eq $zone);
+
+ # only handle KSKs, i.e. keys with the SEP flag set
+ next if ($type eq 'DNSKEY' && !($rr->is_sep));
+
+ push @result, $rr->keytag;
+ };
+ my %unique = ();
+ @result = sort {$a <=> $b} grep {!$unique{$_}++} @result;
+ return @result
+};
+
+sub get_dnskeytags {
+ my $zone = shift;
+ return get_tag_generic($zone, 'DNSKEY');
+};
+sub get_dstags {
+ my $zone = shift;
+ return get_tag_generic($zone, 'DS');
+};
+sub get_dlvtags {
+ my $zone = shift;
+ $zone .= ".".$DLV;
+ return get_tag_generic($zone, 'DLV');
+};
+
+sub usage {
+ my $fd = shift;
+ my $exit = shift;
+
+ print $fd "Usage: $PROGRAM_NAME overview|check-dlv|check-ds zone [zone...]\n";
+ print $fd " $PROGRAM_NAME --dir <dir> overview|check-dlv|check-ds zone [zone...]\n";
+ print $fd " $PROGRAM_NAME --help\n";
+ exit $exit;
+}
+
+my $params;
+Getopt::Long::config('bundling');
+GetOptions (
+ '--help' => \$params->{'help'},
+ '--dir=s' => \$params->{'dir'},
+ '--dlv=s' => \$params->{'dlv'},
+) or usage(\*STDERR, 1);
+usage(\*STDOUT, 0) if ($params->{'help'});
+
+my $mode = shift @ARGV;
+
+my @zones;
+if (scalar @ARGV) {
+ warn "--dir option ignored" if defined $params->{'dir'};
+ @zones = @ARGV;
+} else {
+ my $dir = $params->{'dir'};
+ usage(\*STDOUT, 0) unless (defined $dir);
+
+ chdir $dir or die "chdir $dir failed? $!\n";
+ opendir DIR, '.' or die ("Cannot opendir $dir\n");
+ for my $file (readdir DIR) {
+ next if ( -l "$file" );
+ next unless ( -f "$file" );
+ next if $file =~ /^(dsset|keyset)-/;
+
+ push @zones, $file;
+ }
+ closedir(DIR);
+};
+
+usage(\*STDOUT, 0) unless (defined $mode && $mode =~ /^(overview|check-dlv|check-ds)$/);
+$DLV = $params->{'dlv'} if $params->{'dlv'};
+
+my %data;
+for my $zone (@zones) {
+ $data{$zone} = { 'dnskey' => join(', ', get_dnskeytags($zone)),
+ 'ds' => join(', ', get_dstags($zone)),
+ 'dlv' => join(', ', get_dlvtags($zone)) };
+}
+
+if ($mode eq 'overview') {
+ my $format = "%30s %-16s %-16s %-16s\n";
+ printf $format, "zone", "DNSKEY", "DS in parent", "DLV";
+ printf $format, "-"x 30, "-"x 16, "-"x 16, "-"x 16;
+ for my $zone (sort {$a cmp $b} keys %data) {
+ printf $format, $zone,
+ $data{$zone}->{'dnskey'},
+ $data{$zone}->{'ds'},
+ $data{$zone}->{'dlv'};
+ }
+ exit(0);
+} elsif ($mode eq 'check-dlv' || $mode eq 'check-ds') {
+ my $key;
+ $key = 'dlv' if $mode eq 'check-dlv';
+ $key = 'ds' if $mode eq 'check-ds';
+ die ("key undefined") unless $key;
+
+ my @warn;
+ my @ok;
+ for my $zone (sort {$a cmp $b} keys %data) {
+ my $dnskey = $data{$zone}->{'dnskey'} || '-';
+ my $target = $data{$zone}->{$key} || '-';
+
+ if ($dnskey ne $target) {
+ push @warn, "$zone ($dnskey != $target)";
+ } else {
+ push @ok, "$zone ($dnskey)";
+ };
+ }
+ print "WARNING: ", join(", ", @warn), "\n" if (scalar @warn);
+ print "OK: ", join(", ", @ok), "\n" if (scalar @ok);
+ exit (1) if (scalar @warn);
+ exit (0);
+} else {
+ die ("Invalid mode '$mode'\n");
+};
+