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