1#!/usr/bin/env perl 2# 3# Copyright (C) Internet Systems Consortium, Inc. ("ISC") 4# 5# This Source Code Form is subject to the terms of the Mozilla Public 6# License, v. 2.0. If a copy of the MPL was not distributed with this 7# file, you can obtain one at https://mozilla.org/MPL/2.0/. 8# 9# See the COPYRIGHT file distributed with this work for additional 10# information regarding copyright ownership. 11 12use strict; 13use warnings; 14 15sub process_changeset; 16 17my @changeset; 18 19while( my $line = <> ) { 20 chomp $line; 21 22 if( $line =~ /^(?<op>add|del) (?<label>\S+)\s+(?<ttl>\d+)\s+IN\s+(?<rrtype>\S+)\s+(?<rdata>.*)/ ) { 23 my $change = { 24 op => $+{op}, 25 label => $+{label}, 26 ttl => $+{ttl}, 27 rrtype => $+{rrtype}, 28 rdata => $+{rdata}, 29 }; 30 31 if( $change->{op} eq 'del' and $change->{rrtype} eq 'SOA' ) { 32 if( @changeset ) { 33 process_changeset( @changeset ); 34 @changeset = (); 35 } 36 } 37 38 push @changeset, $change; 39 } 40 else { 41 die "error parsing journal data"; 42 } 43} 44 45if( @changeset ) { 46 process_changeset( @changeset ); 47} 48 49{ 50 my %rrsig_db; 51 my %keys; 52 my $apex; 53 54 sub process_changeset { 55 my @changeset = @_; 56 57 if( not $apex ) { 58 # the first record of the first changeset is guaranteed to be the apex 59 $apex = $changeset[0]{label}; 60 } 61 62 my $newserial; 63 my %touched_rrsigs; 64 my %touched_keys; 65 66 foreach my $change( @changeset ) { 67 if( $change->{rrtype} eq 'SOA' ) { 68 if( $change->{op} eq 'add' ) { 69 if( $change->{rdata} !~ /^\S+ \S+ (?<serial>\d+)/ ) { 70 die "unable to parse SOA"; 71 } 72 73 $newserial = $+{serial}; 74 } 75 } 76 elsif( $change->{rrtype} eq 'NSEC' ) { 77 ; # do nothing 78 } 79 elsif( $change->{rrtype} eq 'DNSKEY' ) { 80 ; # ignore for now 81 } 82 elsif( $change->{rrtype} eq 'TYPE65534' and $change->{label} eq $apex ) { 83 # key status 84 if( $change->{rdata} !~ /^\\# (?<datasize>\d+) (?<data>[0-9A-F]+)$/ ) { 85 die "unable to parse key status record"; 86 } 87 88 my $datasize = $+{datasize}; 89 my $data = $+{data}; 90 91 if( $datasize == 5 ) { 92 my( $alg, $id, $flag_del, $flag_done ) = unpack 'CnCC', pack( 'H10', $data ); 93 94 if( $change->{op} eq 'add' ) { 95 if( not exists $keys{$id} ) { 96 $touched_keys{$id} //= 1; 97 98 $keys{$id} = { 99 $data => 1, 100 rrs => 1, 101 done_signing => $flag_done, 102 deleting => $flag_del, 103 }; 104 } 105 else { 106 if( not exists $keys{$id}{$data} ) { 107 my $keydata = $keys{$id}; 108 $touched_keys{$id} = { %$keydata }; 109 110 $keydata->{rrs}++; 111 $keydata->{$data} = 1; 112 $keydata->{done_signing} += $flag_done; 113 $keydata->{deleting} += $flag_del; 114 } 115 } 116 } 117 else { 118 # this logic relies upon the convention that there won't 119 # ever be multiple records with the same flag set 120 if( exists $keys{$id} ) { 121 my $keydata = $keys{$id}; 122 123 if( exists $keydata->{$data} ) { 124 $touched_keys{$id} = { %$keydata }; 125 126 $keydata->{rrs}--; 127 delete $keydata->{$data}; 128 $keydata->{done_signing} -= $flag_done; 129 $keydata->{deleting} -= $flag_del; 130 131 if( $keydata->{rrs} == 0 ) { 132 delete $keys{$id}; 133 } 134 } 135 } 136 } 137 } 138 else { 139 die "unexpected key status record content"; 140 } 141 } 142 elsif( $change->{rrtype} eq 'RRSIG' ) { 143 if( $change->{rdata} !~ /^(?<covers>\S+) \d+ \d+ \d+ (?<validity_end>\d+) (?<validity_start>\d+) (?<signing_key>\d+)/ ) { 144 die "unable to parse RRSIG rdata"; 145 } 146 147 $change->{covers} = $+{covers}; 148 $change->{validity_end} = $+{validity_end}; 149 $change->{validity_start} = $+{validity_start}; 150 $change->{signing_key} = $+{signing_key}; 151 152 my $db_key = $change->{label} . ':' . $change->{covers}; 153 154 $rrsig_db{$db_key} //= {}; 155 $touched_rrsigs{$db_key} = 1; 156 157 if( $change->{op} eq 'add' ) { 158 $rrsig_db{$db_key}{ $change->{signing_key} } = 1; 159 } 160 else { 161 # del 162 delete $rrsig_db{$db_key}{ $change->{signing_key} }; 163 } 164 } 165 } 166 167 foreach my $key_id( sort keys %touched_keys ) { 168 my $old_data; 169 my $new_data; 170 171 if( ref $touched_keys{$key_id} ) { 172 $old_data = $touched_keys{$key_id}; 173 } 174 175 if( exists $keys{$key_id} ) { 176 $new_data = $keys{$key_id}; 177 } 178 179 if( $old_data ) { 180 if( $new_data ) { 181 print "at serial $newserial key $key_id status changed from ($old_data->{deleting},$old_data->{done_signing}) to ($new_data->{deleting},$new_data->{done_signing})\n"; 182 } 183 else { 184 print "at serial $newserial key $key_id status removed from zone\n"; 185 } 186 } 187 else { 188 print "at serial $newserial key $key_id status added with flags ($new_data->{deleting},$new_data->{done_signing})\n"; 189 } 190 } 191 192 foreach my $rrsig_id( sort keys %touched_rrsigs ) { 193 my $n_signing_keys = keys %{ $rrsig_db{$rrsig_id} }; 194 195 if( $n_signing_keys == 0 ) { 196 print "at serial $newserial $rrsig_id went unsigned\n"; 197 } 198 elsif( $rrsig_id =~ /:DNSKEY$/ ) { 199 if( $n_signing_keys != 2 ) { 200 print "at serial $newserial $rrsig_id was signed $n_signing_keys time(s) when it should have been signed twice\n"; 201 } 202 } 203 elsif( $n_signing_keys > 1 ) { 204 my @signing_keys = sort { $a <=> $b } keys %{ $rrsig_db{$rrsig_id} }; 205 print "at serial $newserial $rrsig_id was signed too many times, keys (@signing_keys)\n"; 206 } 207 } 208 } 209} 210