1#!/bin/env perl
2##############################################################################
3#
4# These parameters intended to be changed
5#
6
7my $INSTALL_ROOT = "/";
8my $SYSTEM_LOG = "/var/log/messages";
9my $LOG_FILE = "/var/opendnssec/ods-sequencer.log";
10my $ZONE_NAME = "example.com";
11my $verbose = 0;
12
13##############################################################################
14#
15# The implementation below should not be needed to be changed.
16#
17
18use strict;
19use warnings;
20use MIME::Base64;
21use XML::LibXML qw( );
22use IO::Handle;
23use IO::Select;
24use Symbol qw(qualify_to_ref);
25use File::Copy qw(copy);
26use Date::Parse;
27use Cwd 'abs_path';
28use POSIX qw(strftime);
29
30sub sysreadline(*;$) {
31    my($handle, $timeout) = @_;
32    $handle = qualify_to_ref($handle, caller( ));
33    my $infinitely_patient = (@_ = 1 || $timeout < 0);
34    my $start_time = time( );
35    my $selector = IO::Select->new( );
36    $selector->add($handle);
37    my $line = "";
38SLEEP:
39    until (at_eol($line)) {
40        unless ($infinitely_patient) {
41            return $line if time( ) > ($start_time + $timeout);
42        }
43        # sleep only 1 second before checking again
44        next SLEEP unless $selector->can_read(1.0);
45INPUT_READY:
46        while ($selector->can_read(0.0)) {
47            my $was_blocking = $handle->blocking(0);
48CHAR:       while (sysread($handle, my $nextbyte, 1)) {
49                $line .= $nextbyte;
50                last CHAR if $nextbyte eq "\n";
51            }
52            $handle->blocking($was_blocking);
53            # if incomplete line, keep trying
54            next SLEEP unless at_eol($line);
55            last INPUT_READY;
56        }
57    }
58    return $line;
59}
60sub at_eol($) { $_[0] =~ /\n\z/ }
61
62my $matchmonitorlogfound;
63
64sub startmonitorlog() {
65    open(LOG, "tail -f " . $SYSTEM_LOG . " |");
66    $matchmonitorlogfound=0;
67}
68
69sub matchmonitorlog() {
70    my $line;
71    return 1 if($matchmonitorlogfound);
72    do {
73        $line = sysreadline(LOG, 0);
74        if($line =~ /STATS/) {
75            $matchmonitorlogfound = 1;
76            return 1;
77        }
78    } while(at_eol($line));
79    return 0;
80}
81
82sub endmonitorlog() {
83    close(LOG);
84}
85
86sub makeannotatedsignconf {
87    my $inputsignconf = $_[0];
88    my $backupfile = $_[1];
89    my $outputsignconf = $_[2];
90    # Read in zone file
91    my $parser = XML::LibXML->new();
92    my $document = XML::LibXML->load_xml(location => $inputsignconf);
93
94    # for each KSK key look up entry in backup file of Key field and
95    # actual DNSKEY field.  Then add ResourceRecord entity to KSK key entry
96    # of parsed signconf XML.
97    foreach my $keyNode ($document->findnodes('//SignerConfiguration/Zone/Keys/Key')) {
98        my $flagsValue = $keyNode->findvalue('Flags/text()');
99        if($flagsValue eq "257") {
100            my $locatorNode = $keyNode->find('Locator')->get_node(1);
101            my $locatorValue = $keyNode->findvalue('Locator/text()');
102            my $resourcerecord = "";
103            my $keytag = "skip";
104            open(FILE, $backupfile);
105            while(<FILE>) {
106                if(m/;;Key: locator $locatorValue algorithm \d+ flags 257 publish \d+ ksk \d+ zsk \d+ keytag (\d+)/) {
107                    $keytag=$1;
108                }
109                if(m/^(.*	.*	IN	DNSKEY	257 \d+ \d+ .*) ;{id = $keytag \(ksk\), size .*}$/) {
110                    $resourcerecord = encode_base64($1);
111                }
112            }
113            close(FILE);
114            foreach my $locatorNode ($keyNode->findnodes('Locator')) {
115                $keyNode->removeChild($locatorNode);
116            }
117            my $resourceNode = XML::LibXML::Element->new('ResourceRecord');
118            $resourceNode->appendText($resourcerecord);
119            $keyNode->appendChild($resourceNode);
120        }
121    }
122
123    # also retrieve any signatures over the DNSKEY entries and add a SignatureRecordRecord for them
124    my $resourcerecord = "";
125    open(FILE, $backupfile);
126    while(<FILE>) {
127            if(m/^(.*	.*	IN	RRSIG	DNSKEY \d+ \d+ \d+ \d+ \d+ \d+ .* .*); {locator .* flags 257}$/) {
128                my $resourceNode = XML::LibXML::Element->new('SignatureResourceRecord');
129                $resourceNode->appendText(encode_base64($1));
130                $document->find('//SignerConfiguration/Zone/Keys')->get_node(1)->appendChild($resourceNode);;
131            }
132    }
133    close(FILE);
134
135    # Output the signconf
136    open(FILE, "| xmllint --format - > " . $outputsignconf);
137    print FILE $document->toString();
138    close(FILE);
139}
140
141sub enforcerinfo() {
142    my $busy;
143    my $timenow=0;
144    my $timenext=0;
145    my @dsseenkeys;
146
147    do {
148        $busy=0;
149        sleep(3);
150        open(FILE, "./sbin/ods-enforcer queue 2>>$LOG_FILE |");
151        while(<FILE>) {
152            $busy=1  if(m/^Next task scheduled immediately/);
153            $busy=1  if(m/^Working with/);
154            if(m/^It is now.*\(([0-9][0-9]*)[^)]*\).*$/) {
155                $timenow=$1;
156            }
157            if(m/^Next task scheduled.*\(([0-9][0-9]*)[^)]*\).*$/) {
158                $timenext=$1;
159            }
160        }
161        close(FILE);
162    } while($busy);
163
164    die "ERROR: enforcer not running properly\n"  if($timenow==0);
165
166    open(FILE, "./sbin/ods-enforcer 2>>$LOG_FILE key list --verbose |");
167    while(<FILE>) {
168        if(m/^(\S+).*\s+waiting for ds-seen\s+\d+\s+\d+\s+([0-9a-fA-F]+)\s*.*$/) {
169            push(@dsseenkeys, $2);
170        }
171    }
172    close(FILE);
173
174    return ($timenow, $timenext, @dsseenkeys);
175}
176
177sub enforceridle() {
178    my $busy;
179    do {
180        $busy=0;
181        sleep(3);
182        open(FILE, "./sbin/ods-enforcer queue 2>>$LOG_FILE |");
183        while(<FILE>) {
184            $busy=1  if(m/^Next task scheduled immediately/);
185            $busy=1  if(m/^Working with/);
186        }
187        close(FILE);
188    } while($busy);
189}
190
191sub makesignedzone {
192    my $timenow;
193    my $timenext;
194    my @dsseenkeys;
195    my $timecurrent = $_[0];
196    startmonitorlog();
197    print "  ..signing zone\n"  if($verbose);
198    system("./sbin/ods-signerd 2>>$LOG_FILE >>$LOG_FILE --set-time " . $timecurrent);
199    sleep(10);
200    system("./sbin/ods-signer 2>>$LOG_FILE >>$LOG_FILE sign --all");
201    print "  ..waiting for signed zone\n"  if($verbose);
202    while(!matchmonitorlog()) {
203        sleep(1);
204    }
205    print "  ..stopping signer\n"   if($verbose);
206    system("./sbin/ods-signer 2>>$LOG_FILE >>$LOG_FILE stop");
207    print "  ..annotating signconf\n"   if($verbose);
208    makeannotatedsignconf("var/opendnssec/signconf/" . $ZONE_NAME . ".xml",
209                          "var/opendnssec/signer/" . $ZONE_NAME . ".backup2",
210                          "var/opendnssec/sequences/" . $timecurrent . "-" . $ZONE_NAME . ".xml");
211    unlink("var/opendnssec/signed/" . $ZONE_NAME);
212    unlink("var/opendnssec/signer/" . $ZONE_NAME . ".backup2");
213    copy("var/opendnssec/kasp.db",
214         "var/opendnssec/sequences/" . $timecurrent . "-kasp.db");
215    endmonitorlog();
216}
217
218sub makesequence {
219    my $currenttime = $_[0];
220    print "  ..generating sequence\n"   if($verbose);
221    system("./sbin/ods-enforcer 2>>$LOG_FILE >>$LOG_FILE signconf");
222    enforceridle();
223    system("./sbin/ods-enforcer 2>>$LOG_FILE >>$LOG_FILE stop");
224    makesignedzone($currenttime);
225    system("./sbin/ods-enforcerd 2>>$LOG_FILE >>$LOG_FILE --set-time " . $currenttime);
226}
227
228##############################################################################
229
230$LOG_FILE=abs_path($LOG_FILE);
231chdir $INSTALL_ROOT;
232die "Sequencing directory not set up or misconfigured.\n" unless -d "var/opendnssec/sequences";
233
234my $sequence;
235my @sequences;
236
237opendir DIR, "var/opendnssec/sequences" or die;
238@sequences = readdir(DIR) or die;
239closedir(DIR);
240@sequences = grep { s/^(\d+)-$ZONE_NAME.xml$/$1/ } @sequences;
241@sequences = sort { $a <=> $b } @sequences;
242
243my $targettime;
244my $currenttime;
245my $timenow;
246my $timenext;
247my @dsseenkeys;
248my $key;
249my @dssubmitfiles;
250my $dssubmitfile;
251
252if($#ARGV == 0 && $ARGV[0] eq "update") {
253    $currenttime = time();
254    undef $timenow;
255    undef $targettime;
256    PLAY: foreach $timenext (@sequences) {
257        if($timenext > $currenttime) {
258            $targettime = $timenext;
259            last PLAY;
260        }
261        $timenow = $timenext;
262        print "Updated configuration to " . (strftime('%Y-%m-%d-%H:%M:%S',localtime($timenow))) . "\n";
263        if(-f "var/opendnssec/sequences/" . $timenow . "-kasp.db", "var/opendnssec/kasp.db") {
264            copy("var/opendnssec/sequences/" . $timenow . "-kasp.db", "var/opendnssec/kasp.db") or die "Unable to update enforcer configuration: $!\n";
265            unlink("var/opendnssec/sequences/" . $timenow . "-kasp.db", "var/opendnssec/kasp.db");
266        }
267        copy("var/opendnssec/sequences/" . $timenow . "-" . $ZONE_NAME . ".xml",
268             "var/opendnssec/signconf/" . $ZONE_NAME . ".xml") or die "Unable to update signer configuration\n";
269        unlink("var/opendnssec/sequences/" . $timenow . "-" . $ZONE_NAME . ".xml");
270    }
271    if(defined($timenow)) {
272        print "Notifying signer\n";
273        system("./sbin/ods-signer update --all");
274    }
275    if(defined($targettime)) {
276        print "Next configuration update due " . (strftime('%Y-%m-%d-%H:%M:%S',localtime($targettime))) . ".\n"  if($verbose);
277    } else {
278        if(defined($timenow)) {
279            print "Last signer configuration update performed.\n";
280        } else {
281            die "No more signer configuration updates.\n";
282        }
283    }
284} elsif($#ARGV == 1 && $ARGV[0] eq "scenario") {
285    $targettime = str2time($ARGV[1]);
286    die "Unrecognized target time"  if(!defined $targettime);
287    if($#sequences < 0) {
288        $currenttime = time();
289        print "generating sequences from scratch starting now at " . localtime($currenttime) . "\n";
290        system("./sbin/ods-enforcerd 2>>$LOG_FILE >>$LOG_FILE --set-time " . $currenttime);
291        print "  ..waiting for information and enforcer idle\n"   if($verbose);
292        sleep(10);
293        ($timenow, $timenext, @dsseenkeys) = enforcerinfo();
294        makesequence($currenttime);
295    } else {
296        $currenttime = $sequences[$#sequences];
297        print "generating sequences picking up from " . localtime($currenttime) . "\n";
298        copy("var/opendnssec/sequences/" . $currenttime . "-kasp.db", "var/opendnssec/kasp.db");
299        system("./sbin/ods-enforcerd 2>>$LOG_FILE >>$LOG_FILE --set-time " . $currenttime);
300        sleep(10);
301        ($timenow, $timenext, @dsseenkeys) = enforcerinfo();
302    }
303    while($currenttime <= $targettime) {
304        print "determining what to do on " . (strftime('%Y-%m-%d-%H:%M:%S',localtime($currenttime))) . "\n"   if($verbose);
305        if($#dsseenkeys < 0) {
306            if($timenext <= $targettime) {
307                print "  leaping to " . (strftime('%Y-%m-%d-%H:%M:%S',localtime($timenext))) . "\n"   if($verbose);
308                system("./sbin/ods-enforcer 2>>$LOG_FILE >>$LOG_FILE time leap --time " . (strftime('%Y-%m-%d-%H:%M:%S',localtime($timenext))));
309                sleep(10);
310                enforceridle();
311                ($timenow, $timenext, @dsseenkeys) = enforcerinfo();
312                $currenttime = $timenow;
313                makesequence($currenttime);
314                print "signer configuration for " . (strftime('%Y-%m-%d-%H:%M:%S',localtime($currenttime))) . "\n";
315            } else {
316                $currenttime = $timenext;
317            }
318        } else {
319            print "  publishing ds keys " . $#dsseenkeys . "\t" . @dsseenkeys . "\n"   if($verbose);
320            foreach $key (@dsseenkeys) {
321                print "  ..publishing cka_id " . $key      if($verbose);
322                print "ds seen given for " . $key . "\n"   if(!$verbose);
323                system("./sbin/ods-enforcer 2>>$LOG_FILE >>$LOG_FILE key ds-seen --zone " . $ZONE_NAME . " --cka_id " . $key);
324            }
325            ($timenow, $timenext, @dsseenkeys) = enforcerinfo();
326            $currenttime = $timenow;
327            makesequence($currenttime);
328        }
329    }
330    system("./sbin/ods-enforcer 2>>$LOG_FILE >>$LOG_FILE stop");
331
332    # As a reminder to the operator, at the end write out the DS records
333    # that need to be explicitly submitted to the parent zone
334    opendir DIR, "var/opendnssec/sequences" or die;
335    @sequences = readdir(DIR) or die;
336    closedir(DIR);
337    @sequences = grep { s/^(\d+)-dssubmit$/$1/ } @sequences;
338    @sequences = sort { $a <=> $b } @sequences;
339    foreach $sequence (@sequences) {
340      print "On " . strftime('%Y-%m-%d-%H:%M:%S',localtime($sequence)) . " submit " . $sequence . "-dssubmit\n";
341    }
342} else {
343    print "\n";
344    print "Usage: ods-sequencer update\n";
345    print "       Intended to be run periodically, updates an environment without\n";
346    print "       enforcer to a prerecorded enforcer scenario\n";
347    print "Or:    ods-sequencer scenario <runtime>\n";
348    print "       Record a sequence of signer configurations starting from the\n";
349    print "       latest recorded state towards the given runtime parameter\n";
350    print "\n";
351    die "Unrecognized usage.\n";
352}
353
354##############################################################################
355