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