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