Home | History | Annotate | Download | only in lib
      1 #!/usr/bin/env perl
      2 # ***************************************************************************
      3 # *                                  _   _ ____  _
      4 # *  Project                     ___| | | |  _ \| |
      5 # *                             / __| | | | |_) | |
      6 # *                            | (__| |_| |  _ <| |___
      7 # *                             \___|\___/|_| \_\_____|
      8 # *
      9 # * Copyright (C) 1998 - 2016, Daniel Stenberg, <daniel (at] 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).";
    554