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