]> CyberLeo.Net >> Repos - FreeBSD/releng/10.3.git/blob - contrib/ntp/scripts/update-leap/update-leap.in
Fix multiple vulnerabilities in ntp. [SA-18:02.ntp]
[FreeBSD/releng/10.3.git] / contrib / ntp / scripts / update-leap / update-leap.in
1 #! @PATH_PERL@ -w
2
3 # Copyright (C) 2015, 2017 Network Time Foundation
4 # Author: Harlan Stenn
5 #
6 # General cleanup and https support: Paul McMath
7 #
8 # Original shell version:
9 # Copyright (C) 2014 Timothe Litt litt at acm dot org
10 #
11 # This script may be freely copied, used and modified providing that
12 # this notice and the copyright statement are included in all copies
13 # and derivative works.  No warranty is offered, and use is entirely at
14 # your own risk.  Bugfixes and improvements would be appreciated by the
15 # author.
16
17 ######## BEGIN #########
18 use strict;
19
20 # Core modules
21 use Digest::SHA qw(sha1_hex);
22 use File::Basename;
23 use File::Copy qw(move);
24 use File::Temp qw(tempfile);
25 use Getopt::Long qw(:config auto_help no_ignore_case bundling);
26 use Sys::Syslog qw(:standard :macros);
27
28 # External modules
29 use HTTP::Tiny 0.056;
30 use Net::SSLeay 1.49;
31 use IO::Socket::SSL 1.56;
32
33 my $VERSION = '1.004';
34
35 my $RUN_DIR = '/tmp';
36 my $RUN_UID = 0;
37 my $TMP_FILE;
38 my $TMP_FH;
39 my $FILE_MODE = 0644;
40
41 ######## DEFAULT CONFIGURATION ##########
42 # LEAP FILE SRC URIS
43 #    HTTPS - (default)
44 #       https://www.ietf.org/timezones/data/leap-seconds
45 #    HTTP - No TLS/SSL - (not recommended)
46 #       http://www.ietf.org/timezones/data/leap-seconds.list
47
48 my $LEAPSRC = 'https://www.ietf.org/timezones/data/leap-seconds.list';
49 my $LEAPFILE;
50
51 # How many times to try to download new file
52 my $MAXTRIES = 6;
53 my $INTERVAL = 10;
54
55 my $NTPCONF='/etc/ntp.conf';
56
57 # How long (in days) before expiration to get updated file
58 my $PREFETCH = 60;
59 my $EXPIRES;
60 my $FORCE;
61
62 # Output Flags
63 my $QUIET;
64 my $DEBUG;
65 my $SYSLOG;
66 my $TOTERM;
67 my $LOGFAC = 'LOG_USER';
68
69 ######### PARSE/SET OPTIONS #########
70 my %SSL_OPTS;
71 my %SSL_ATTRS = (
72     verify_SSL => 1,  
73     SSL_options => \%SSL_OPTS,
74 );
75
76 our(%opt);
77
78 GetOptions(\%opt,       
79         'C=s',
80         'D=s',
81         'e:60',
82         'F',
83         'f=s',
84         'h|help',
85         'i:10',
86         'L=s',
87         'l=s',
88         'q',
89         'r:6',
90         's',
91         't',
92         'u=s',
93         'v',
94         );
95
96 $LOGFAC   = $opt{l} if defined $opt{l};
97 $LEAPSRC  = $opt{u} if defined $opt{u};
98 $LEAPFILE = $opt{L} if defined $opt{L};
99 $PREFETCH = $opt{e} if defined $opt{e};
100 $NTPCONF  = $opt{f} if defined $opt{f};
101 $MAXTRIES = $opt{r} if defined $opt{r};
102 $INTERVAL = $opt{i} if defined $opt{i};
103
104 $FORCE   = 1 if defined $opt{F};
105 $DEBUG   = 1 if defined $opt{v};
106 $QUIET   = 1 if defined $opt{q};
107 $SYSLOG  = 1 if defined $opt{s};
108 $TOTERM  = 1 if defined $opt{t};
109
110 $SSL_OPTS{SSL_ca_file} = $opt{C} if (defined($opt{C}));
111 $SSL_OPTS{SSL_ca_path} = $opt{D} if (defined($opt{D}));
112
113 ###############
114 ## START MAIN
115 ###############
116 my $PROG = basename($0);
117
118 # Logging - Default is to use syslog(3) if STDOUT isn't 
119 # connected to a tty.
120 if ($SYSLOG || !-t STDOUT) {
121     $SYSLOG = 1;
122     openlog($PROG, 'pid', $LOGFAC);
123
124 else {
125     $TOTERM = 1;
126 }
127
128 if (defined $opt{q} && defined $opt{v}) {
129     log_fatal(LOG_ERR, '-q and -v options mutually exclusive');
130 }
131
132 if (defined $opt{L} && defined $opt{f}) {
133     log_fatal(LOG_ERR, '-L and -f options mutually exclusive');
134 }
135
136 $SIG{INT} = \&signal_catcher;
137 $SIG{TERM} = \&signal_catcher;
138 $SIG{QUIT} = \&signal_catcher;
139
140 # Take some security precautions
141 close STDIN;
142
143 # Show help
144 if (defined $opt{h}) {
145     show_help();
146     exit 0;
147 }
148
149 if ($< != $RUN_UID) {
150     log_fatal(LOG_ERR, 'User ' . getpwuid($<) . " (UID $<) tried to run $PROG");
151 }
152
153 chdir $RUN_DIR || log_fatal("Failed to change dir to $RUN_DIR");
154
155 # Parse ntp.conf for path to leapfile if not set by user
156 if (! $LEAPFILE) {
157
158     open my $LF, '<', $NTPCONF || log_fatal(LOG_ERR, "Can't open <$NTPCONF>: $!");
159
160     while (<$LF>) {
161         chomp;
162         $LEAPFILE = $1 if /^ *leapfile\s+"(\S+)"/;
163     }
164     close $LF;
165
166     if (! $LEAPFILE) {
167         log_fatal(LOG_ERR, "No leapfile directive in $NTPCONF; leapfile location not known"); 
168     }
169 }
170
171 -s $LEAPFILE || logger(LOG_DEBUG, "Leapfile $LEAPFILE is empty");
172
173 # Download new file if:
174 #   1. file doesn't exist
175 #   2. invoked w/ force flag (-F)
176 #   3. current file isn't valid
177 #   4. current file expired or expires soon
178
179 if ( !-e $LEAPFILE || $FORCE || ! verifySHA($LEAPFILE) || 
180         ( $EXPIRES lt ( $PREFETCH * 86400 + time() ) )) {
181
182     for (my $try = 1; $try <= $MAXTRIES; $try++) {
183         logger(LOG_DEBUG, "Attempting download from $LEAPSRC, try $try..");
184
185         ($TMP_FH, $TMP_FILE) = tempfile(UNLINK => 1, SUFFIX => '.list');
186
187         if (retrieve_file($TMP_FH)) {
188
189             if ( verifySHA($TMP_FILE) ) {
190                 move_file($TMP_FILE, $LEAPFILE);
191                 chmod $FILE_MODE, $LEAPFILE; 
192                 logger(LOG_INFO, "Installed new $LEAPFILE from $LEAPSRC");
193             }
194             else {
195                 logger(LOG_ERR, "Downloaded file $TMP_FILE rejected -- saved for diagnosis");
196                 move_file($TMP_FILE, 'leap-seconds.list_corrupt');
197                 exit 1;
198             }
199             # Fall through
200             exit 0;
201         }
202
203         # Failure
204         unlink $TMP_FILE;
205         logger(LOG_INFO, "Download failed. Waiting $INTERVAL minutes before retrying...");
206         sleep $INTERVAL * 60 ;
207     }
208
209     # Failed and out of retries
210     log_fatal(LOG_ERR, "Download from $LEAPSRC failed after $MAXTRIES attempts");
211 }
212
213 logger(LOG_INFO, "Not time to replace $LEAPFILE");
214
215 exit 0;
216
217 ######## SUB ROUTINES #########
218 sub move_file {
219
220     (my $src, my $dst) = @_;
221
222     if ( move($src, $dst) ) {
223         logger(LOG_DEBUG, "Moved $src to $dst");
224     } 
225     else {
226         log_fatal(LOG_ERR, "Moving $src to $dst failed: $!");
227     }
228 }
229
230 # Removes temp file if terminating signal recv'd
231 sub signal_catcher {
232     my $signame = shift;
233
234     close $TMP_FH;
235     unlink $TMP_FILE;
236     log_fatal(LOG_INFO, "Recv'd SIG${signame}. Terminating.");
237 }           
238
239 sub log_fatal {
240     my ($p, $msg) = @_;
241     logger($p, $msg);
242     exit 1;
243 }
244
245 sub logger {
246     my ($p, $msg) = @_;
247
248     # Suppress LOG_DEBUG msgs unless $DEBUG set
249     return if (!$DEBUG && $p eq LOG_DEBUG);
250
251     # Suppress all but LOG_ERR msgs if $QUIET set
252     return if ($QUIET && $p ne LOG_ERR);
253
254     if ($TOTERM) {
255         if ($p eq LOG_ERR) {    # errors should go to STDERR
256             print STDERR "$msg\n";
257         }
258         else {
259             print STDOUT "$msg\n";
260         }
261     }
262
263     if ($SYSLOG) {
264         syslog($p, $msg)
265     }
266 }
267
268 #################################
269 # Connect to server and retrieve file
270 #
271 # Since we make as many as $MAXTRIES attempts to connect to the remote
272 # server to download the file, the network socket should be closed after
273 # each attempt, rather than let it be reused (because it may be in some
274 # unknown state).
275 #
276 # HTTP::Tiny doesn't export a method to explicitly close a connected
277 # socket, therefore, we instantiate the lexically scoped $http object in
278 # a function; when the function returns, the object goes out of scope
279 # and is destroyed, closing the socket.
280 sub retrieve_file {
281
282     my $fh = shift;
283     my $http;
284
285     if ($LEAPSRC =~ /^https\S+/) {
286         $http = HTTP::Tiny->new(%SSL_ATTRS);
287         (my $ok, my $why) = $http->can_ssl;
288         log_fatal(LOG_ERR, "TLS/SSL config error: $why") if ! $ok;
289     } 
290     else {
291         $http = HTTP::Tiny->new();
292     }
293
294     my $reply = $http->get($LEAPSRC);
295
296     if ($reply->{success}) {
297         logger(LOG_DEBUG, "Download of $LEAPSRC succeeded");
298         print $fh $reply->{content} || 
299             log_fatal(LOG_ERR, "Couldn't write new file contents to temp file: $!");
300         close $fh;
301         return 1;
302     } 
303     else {
304         close $fh;
305         return 0;
306     }
307 }
308
309 ########################
310 # Validate a leap-seconds file checksum
311 #
312 # File format: (full description in file)
313 # Pound sign (#) marks comments, EXCEPT:
314 #       #$ number : the NTP date of the last update
315 #       #@ number : the NTP date that the file expires
316 #       #h hex hex hex hex hex : the SHA-1 checksum of the data & dates, 
317 #          excluding whitespace w/o leading zeroes
318 #
319 # Date (seconds since 1900) leaps : leaps is the # of seconds to add
320 #  for times >= Date 
321 # Date lines have comments.
322 #
323 # Returns:
324 #   0   Invalid Checksum/Expired
325 #   1   File is valid
326
327 sub verifySHA {
328
329     my $file = shift;
330     my $fh;
331     my $data;
332     my $FSHA;
333
334     open $fh, '<', $file || log_fatal(LOG_ERR, "Can't open $file: $!");
335
336     # Remove comments, except those that are markers for last update,
337     # expires and hash
338     while (<$fh>) {
339         if (/^#\$/) {
340             s/^..//;
341             $data .= $_;
342         }
343         elsif (/^#\@/) {
344             s/^..//;
345             $data .= $_;
346             s/\s+//g;
347             $EXPIRES = $_ - 2208988800;
348         }
349         elsif (/^#h\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+)/) {
350             chomp;
351             $FSHA = sprintf("%08s%08s%08s%08s%08s", $1, $2, $3, $4, $5);
352         }
353         elsif (/^#/) {
354             # ignore it
355         }
356         elsif (/^\d/) {
357             s/#.*$//;
358             $data .= $_;
359         } 
360         else {
361             chomp;
362             print "Unexpected line: <$_>\n";
363         }
364     }
365     close $fh;
366
367     if ( $EXPIRES < time() ) {
368         logger(LOG_DEBUG, 'File expired on ' . gmtime($EXPIRES));
369         return 0;
370     }
371
372     if (! $FSHA) {
373         logger(LOG_NOTICE, "no checksum record found in file");
374         return 0;
375     }
376
377     # Remove all white space
378     $data =~ s/\s//g;
379
380     # Compute the SHA hash of the data, removing the marker and filename
381     # Computed in binary mode, which shouldn't matter since whitespace has been removed
382     my $DSHA = sha1_hex($data);
383
384     if ($FSHA eq $DSHA) {
385         logger(LOG_DEBUG, "Checksum of $file validated");
386         return 1;
387     } 
388     else {
389         logger(LOG_NOTICE, "Checksum of $file is invalid EXPECTED: $FSHA COMPUTED: $DSHA");
390         return 0;
391     }
392 }
393
394 sub show_help {
395 print <<EOF
396
397 Usage: $PROG [options]
398
399 Verifies and if necessary, updates leap-second definition file
400
401 All arguments are optional:  Default (or current value) shown:
402     -C    Absolute path to CA Cert (see SSL/TLS Considerations)
403     -D    Path to a CAdir (see SSL/TLS Considerations)
404     -e    Specify how long (in days) before expiration the file is to be
405           refreshed.  Note that larger values imply more frequent refreshes.
406           $PREFETCH
407     -F    Force update even if current file is OK and not close to expiring.
408     -f    Absolute path ntp.conf file (default /etc/ntp.conf)
409           $NTPCONF
410     -h    show help
411     -i    Specify number of minutes between retries
412           $INTERVAL
413     -L    Absolute path to leapfile on the local system
414           (overrides value in ntp.conf)
415     -l    Specify the syslog(3) facility for logging
416           $LOGFAC
417     -q    Only report errors (cannot be used with -v)
418     -r    Specify number of attempts to retrieve file
419           $MAXTRIES
420     -s    Send output to syslog(3) - implied if STDOUT has no tty or redirected
421     -t    Send output to terminal - implied if STDOUT attached to terminal
422     -u    Specify the URL of the master copy to download
423           $LEAPSRC
424     -v    Verbose - show debug messages (cannot be used with -q)
425
426 The following options are not (yet) implemented in the perl version:
427     -4    Use only IPv4
428     -6    Use only IPv6
429     -c    Command to restart NTP after installing a new file
430           <none> - ntpd checks file daily
431     -p 4|6
432           Prefer IPv4 or IPv6 (as specified) addresses, but use either
433
434 $PROG will validate the file currently on the local system.
435
436 Ordinarily, the leapfile is found using the 'leapfile' directive in
437 $NTPCONF.  However, an alternate location can be specified on the
438 command line with the -L flag.
439
440 If the leapfile does not exist, is not valid, has expired, or is
441 expiring soon, a new copy will be downloaded.  If the new copy is
442 valid, it is installed.
443
444 If the current file is acceptable, no download or restart occurs.
445
446 This can be run as a cron job.  As the file is rarely updated, and
447 leap seconds are announced at least one month in advance (usually
448 longer), it need not be run more frequently than about once every
449 three weeks.
450
451 SSL/TLS Considerations
452 -----------------------
453 The perl modules can usually locate the CA certificate used to verify
454 the peer's identity.
455
456 On BSDs, the default is typically the file /etc/ssl/certs.pem.  On
457 Linux, the location is typically a path to a CAdir - a directory of
458 symlinks named according to a hash of the certificates' subject names.
459
460 The -C or -D options are available to pass in a location if no CA cert
461 is found in the default location.
462
463 External Dependencies
464 ---------------------
465 The following perl modules are required:
466 HTTP::Tiny      - version >= 0.056
467 IO::Socket::SSL - version >= 1.56
468 NET::SSLeay     - version >= 1.49
469
470 Version: $VERSION
471
472 EOF
473 }
474