Imported Upstream version 7.59.0
[platform/upstream/curl.git] / lib / mk-ca-bundle.pl
1 #!/usr/bin/env perl
2 # ***************************************************************************
3 # *                                  _   _ ____  _
4 # *  Project                     ___| | | |  _ \| |
5 # *                             / __| | | | |_) | |
6 # *                            | (__| |_| |  _ <| |___
7 # *                             \___|\___/|_| \_\_____|
8 # *
9 # * Copyright (C) 1998 - 2016, Daniel Stenberg, <daniel@haxx.se>, et al.
10 # *
11 # * This software is licensed as described in the file COPYING, which
12 # * you should have received as part of this distribution. The terms
13 # * are also available at https://curl.haxx.se/docs/copyright.html.
14 # *
15 # * You may opt to use, copy, modify, merge, publish, distribute and/or sell
16 # * copies of the Software, and permit persons to whom the Software is
17 # * furnished to do so, under the terms of the COPYING file.
18 # *
19 # * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20 # * KIND, either express or implied.
21 # *
22 # ***************************************************************************
23 # This Perl script creates a fresh ca-bundle.crt file for use with libcurl.
24 # It downloads certdata.txt from Mozilla's source tree (see URL below),
25 # then parses certdata.txt and extracts CA Root Certificates into PEM format.
26 # These are then processed with the OpenSSL commandline tool to produce the
27 # final ca-bundle.crt file.
28 # The script is based on the parse-certs script written by Roland Krikava.
29 # This Perl script works on almost any platform since its only external
30 # dependency is the OpenSSL commandline tool for optional text listing.
31 # Hacked by Guenter Knauf.
32 #
33 use Encode;
34 use Getopt::Std;
35 use MIME::Base64;
36 use strict;
37 use warnings;
38 use vars qw($opt_b $opt_d $opt_f $opt_h $opt_i $opt_k $opt_l $opt_m $opt_n $opt_p $opt_q $opt_s $opt_t $opt_u $opt_v $opt_w);
39 use List::Util;
40 use Text::Wrap;
41 my $MOD_SHA = "Digest::SHA";
42 eval "require $MOD_SHA";
43 if ($@) {
44   $MOD_SHA = "Digest::SHA::PurePerl";
45   eval "require $MOD_SHA";
46 }
47 eval "require LWP::UserAgent";
48
49 my %urls = (
50   'nss' =>
51     'https://hg.mozilla.org/projects/nss/raw-file/default/lib/ckfw/builtins/certdata.txt',
52   'central' =>
53     'https://hg.mozilla.org/mozilla-central/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt',
54   'beta' =>
55     'https://hg.mozilla.org/releases/mozilla-beta/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt',
56   'release' =>
57     'https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt',
58 );
59
60 $opt_d = 'release';
61
62 # If the OpenSSL commandline is not in search path you can configure it here!
63 my $openssl = 'openssl';
64
65 my $version = '1.27';
66
67 $opt_w = 76; # default base64 encoded lines length
68
69 # default cert types to include in the output (default is to include CAs which may issue SSL server certs)
70 my $default_mozilla_trust_purposes = "SERVER_AUTH";
71 my $default_mozilla_trust_levels = "TRUSTED_DELEGATOR";
72 $opt_p = $default_mozilla_trust_purposes . ":" . $default_mozilla_trust_levels;
73
74 my @valid_mozilla_trust_purposes = (
75   "DIGITAL_SIGNATURE",
76   "NON_REPUDIATION",
77   "KEY_ENCIPHERMENT",
78   "DATA_ENCIPHERMENT",
79   "KEY_AGREEMENT",
80   "KEY_CERT_SIGN",
81   "CRL_SIGN",
82   "SERVER_AUTH",
83   "CLIENT_AUTH",
84   "CODE_SIGNING",
85   "EMAIL_PROTECTION",
86   "IPSEC_END_SYSTEM",
87   "IPSEC_TUNNEL",
88   "IPSEC_USER",
89   "TIME_STAMPING",
90   "STEP_UP_APPROVED"
91 );
92
93 my @valid_mozilla_trust_levels = (
94   "TRUSTED_DELEGATOR",    # CAs
95   "NOT_TRUSTED",          # Don't trust these certs.
96   "MUST_VERIFY_TRUST",    # This explicitly tells us that it ISN'T a CA but is otherwise ok. In other words, this should tell the app to ignore any other sources that claim this is a CA.
97   "TRUSTED"               # This cert is trusted, but only for itself and not for delegates (i.e. it is not a CA).
98 );
99
100 my $default_signature_algorithms = $opt_s = "MD5";
101
102 my @valid_signature_algorithms = (
103   "MD5",
104   "SHA1",
105   "SHA256",
106   "SHA384",
107   "SHA512"
108 );
109
110 $0 =~ s@.*(/|\\)@@;
111 $Getopt::Std::STANDARD_HELP_VERSION = 1;
112 getopts('bd:fhiklmnp:qs:tuvw:');
113
114 if(!defined($opt_d)) {
115     # to make plain "-d" use not cause warnings, and actually still work
116     $opt_d = 'release';
117 }
118
119 # Use predefined URL or else custom URL specified on command line.
120 my $url;
121 if(defined($urls{$opt_d})) {
122   $url = $urls{$opt_d};
123   if(!$opt_k && $url !~ /^https:\/\//i) {
124     die "The URL for '$opt_d' is not HTTPS. Use -k to override (insecure).\n";
125   }
126 }
127 else {
128   $url = $opt_d;
129 }
130
131 my $curl = `curl -V`;
132
133 if ($opt_i) {
134   print ("=" x 78 . "\n");
135   print "Script Version                   : $version\n";
136   print "Perl Version                     : $]\n";
137   print "Operating System Name            : $^O\n";
138   print "Getopt::Std.pm Version           : ${Getopt::Std::VERSION}\n";
139   print "MIME::Base64.pm Version          : ${MIME::Base64::VERSION}\n";
140   print "LWP::UserAgent.pm Version        : ${LWP::UserAgent::VERSION}\n" if($LWP::UserAgent::VERSION);
141   print "LWP.pm Version                   : ${LWP::VERSION}\n" if($LWP::VERSION);
142   print "Digest::SHA.pm Version           : ${Digest::SHA::VERSION}\n" if ($Digest::SHA::VERSION);
143   print "Digest::SHA::PurePerl.pm Version : ${Digest::SHA::PurePerl::VERSION}\n" if ($Digest::SHA::PurePerl::VERSION);
144   print ("=" x 78 . "\n");
145 }
146
147 sub warning_message() {
148   if ( $opt_d =~ m/^risk$/i ) { # Long Form Warning and Exit
149     print "Warning: Use of this script may pose some risk:\n";
150     print "\n";
151     print "  1) If you use HTTP URLs they are subject to a man in the middle attack\n";
152     print "  2) Default to 'release', but more recent updates may be found in other trees\n";
153     print "  3) certdata.txt file format may change, lag time to update this script\n";
154     print "  4) Generally unwise to blindly trust CAs without manual review & verification\n";
155     print "  5) Mozilla apps use additional security checks aren't represented in certdata\n";
156     print "  6) Use of this script will make a security engineer grind his teeth and\n";
157     print "     swear at you.  ;)\n";
158     exit;
159   } else { # Short Form Warning
160     print "Warning: Use of this script may pose some risk, -d risk for more details.\n";
161   }
162 }
163
164 sub HELP_MESSAGE() {
165   print "Usage:\t${0} [-b] [-d<certdata>] [-f] [-i] [-k] [-l] [-n] [-p<purposes:levels>] [-q] [-s<algorithms>] [-t] [-u] [-v] [-w<l>] [<outputfile>]\n";
166   print "\t-b\tbackup an existing version of ca-bundle.crt\n";
167   print "\t-d\tspecify Mozilla tree to pull certdata.txt or custom URL\n";
168   print "\t\t  Valid names are:\n";
169   print "\t\t    ", join( ", ", map { ( $_ =~ m/$opt_d/ ) ? "$_ (default)" : "$_" } sort keys %urls ), "\n";
170   print "\t-f\tforce rebuild even if certdata.txt is current\n";
171   print "\t-i\tprint version info about used modules\n";
172   print "\t-k\tallow URLs other than HTTPS, enable HTTP fallback (insecure)\n";
173   print "\t-l\tprint license info about certdata.txt\n";
174   print "\t-m\tinclude meta data in output\n";
175   print "\t-n\tno download of certdata.txt (to use existing)\n";
176   print wrap("\t","\t\t", "-p\tlist of Mozilla trust purposes and levels for certificates to include in output. Takes the form of a comma separated list of purposes, a colon, and a comma separated list of levels. (default: $default_mozilla_trust_purposes:$default_mozilla_trust_levels)"), "\n";
177   print "\t\t  Valid purposes are:\n";
178   print wrap("\t\t    ","\t\t    ", join( ", ", "ALL", @valid_mozilla_trust_purposes ) ), "\n";
179   print "\t\t  Valid levels are:\n";
180   print wrap("\t\t    ","\t\t    ", join( ", ", "ALL", @valid_mozilla_trust_levels ) ), "\n";
181   print "\t-q\tbe really quiet (no progress output at all)\n";
182   print wrap("\t","\t\t", "-s\tcomma separated list of certificate signatures/hashes to output in plain text mode. (default: $default_signature_algorithms)\n");
183   print "\t\t  Valid signature algorithms are:\n";
184   print wrap("\t\t    ","\t\t    ", join( ", ", "ALL", @valid_signature_algorithms ) ), "\n";
185   print "\t-t\tinclude plain text listing of certificates\n";
186   print "\t-u\tunlink (remove) certdata.txt after processing\n";
187   print "\t-v\tbe verbose and print out processed CAs\n";
188   print "\t-w <l>\twrap base64 output lines after <l> chars (default: ${opt_w})\n";
189   exit;
190 }
191
192 sub VERSION_MESSAGE() {
193   print "${0} version ${version} running Perl ${]} on ${^O}\n";
194 }
195
196 warning_message() unless ($opt_q || $url =~ m/^(ht|f)tps:/i );
197 HELP_MESSAGE() if ($opt_h);
198
199 sub report($@) {
200   my $output = shift;
201
202   print STDERR $output . "\n" unless $opt_q;
203 }
204
205 sub is_in_list($@) {
206   my $target = shift;
207
208   return defined(List::Util::first { $target eq $_ } @_);
209 }
210
211 # Parses $param_string as a case insensitive comma separated list with optional whitespace
212 # validates that only allowed parameters are supplied
213 sub parse_csv_param($$@) {
214   my $description = shift;
215   my $param_string = shift;
216   my @valid_values = @_;
217
218   my @values = map {
219     s/^\s+//;  # strip leading spaces
220     s/\s+$//;  # strip trailing spaces
221     uc $_      # return the modified string as upper case
222   } split( ',', $param_string );
223
224   # Find all values which are not in the list of valid values or "ALL"
225   my @invalid = grep { !is_in_list($_,"ALL",@valid_values) } @values;
226
227   if ( scalar(@invalid) > 0 ) {
228     # Tell the user which parameters were invalid and print the standard help message which will exit
229     print "Error: Invalid ", $description, scalar(@invalid) == 1 ? ": " : "s: ", join( ", ", map { "\"$_\"" } @invalid ), "\n";
230     HELP_MESSAGE();
231   }
232
233   @values = @valid_values if ( is_in_list("ALL",@values) );
234
235   return @values;
236 }
237
238 sub sha256 {
239   my $result;
240   if ($Digest::SHA::VERSION || $Digest::SHA::PurePerl::VERSION) {
241     open(FILE, $_[0]) or die "Can't open '$_[0]': $!";
242     binmode(FILE);
243     $result = $MOD_SHA->new(256)->addfile(*FILE)->hexdigest;
244     close(FILE);
245   } else {
246     # Use OpenSSL command if Perl Digest::SHA modules not available
247     $result = `"$openssl" dgst -r -sha256 "$_[0]"`;
248     $result =~ s/^([0-9a-f]{64}) .+/$1/is;
249   }
250   return $result;
251 }
252
253
254 sub oldhash {
255   my $hash = "";
256   open(C, "<$_[0]") || return 0;
257   while(<C>) {
258     chomp;
259     if($_ =~ /^\#\# SHA256: (.*)/) {
260       $hash = $1;
261       last;
262     }
263   }
264   close(C);
265   return $hash;
266 }
267
268 if ( $opt_p !~ m/:/ ) {
269   print "Error: Mozilla trust identifier list must include both purposes and levels\n";
270   HELP_MESSAGE();
271 }
272
273 (my $included_mozilla_trust_purposes_string, my $included_mozilla_trust_levels_string) = split( ':', $opt_p );
274 my @included_mozilla_trust_purposes = parse_csv_param( "trust purpose", $included_mozilla_trust_purposes_string, @valid_mozilla_trust_purposes );
275 my @included_mozilla_trust_levels = parse_csv_param( "trust level", $included_mozilla_trust_levels_string, @valid_mozilla_trust_levels );
276
277 my @included_signature_algorithms = parse_csv_param( "signature algorithm", $opt_s, @valid_signature_algorithms );
278
279 sub should_output_cert(%) {
280   my %trust_purposes_by_level = @_;
281
282   foreach my $level (@included_mozilla_trust_levels) {
283     # for each level we want to output, see if any of our desired purposes are included
284     return 1 if ( defined( List::Util::first { is_in_list( $_, @included_mozilla_trust_purposes ) } @{$trust_purposes_by_level{$level}} ) );
285   }
286
287   return 0;
288 }
289
290 my $crt = $ARGV[0] || 'ca-bundle.crt';
291 (my $txt = $url) =~ s@(.*/|\?.*)@@g;
292
293 my $stdout = $crt eq '-';
294 my $resp;
295 my $fetched;
296
297 my $oldhash = oldhash($crt);
298
299 report "SHA256 of old file: $oldhash";
300
301 if(!$opt_n) {
302   report "Downloading $txt ...";
303
304   # If we have an HTTPS URL then use curl
305   if($url =~ /^https:\/\//i) {
306     if($curl) {
307       if($curl =~ /^Protocols:.* https( |$)/m) {
308         report "Get certdata with curl!";
309         my $proto = !$opt_k ? "--proto =https" : "";
310         my $quiet = $opt_q ? "-s" : "";
311         my @out = `curl -w %{response_code} $proto $quiet -o "$txt" "$url"`;
312         if(!$? && @out && $out[0] == 200) {
313           $fetched = 1;
314           report "Downloaded $txt";
315         }
316         else {
317           report "Failed downloading via HTTPS with curl";
318           if(-e $txt && !unlink($txt)) {
319             report "Failed to remove '$txt': $!";
320           }
321         }
322       }
323       else {
324         report "curl lacks https support";
325       }
326     }
327     else {
328       report "curl not found";
329     }
330   }
331
332   # If nothing was fetched then use LWP
333   if(!$fetched) {
334     if($url =~ /^https:\/\//i) {
335       report "Falling back to HTTP";
336       $url =~ s/^https:\/\//http:\/\//i;
337     }
338     if(!$opt_k) {
339       report "URLs other than HTTPS are disabled by default, to enable use -k";
340       exit 1;
341     }
342     report "Get certdata with LWP!";
343     if(!defined(${LWP::UserAgent::VERSION})) {
344       report "LWP is not available (LWP::UserAgent not found)";
345       exit 1;
346     }
347     my $ua  = new LWP::UserAgent(agent => "$0/$version");
348     $ua->env_proxy();
349     $resp = $ua->mirror($url, $txt);
350     if($resp && $resp->code eq '304') {
351       report "Not modified";
352       exit 0 if -e $crt && !$opt_f;
353     }
354     else {
355       $fetched = 1;
356       report "Downloaded $txt";
357     }
358     if(!$resp || $resp->code !~ /^(?:200|304)$/) {
359       report "Unable to download latest data: "
360         . ($resp? $resp->code . ' - ' . $resp->message : "LWP failed");
361       exit 1 if -e $crt || ! -r $txt;
362     }
363   }
364 }
365
366 my $filedate = $resp ? $resp->last_modified : (stat($txt))[9];
367 my $datesrc = "as of";
368 if(!$filedate) {
369     # mxr.mozilla.org gave us a time, hg.mozilla.org does not!
370     $filedate = time();
371     $datesrc="downloaded on";
372 }
373
374 # get the hash from the download file
375 my $newhash= sha256($txt);
376
377 if(!$opt_f && $oldhash eq $newhash) {
378     report "Downloaded file identical to previous run\'s source file. Exiting";
379     exit;
380 }
381
382 report "SHA256 of new file: $newhash";
383
384 my $currentdate = scalar gmtime($filedate);
385
386 my $format = $opt_t ? "plain text and " : "";
387 if( $stdout ) {
388     open(CRT, '> -') or die "Couldn't open STDOUT: $!\n";
389 } else {
390     open(CRT,">$crt.~") or die "Couldn't open $crt.~: $!\n";
391 }
392 print CRT <<EOT;
393 ##
394 ## Bundle of CA Root Certificates
395 ##
396 ## Certificate data from Mozilla ${datesrc}: ${currentdate} GMT
397 ##
398 ## This is a bundle of X.509 certificates of public Certificate Authorities
399 ## (CA). These were automatically extracted from Mozilla's root certificates
400 ## file (certdata.txt).  This file can be found in the mozilla source tree:
401 ## ${url}
402 ##
403 ## It contains the certificates in ${format}PEM format and therefore
404 ## can be directly used with curl / libcurl / php_curl, or with
405 ## an Apache+mod_ssl webserver for SSL client authentication.
406 ## Just configure this file as the SSLCACertificateFile.
407 ##
408 ## Conversion done with mk-ca-bundle.pl version $version.
409 ## SHA256: $newhash
410 ##
411
412 EOT
413
414 report "Processing  '$txt' ...";
415 my $caname;
416 my $certnum = 0;
417 my $skipnum = 0;
418 my $start_of_cert = 0;
419 my @precert;
420
421 open(TXT,"$txt") or die "Couldn't open $txt: $!\n";
422 while (<TXT>) {
423   if (/\*\*\*\*\* BEGIN LICENSE BLOCK \*\*\*\*\*/) {
424     print CRT;
425     print if ($opt_l);
426     while (<TXT>) {
427       print CRT;
428       print if ($opt_l);
429       last if (/\*\*\*\*\* END LICENSE BLOCK \*\*\*\*\*/);
430     }
431   }
432   elsif(/^# (Issuer|Serial Number|Subject|Not Valid Before|Not Valid After |Fingerprint \(MD5\)|Fingerprint \(SHA1\)):/) {
433       push @precert, $_;
434       next;
435   }
436   elsif(/^#|^\s*$/) {
437       undef @precert;
438       next;
439   }
440   chomp;
441
442   # this is a match for the start of a certificate
443   if (/^CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE/) {
444     $start_of_cert = 1
445   }
446   if ($start_of_cert && /^CKA_LABEL UTF8 \"(.*)\"/) {
447     $caname = $1;
448   }
449   my %trust_purposes_by_level;
450   if ($start_of_cert && /^CKA_VALUE MULTILINE_OCTAL/) {
451     my $data;
452     while (<TXT>) {
453       last if (/^END/);
454       chomp;
455       my @octets = split(/\\/);
456       shift @octets;
457       for (@octets) {
458         $data .= chr(oct);
459       }
460     }
461     # scan forwards until the trust part
462     while (<TXT>) {
463       last if (/^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST/);
464       chomp;
465     }
466     # now scan the trust part to determine how we should trust this cert
467     while (<TXT>) {
468       last if (/^#/);
469       if (/^CKA_TRUST_([A-Z_]+)\s+CK_TRUST\s+CKT_NSS_([A-Z_]+)\s*$/) {
470         if ( !is_in_list($1,@valid_mozilla_trust_purposes) ) {
471           report "Warning: Unrecognized trust purpose for cert: $caname. Trust purpose: $1. Trust Level: $2";
472         } elsif ( !is_in_list($2,@valid_mozilla_trust_levels) ) {
473           report "Warning: Unrecognized trust level for cert: $caname. Trust purpose: $1. Trust Level: $2";
474         } else {
475           push @{$trust_purposes_by_level{$2}}, $1;
476         }
477       }
478     }
479
480     if ( !should_output_cert(%trust_purposes_by_level) ) {
481       $skipnum ++;
482     } else {
483       my $encoded = MIME::Base64::encode_base64($data, '');
484       $encoded =~ s/(.{1,${opt_w}})/$1\n/g;
485       my $pem = "-----BEGIN CERTIFICATE-----\n"
486               . $encoded
487               . "-----END CERTIFICATE-----\n";
488       print CRT "\n$caname\n";
489       print CRT @precert if($opt_m);
490       my $maxStringLength = length(decode('UTF-8', $caname, Encode::FB_CROAK));
491       if ($opt_t) {
492         foreach my $key (keys %trust_purposes_by_level) {
493            my $string = $key . ": " . join(", ", @{$trust_purposes_by_level{$key}});
494            $maxStringLength = List::Util::max( length($string), $maxStringLength );
495            print CRT $string . "\n";
496         }
497       }
498       print CRT ("=" x $maxStringLength . "\n");
499       if (!$opt_t) {
500         print CRT $pem;
501       } else {
502         my $pipe = "";
503         foreach my $hash (@included_signature_algorithms) {
504           $pipe = "|$openssl x509 -" . $hash . " -fingerprint -noout -inform PEM";
505           if (!$stdout) {
506             $pipe .= " >> $crt.~";
507             close(CRT) or die "Couldn't close $crt.~: $!";
508           }
509           open(TMP, $pipe) or die "Couldn't open openssl pipe: $!";
510           print TMP $pem;
511           close(TMP) or die "Couldn't close openssl pipe: $!";
512           if (!$stdout) {
513             open(CRT, ">>$crt.~") or die "Couldn't open $crt.~: $!";
514           }
515         }
516         $pipe = "|$openssl x509 -text -inform PEM";
517         if (!$stdout) {
518           $pipe .= " >> $crt.~";
519           close(CRT) or die "Couldn't close $crt.~: $!";
520         }
521         open(TMP, $pipe) or die "Couldn't open openssl pipe: $!";
522         print TMP $pem;
523         close(TMP) or die "Couldn't close openssl pipe: $!";
524         if (!$stdout) {
525           open(CRT, ">>$crt.~") or die "Couldn't open $crt.~: $!";
526         }
527       }
528       report "Parsing: $caname" if ($opt_v);
529       $certnum ++;
530       $start_of_cert = 0;
531     }
532     undef @precert;
533   }
534
535 }
536 close(TXT) or die "Couldn't close $txt: $!\n";
537 close(CRT) or die "Couldn't close $crt.~: $!\n";
538 unless( $stdout ) {
539     if ($opt_b && -e $crt) {
540         my $bk = 1;
541         while (-e "$crt.~${bk}~") {
542             $bk++;
543         }
544         rename $crt, "$crt.~${bk}~" or die "Failed to create backup $crt.~$bk}~: $!\n";
545     } elsif( -e $crt ) {
546         unlink( $crt ) or die "Failed to remove $crt: $!\n";
547     }
548     rename "$crt.~", $crt or die "Failed to rename $crt.~ to $crt: $!\n";
549 }
550 if($opt_u && -e $txt && !unlink($txt)) {
551   report "Failed to remove $txt: $!\n";
552 }
553 report "Done ($certnum CA certs processed, $skipnum skipped).";