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