From 94898303d2b51198e90aa8e09545ed5e5b6b871c Mon Sep 17 00:00:00 2001 From: Patrick Watson Date: Thu, 8 May 2014 11:37:45 +0200 Subject: mk-ca-bundle: added -p -p takes a list 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. --- docs/mk-ca-bundle.1 | 39 +++++++++++-- lib/mk-ca-bundle.pl | 159 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 182 insertions(+), 16 deletions(-) diff --git a/docs/mk-ca-bundle.1 b/docs/mk-ca-bundle.1 index 1c43c1a2a..aa38612a8 100644 --- a/docs/mk-ca-bundle.1 +++ b/docs/mk-ca-bundle.1 @@ -24,13 +24,14 @@ .SH NAME mk-ca-bundle \- convert mozilla's certdata.txt to PEM format .SH SYNOPSIS -mk-ca-bundle [bilnqtuv] +mk-ca-bundle [bilnpqstuv] .I [outputfile] .SH DESCRIPTION The mk-ca-bundle tool downloads the certdata.txt file from Mozilla's source -tree over HTTP, then parses certdata.txt and extracts CA Root Certificates -into PEM format. These are then processed with the OpenSSL commandline tool -to produce the final ca-bundle file. +tree over HTTP, then parses certdata.txt and extracts certificates +into PEM format. By default, only CA root certificates trusted to issue SSL +server authentication certificates are extracted. These are then processed with +the OpenSSL commandline tool to produce the final ca-bundle file. The default \fIoutputfile\fP name is \fBca-bundle.crt\fP. By setting it to '-' (a single dash) you will get the output sent to STDOUT instead of a file. @@ -54,10 +55,40 @@ print version info about used modules print license info about certdata.txt .IP -n no download of certdata.txt (to use existing) +.IP "-p [purposes]:[levels]" +list 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. The default is to include all certificates trusted +to issue SSL Server certificates (SERVER_AUTH:TRUSTED_DELEGATOR). + +(Added in version 1.21, Perl only) + +Valid purposes are: +.RS +ALL, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, +DATA_ENCIPHERMENT, KEY_AGREEMENT, KEY_CERT_SIGN, CRL_SIGN, +SERVER_AUTH (default), CLIENT_AUTH, CODE_SIGNING, EMAIL_PROTECTION, +IPSEC_END_SYSTEM, IPSEC_TUNNEL, IPSEC_USER, TIME_STAMPING, STEP_UP_APPROVED +.RE + +Valid trust levels are: +.RS +ALL, TRUSTED_DELEGATOR (default), NOT_TRUSTED, MUST_VERIFY_TRUST, TRUSTED +.RE .IP -q be really quiet (no progress output at all) .IP -t include plain text listing of certificates +.IP "-s [algorithms]" +comma separated list of signature algorithms with which to hash/fingerprint +each certificate and output when run in plain text mode. + +(Added in version 1.21, Perl only) + +Valid algorithms are: +.RS +ALL, NONE, MD5 (default), SHA1, SHA256, SHA512 +.RE .IP -u unlink (remove) certdata.txt after processing .IP -v diff --git a/lib/mk-ca-bundle.pl b/lib/mk-ca-bundle.pl index 4b78ff187..232c36e4f 100755 --- a/lib/mk-ca-bundle.pl +++ b/lib/mk-ca-bundle.pl @@ -34,7 +34,9 @@ use Getopt::Std; use MIME::Base64; use LWP::UserAgent; use strict; -use vars qw($opt_b $opt_d $opt_f $opt_h $opt_i $opt_l $opt_n $opt_q $opt_t $opt_u $opt_v $opt_w); +use vars qw($opt_b $opt_d $opt_f $opt_h $opt_i $opt_l $opt_n $opt_p $opt_q $opt_s $opt_t $opt_u $opt_v $opt_w); +use List::Util; +use Text::Wrap; my %urls = ( 'nss' => @@ -56,13 +58,53 @@ $opt_d = 'release'; # If the OpenSSL commandline is not in search path you can configure it here! my $openssl = 'openssl'; -my $version = '1.20'; +my $version = '1.21'; $opt_w = 76; # default base64 encoded lines length +# default cert types to include in the output (default is to include CAs which may issue SSL server certs) +my $default_mozilla_trust_purposes = "SERVER_AUTH"; +my $default_mozilla_trust_levels = "TRUSTED_DELEGATOR"; +$opt_p = $default_mozilla_trust_purposes . ":" . $default_mozilla_trust_levels; + +my @valid_mozilla_trust_purposes = ( + "DIGITAL_SIGNATURE", + "NON_REPUDIATION", + "KEY_ENCIPHERMENT", + "DATA_ENCIPHERMENT", + "KEY_AGREEMENT", + "KEY_CERT_SIGN", + "CRL_SIGN", + "SERVER_AUTH", + "CLIENT_AUTH", + "CODE_SIGNING", + "EMAIL_PROTECTION", + "IPSEC_END_SYSTEM", + "IPSEC_TUNNEL", + "IPSEC_USER", + "TIME_STAMPING", + "STEP_UP_APPROVED" +); + +my @valid_mozilla_trust_levels = ( + "TRUSTED_DELEGATOR", # CAs + "NOT_TRUSTED", # Don't trust these certs. + "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. + "TRUSTED" # This cert is trusted, but only for itself and not for delegates (i.e. it is not a CA). +); + +my $default_signature_algorithms = $opt_s = "MD5"; + +my @valid_signature_algorithms = ( + "MD5", + "SHA1", + "SHA256", + "SHA512" +); + $0 =~ s@.*(/|\\)@@; $Getopt::Std::STANDARD_HELP_VERSION = 1; -getopts('bd:fhilnqtuvw:'); +getopts('bd:fhilnp:qs:tuvw:'); if(!defined($opt_d)) { # to make plain "-d" use not cause warnings, and actually still work @@ -102,7 +144,7 @@ sub WARNING_MESSAGE() { } sub HELP_MESSAGE() { - print "Usage:\t${0} [-b] [-d] [-f] [-i] [-l] [-n] [-q] [-t] [-u] [-v] [-w] []\n"; + print "Usage:\t${0} [-b] [-d] [-f] [-i] [-l] [-n] [-p] [-q] [-s] [-t] [-u] [-v] [-w] []\n"; print "\t-b\tbackup an existing version of ca-bundle.crt\n"; print "\t-d\tspecify Mozilla tree to pull certdata.txt or custom URL\n"; print "\t\t Valid names are:\n"; @@ -111,7 +153,15 @@ sub HELP_MESSAGE() { print "\t-i\tprint version info about used modules\n"; print "\t-l\tprint license info about certdata.txt\n"; print "\t-n\tno download of certdata.txt (to use existing)\n"; + 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"; + print "\t\t Valid purposes are:\n"; + print wrap("\t\t ","\t\t ", join( ", ", "ALL", @valid_mozilla_trust_purposes ) ), "\n"; + print "\t\t Valid levels are:\n"; + print wrap("\t\t ","\t\t ", join( ", ", "ALL", @valid_mozilla_trust_levels ) ), "\n"; print "\t-q\tbe really quiet (no progress output at all)\n"; + print wrap("\t","\t\t", "-s\tcomma separated list of certificate signatures/hashes to output in plain text mode. (default: $default_signature_algorithms)\n"); + print "\t\t Valid signature algorithms are:\n"; + print wrap("\t\t ","\t\t ", join( ", ", "ALL", @valid_signature_algorithms ) ), "\n"; print "\t-t\tinclude plain text listing of certificates\n"; print "\t-u\tunlink (remove) certdata.txt after processing\n"; print "\t-v\tbe verbose and print out processed CAs\n"; @@ -126,6 +176,61 @@ sub VERSION_MESSAGE() { WARNING_MESSAGE() unless ($opt_q || $url =~ m/^(ht|f)tps:/i ); HELP_MESSAGE() if ($opt_h); +sub IS_IN_LIST($@) { + my $target = shift; + + return defined(List::Util::first { $target eq $_ } @_); +} + +# Parses $param_string as a case insensitive comma separated list with optional whitespace +# validates that only allowed parameters are supplied +sub PARSE_CSV_PARAM($$@) { + my $description = shift; + my $param_string = shift; + my @valid_values = @_; + + my @values = map { + s/^\s+//; # strip leading spaces + s/\s+$//; # strip trailing spaces + uc $_ # return the modified string as upper case + } split( ',', $param_string ); + + # Find all values which are not in the list of valid values or "ALL" + my @invalid = grep { !IS_IN_LIST($_,"ALL",@valid_values) } @values; + + if ( scalar(@invalid) > 0 ) { + # Tell the user which parameters were invalid and print the standard help message which will exit + print "Error: Invalid ", $description, scalar(@invalid) == 1 ? ": " : "s: ", join( ", ", map { "\"$_\"" } @invalid ), "\n"; + HELP_MESSAGE(); + } + + @values = @valid_values if ( IS_IN_LIST("ALL",@values) ); + + return @values; +} + +if ( $opt_p !~ m/:/ ) { + print "Error: Mozilla trust identifier list must include both purposes and levels\n"; + HELP_MESSAGE(); +} + +(my $included_mozilla_trust_purposes_string, my $included_mozilla_trust_levels_string) = split( ':', $opt_p ); +my @included_mozilla_trust_purposes = PARSE_CSV_PARAM( "trust purpose", $included_mozilla_trust_purposes_string, @valid_mozilla_trust_purposes ); +my @included_mozilla_trust_levels = PARSE_CSV_PARAM( "trust level", $included_mozilla_trust_levels_string, @valid_mozilla_trust_levels ); + +my @included_signature_algorithms = PARSE_CSV_PARAM( "signature algorithm", $opt_s, @valid_signature_algorithms ); + +sub SHOULD_OUTPUT_CERT(%) { + my %trust_purposes_by_level = @_; + + foreach my $level (@included_mozilla_trust_levels) { + # for each level we want to output, see if any of our desired purposes are included + return 1 if ( defined( List::Util::first { IS_IN_LIST( $_, @included_mozilla_trust_purposes ) } @{$trust_purposes_by_level{$level}} ) ); + } + + return 0; +} + my $crt = $ARGV[0] || 'ca-bundle.crt'; (my $txt = $url) =~ s@(.*/|\?.*)@@g; @@ -209,7 +314,7 @@ while () { if ($start_of_cert && /^CKA_LABEL UTF8 \"(.*)\"/) { $caname = $1; } - my $untrusted = 1; + my %trust_purposes_by_level; if ($start_of_cert && /^CKA_VALUE MULTILINE_OCTAL/) { my $data; while () { @@ -226,14 +331,21 @@ while () { last if (/^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST/); chomp; } - # now scan the trust part for untrusted certs + # now scan the trust part to determine how we should trust this cert while () { last if (/^#/); - if (/^CKA_TRUST_SERVER_AUTH\s+CK_TRUST\s+CKT_NSS_TRUSTED_DELEGATOR$/) { - $untrusted = 0; + if (/^CKA_TRUST_([A-Z_]+)\s+CK_TRUST\s+CKT_NSS_([A-Z_]+)\s*$/) { + if ( !IS_IN_LIST($1,@valid_mozilla_trust_purposes) ) { + print STDERR "Warning: Unrecognized trust purpose for cert: $caname. Trust purpose: $1. Trust Level: $2\n" if (!$opt_q); + } elsif ( !IS_IN_LIST($2,@valid_mozilla_trust_levels) ) { + print STDERR "Warning: Unrecognized trust level for cert: $caname. Trust purpose: $1. Trust Level: $2\n" if (!$opt_q); + } else { + push @{$trust_purposes_by_level{$2}}, $1; + } } } - if ($untrusted) { + + if ( !SHOULD_OUTPUT_CERT(%trust_purposes_by_level) ) { $skipnum ++; } else { my $encoded = MIME::Base64::encode_base64($data, ''); @@ -242,11 +354,34 @@ while () { . $encoded . "-----END CERTIFICATE-----\n"; print CRT "\n$caname\n"; - print CRT ("=" x length($caname) . "\n"); + + my $maxStringLength = length($caname); + if ($opt_t) { + foreach my $key (keys %trust_purposes_by_level) { + my $string = $key . ": " . join(", ", @{$trust_purposes_by_level{$key}}); + $maxStringLength = List::Util::max( length($string), $maxStringLength ); + print CRT $string . "\n"; + } + } + print CRT ("=" x $maxStringLength . "\n"); if (!$opt_t) { print CRT $pem; } else { - my $pipe = "|$openssl x509 -md5 -fingerprint -text -inform PEM"; + my $pipe = ""; + foreach my $hash (@included_signature_algorithms) { + $pipe = "|$openssl x509 -" . $hash . " -fingerprint -noout -inform PEM"; + if (!$stdout) { + $pipe .= " >> $crt.~"; + close(CRT) or die "Couldn't close $crt.~: $!"; + } + open(TMP, $pipe) or die "Couldn't open openssl pipe: $!"; + print TMP $pem; + close(TMP) or die "Couldn't close openssl pipe: $!"; + if (!$stdout) { + open(CRT, ">>$crt.~") or die "Couldn't open $crt.~: $!"; + } + } + $pipe = "|$openssl x509 -text -inform PEM"; if (!$stdout) { $pipe .= " >> $crt.~"; close(CRT) or die "Couldn't close $crt.~: $!"; @@ -279,7 +414,7 @@ unless( $stdout ) { rename "$crt.~", $crt or die "Failed to rename $crt.~ to $crt: $!\n"; } unlink $txt if ($opt_u); -print STDERR "Done ($certnum CA certs processed, $skipnum untrusted skipped).\n" if (!$opt_q); +print STDERR "Done ($certnum CA certs processed, $skipnum skipped).\n" if (!$opt_q); exit; -- cgit v1.2.3