1#!/usr/bin/perl -w 2# *************************************************************************** 3# * _ _ ____ _ 4# * Project ___| | | | _ \| | 5# * / __| | | | |_) | | 6# * | (__| |_| | _ <| |___ 7# * \___|\___/|_| \_\_____| 8# * 9# * Copyright (C) 1998 - 2014, 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 http://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# 33use File::Basename 'dirname'; 34use Getopt::Std; 35use MIME::Base64; 36use strict; 37use vars qw($opt_h $opt_i $opt_l $opt_p $opt_q $opt_s $opt_t $opt_v $opt_w); 38use List::Util; 39use Text::Wrap; 40 41# If the OpenSSL commandline is not in search path you can configure it here! 42my $openssl = 'openssl'; 43 44my $version = '1.25'; 45 46$opt_w = 72; # default base64 encoded lines length 47 48# default cert types to include in the output (default is to include CAs which may issue SSL server certs) 49my $default_mozilla_trust_purposes = "SERVER_AUTH"; 50my $default_mozilla_trust_levels = "TRUSTED_DELEGATOR"; 51$opt_p = $default_mozilla_trust_purposes . ":" . $default_mozilla_trust_levels; 52 53my @valid_mozilla_trust_purposes = ( 54 "DIGITAL_SIGNATURE", 55 "NON_REPUDIATION", 56 "KEY_ENCIPHERMENT", 57 "DATA_ENCIPHERMENT", 58 "KEY_AGREEMENT", 59 "KEY_CERT_SIGN", 60 "CRL_SIGN", 61 "SERVER_AUTH", 62 "CLIENT_AUTH", 63 "CODE_SIGNING", 64 "EMAIL_PROTECTION", 65 "IPSEC_END_SYSTEM", 66 "IPSEC_TUNNEL", 67 "IPSEC_USER", 68 "TIME_STAMPING", 69 "STEP_UP_APPROVED" 70); 71 72my @valid_mozilla_trust_levels = ( 73 "TRUSTED_DELEGATOR", # CAs 74 "NOT_TRUSTED", # Don't trust these certs. 75 "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. 76 "TRUSTED" # This cert is trusted, but only for itself and not for delegates (i.e. it is not a CA). 77); 78 79my $default_signature_algorithms = $opt_s = "MD5"; 80 81my @valid_signature_algorithms = ( 82 "MD5", 83 "SHA1", 84 "SHA256", 85 "SHA384", 86 "SHA512" 87); 88 89$0 =~ s@.*(/|\\)@@; 90$Getopt::Std::STANDARD_HELP_VERSION = 1; 91getopts('bd:fhilnp:qs:tuvw:'); 92 93if ($opt_i) { 94 print ("=" x 78 . "\n"); 95 print "Script Version : $version\n"; 96 print "Perl Version : $]\n"; 97 print "Operating System Name : $^O\n"; 98 print "Getopt::Std.pm Version : ${Getopt::Std::VERSION}\n"; 99 print "MIME::Base64.pm Version : ${MIME::Base64::VERSION}\n"; 100 print ("=" x 78 . "\n"); 101} 102 103sub HELP_MESSAGE() { 104 print "Usage:\t${0} [-i] [-l] [-p<purposes:levels>] [-q] [-s<algorithms>] [-t] [-v] [-w<l>] [<outputfile>]\n"; 105 print "\t-i\tprint version info about used modules\n"; 106 print "\t-l\tprint license info about certdata.txt\n"; 107 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"; 108 print "\t\t Valid purposes are:\n"; 109 print wrap("\t\t ","\t\t ", join( ", ", "ALL", @valid_mozilla_trust_purposes ) ), "\n"; 110 print "\t\t Valid levels are:\n"; 111 print wrap("\t\t ","\t\t ", join( ", ", "ALL", @valid_mozilla_trust_levels ) ), "\n"; 112 print "\t-q\tbe really quiet (no progress output at all)\n"; 113 print wrap("\t","\t\t", "-s\tcomma separated list of certificate signatures/hashes to output in plain text mode. (default: $default_signature_algorithms)\n"); 114 print "\t\t Valid signature algorithms are:\n"; 115 print wrap("\t\t ","\t\t ", join( ", ", "ALL", @valid_signature_algorithms ) ), "\n"; 116 print "\t-t\tinclude plain text listing of certificates\n"; 117 print "\t-v\tbe verbose and print out processed CAs\n"; 118 print "\t-w <l>\twrap base64 output lines after <l> chars (default: ${opt_w})\n"; 119 exit; 120} 121 122sub VERSION_MESSAGE() { 123 print "${0} version ${version} running Perl ${]} on ${^O}\n"; 124} 125 126HELP_MESSAGE() if ($opt_h); 127 128sub report($@) { 129 my $output = shift; 130 131 print STDERR $output . "\n" unless $opt_q; 132} 133 134sub is_in_list($@) { 135 my $target = shift; 136 137 return defined(List::Util::first { $target eq $_ } @_); 138} 139 140# Parses $param_string as a case insensitive comma separated list with optional whitespace 141# validates that only allowed parameters are supplied 142sub parse_csv_param($$@) { 143 my $description = shift; 144 my $param_string = shift; 145 my @valid_values = @_; 146 147 my @values = map { 148 s/^\s+//; # strip leading spaces 149 s/\s+$//; # strip trailing spaces 150 uc $_ # return the modified string as upper case 151 } split( ',', $param_string ); 152 153 # Find all values which are not in the list of valid values or "ALL" 154 my @invalid = grep { !is_in_list($_,"ALL",@valid_values) } @values; 155 156 if ( scalar(@invalid) > 0 ) { 157 # Tell the user which parameters were invalid and print the standard help message which will exit 158 print "Error: Invalid ", $description, scalar(@invalid) == 1 ? ": " : "s: ", join( ", ", map { "\"$_\"" } @invalid ), "\n"; 159 HELP_MESSAGE(); 160 } 161 162 @values = @valid_values if ( is_in_list("ALL",@values) ); 163 164 return @values; 165} 166 167if ( $opt_p !~ m/:/ ) { 168 print "Error: Mozilla trust identifier list must include both purposes and levels\n"; 169 HELP_MESSAGE(); 170} 171 172(my $included_mozilla_trust_purposes_string, my $included_mozilla_trust_levels_string) = split( ':', $opt_p ); 173my @included_mozilla_trust_purposes = parse_csv_param( "trust purpose", $included_mozilla_trust_purposes_string, @valid_mozilla_trust_purposes ); 174my @included_mozilla_trust_levels = parse_csv_param( "trust level", $included_mozilla_trust_levels_string, @valid_mozilla_trust_levels ); 175 176my @included_signature_algorithms = parse_csv_param( "signature algorithm", $opt_s, @valid_signature_algorithms ); 177 178sub should_output_cert(%) { 179 my %trust_purposes_by_level = @_; 180 181 foreach my $level (@included_mozilla_trust_levels) { 182 # for each level we want to output, see if any of our desired purposes are included 183 return 1 if ( defined( List::Util::first { is_in_list( $_, @included_mozilla_trust_purposes ) } @{$trust_purposes_by_level{$level}} ) ); 184 } 185 186 return 0; 187} 188 189my $crt = $ARGV[0] || dirname(__FILE__) . '/../src/node_root_certs.h'; 190my $txt = dirname(__FILE__) . '/certdata.txt'; 191 192my $stdout = $crt eq '-'; 193 194if( $stdout ) { 195 open(CRT, '> -') or die "Couldn't open STDOUT: $!\n"; 196} else { 197 open(CRT,">$crt.~") or die "Couldn't open $crt.~: $!\n"; 198} 199 200my $caname; 201my $certnum = 0; 202my $skipnum = 0; 203my $start_of_cert = 0; 204 205open(TXT,"$txt") or die "Couldn't open $txt: $!\n"; 206print CRT "#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS\n"; 207while (<TXT>) { 208 if (/\*\*\*\*\* BEGIN LICENSE BLOCK \*\*\*\*\*/) { 209 print CRT; 210 print if ($opt_l); 211 while (<TXT>) { 212 print CRT; 213 print if ($opt_l); 214 last if (/\*\*\*\*\* END LICENSE BLOCK \*\*\*\*\*/); 215 } 216 } 217 next if /^#|^\s*$/; 218 chomp; 219 if (/^CVS_ID\s+\"(.*)\"/) { 220 print CRT "/* $1 */\n"; 221 } 222 223 # this is a match for the start of a certificate 224 if (/^CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE/) { 225 $start_of_cert = 1 226 } 227 if ($start_of_cert && /^CKA_LABEL UTF8 \"(.*)\"/) { 228 $caname = $1; 229 } 230 my %trust_purposes_by_level; 231 if ($start_of_cert && /^CKA_VALUE MULTILINE_OCTAL/) { 232 my $data; 233 while (<TXT>) { 234 last if (/^END/); 235 chomp; 236 my @octets = split(/\\/); 237 shift @octets; 238 for (@octets) { 239 $data .= chr(oct); 240 } 241 } 242 # scan forwards until the trust part 243 while (<TXT>) { 244 last if (/^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST/); 245 chomp; 246 } 247 # now scan the trust part to determine how we should trust this cert 248 while (<TXT>) { 249 last if (/^#/); 250 if (/^CKA_TRUST_([A-Z_]+)\s+CK_TRUST\s+CKT_NSS_([A-Z_]+)\s*$/) { 251 if ( !is_in_list($1,@valid_mozilla_trust_purposes) ) { 252 report "Warning: Unrecognized trust purpose for cert: $caname. Trust purpose: $1. Trust Level: $2"; 253 } elsif ( !is_in_list($2,@valid_mozilla_trust_levels) ) { 254 report "Warning: Unrecognized trust level for cert: $caname. Trust purpose: $1. Trust Level: $2"; 255 } else { 256 push @{$trust_purposes_by_level{$2}}, $1; 257 } 258 } 259 } 260 261 if ( !should_output_cert(%trust_purposes_by_level) ) { 262 $skipnum ++; 263 } else { 264 my $encoded = MIME::Base64::encode_base64($data, ''); 265 $encoded =~ s/(.{1,${opt_w}})/"$1\\n"\n/g; 266 my $pem = "\"-----BEGIN CERTIFICATE-----\\n\"\n" 267 . $encoded 268 . "\"-----END CERTIFICATE-----\",\n"; 269 print CRT "\n/* $caname */\n"; 270 271 my $maxStringLength = length($caname); 272 if ($opt_t) { 273 foreach my $key (keys %trust_purposes_by_level) { 274 my $string = $key . ": " . join(", ", @{$trust_purposes_by_level{$key}}); 275 $maxStringLength = List::Util::max( length($string), $maxStringLength ); 276 print CRT $string . "\n"; 277 } 278 } 279 if (!$opt_t) { 280 print CRT $pem; 281 } else { 282 my $pipe = ""; 283 foreach my $hash (@included_signature_algorithms) { 284 $pipe = "|$openssl x509 -" . $hash . " -fingerprint -noout -inform PEM"; 285 if (!$stdout) { 286 $pipe .= " >> $crt.~"; 287 close(CRT) or die "Couldn't close $crt.~: $!"; 288 } 289 open(TMP, $pipe) or die "Couldn't open openssl pipe: $!"; 290 print TMP $pem; 291 close(TMP) or die "Couldn't close openssl pipe: $!"; 292 if (!$stdout) { 293 open(CRT, ">>$crt.~") or die "Couldn't open $crt.~: $!"; 294 } 295 } 296 $pipe = "|$openssl x509 -text -inform PEM"; 297 if (!$stdout) { 298 $pipe .= " >> $crt.~"; 299 close(CRT) or die "Couldn't close $crt.~: $!"; 300 } 301 open(TMP, $pipe) or die "Couldn't open openssl pipe: $!"; 302 print TMP $pem; 303 close(TMP) or die "Couldn't close openssl pipe: $!"; 304 if (!$stdout) { 305 open(CRT, ">>$crt.~") or die "Couldn't open $crt.~: $!"; 306 } 307 } 308 report "Parsing: $caname" if ($opt_v); 309 $certnum ++; 310 $start_of_cert = 0; 311 } 312 } 313} 314print CRT "#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS\n"; 315close(TXT) or die "Couldn't close $txt: $!\n"; 316close(CRT) or die "Couldn't close $crt.~: $!\n"; 317unless( $stdout ) { 318 rename "$crt.~", $crt or die "Failed to rename $crt.~ to $crt: $!\n"; 319} 320report "Done ($certnum CA certs processed, $skipnum skipped)."; 321