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