diff --git a/lib/mk-ca-bundle.pl b/lib/mk-ca-bundle.pl index e5a7420c0e..6c981ce1b9 100755 --- a/lib/mk-ca-bundle.pl +++ b/lib/mk-ca-bundle.pl @@ -6,7 +6,7 @@ # * | (__| |_| | _ <| |___ # * \___|\___/|_| \_\_____| # * -# * Copyright (C) 1998 - 2021, Daniel Stenberg, , et al. +# * Copyright (C) 1998 - 2022, Daniel Stenberg, , et al. # * # * This software is licensed as described in the file COPYING, which # * you should have received as part of this distribution. The terms @@ -63,11 +63,12 @@ $opt_d = 'release'; # If the OpenSSL commandline is not in search path you can configure it here! my $openssl = 'openssl'; -my $version = '1.28'; +my $version = '1.29'; $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) +# 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; @@ -94,8 +95,12 @@ my @valid_mozilla_trust_purposes = ( 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). + "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"; @@ -210,8 +215,8 @@ sub is_in_list($@) { 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 +# 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; @@ -227,7 +232,8 @@ sub parse_csv_param($$@) { 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 + # 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(); } @@ -282,7 +288,8 @@ 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 + # 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}} ) ); } @@ -421,9 +428,13 @@ my $caname; my $certnum = 0; my $skipnum = 0; my $start_of_cert = 0; +my $main_block = 0; +my $main_block_name; +my $trust_block = 0; +my $trust_block_name; my @precert; my $cka_value; -my $valid = 1; +my $valid = 0; open(TXT,"$txt") or die "Couldn't open $txt: $!\n"; while () { @@ -435,101 +446,170 @@ while () { print if ($opt_l); last if (/\*\*\*\*\* END LICENSE BLOCK \*\*\*\*\*/); } + next; } -# Not Valid After : Thu Sep 30 14:01:15 2021 - elsif(/^# Not Valid After : (.*)/) { - my $stamp = $1; - use Time::Piece; - my $t = Time::Piece->strptime - ($stamp, "%a %b %d %H:%M:%S %Y"); - my $delta = ($t->epoch - time()); # negative means no longer valid - if($delta < 0) { + # The input file format consists of blocks of Mozilla objects. + # The blocks are separated by blank lines but may be related. + elsif(/^\s*$/) { + $main_block = 0; + $trust_block = 0; + next; + } + # Each certificate has a main block. + elsif(/^# Certificate "(.*)"/) { + (!$main_block && !$trust_block) or die "Unexpected certificate block"; + $main_block = 1; + $main_block_name = $1; + # Reset all other certificate variables. + $trust_block = 0; + $trust_block_name = ""; + $valid = 0; + $start_of_cert = 0; + $caname = ""; + $cka_value = ""; + undef @precert; + next; + } + # Each certificate's main block is followed by a trust block. + elsif(/^# Trust for (?:Certificate )?"(.*)"/) { + (!$main_block && !$trust_block) or die "Unexpected trust block"; + $trust_block = 1; + $trust_block_name = $1; + if($main_block_name ne $trust_block_name) { + die "cert name \"$main_block_name\" != trust name \"$trust_block_name\""; + } + next; + } + # Ignore other blocks. + # + # There is a documentation comment block, a BEGINDATA block, and a bunch of + # blocks starting with "# Explicitly Distrust ". + # + # The latter is for certificates that have already been removed and are not + # included. Not all explicitly distrusted certificates are ignored at this + # point, just those without an actual certificate. + elsif(!$main_block && !$trust_block) { + next; + } + elsif(/^#/) { + # The commented lines in a main block are plaintext metadata that describes + # the certificate. Issuer, Subject, Fingerprint, etc. + if($main_block) { + push @precert, $_ if not /^#$/; + if(/^# Not Valid After : (.*)/) { + my $stamp = $1; + use Time::Piece; + # Not Valid After : Thu Sep 30 14:01:15 2021 + my $t = Time::Piece->strptime($stamp, "%a %b %d %H:%M:%S %Y"); + my $delta = ($t->epoch - time()); # negative means no longer valid + if($delta < 0) { $skipnum++; - report "Skipping: $caname is not valid anymore" if ($opt_v); + report "Skipping: $main_block_name is not valid anymore" if ($opt_v); $valid = 0; - } - else { + } + else { $valid = 1; - } - next; - } - elsif(/^# (Issuer|Serial Number|Subject|Not Valid Before|Fingerprint \(MD5\)|Fingerprint \(SHA1\)):/) { - push @precert, $_; - next; - } - elsif(/^#|^\s*$/) { - undef @precert; - next; - } - chomp; - - # Example: - # CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL - # \062\060\060\066\061\067\060\060\060\060\060\060\132 - # END - - if (/^CKA_NSS_SERVER_DISTRUST_AFTER (CK_BBOOL CK_FALSE|MULTILINE_OCTAL)/) { - if($1 eq "MULTILINE_OCTAL") { - my @timestamp; - while () { - last if (/^END/); - chomp; - my @octets = split(/\\/); - shift @octets; - for (@octets) { - push @timestamp, chr(oct); - } - } - # A trailing Z in the timestamp signifies UTC - if($timestamp[12] ne "Z") { - report "distrust date stamp is not using UTC"; - } - # Example date: 200617000000Z - # Means 2020-06-17 00:00:00 UTC - my $distrustat = - timegm($timestamp[10] . $timestamp[11], # second - $timestamp[8] . $timestamp[9], # minute - $timestamp[6] . $timestamp[7], # hour - $timestamp[4] . $timestamp[5], # day - ($timestamp[2] . $timestamp[3]) - 1, # month - "20" . $timestamp[0] . $timestamp[1]); # year - if(time >= $distrustat) { - # not trusted anymore - $skipnum++; - report "Skipping: $caname is not trusted anymore" if ($opt_v); - $valid = 0; - } - else { - # still trusted - } - } - next; - } - - # this is a match for the start of a certificate - if (/^CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE/) { - $start_of_cert = 1 - } - if ($start_of_cert && /^CKA_LABEL UTF8 \"(.*)\"/) { - $caname = $1; - } - my %trust_purposes_by_level; - if ($start_of_cert && /^CKA_VALUE MULTILINE_OCTAL/) { - $cka_value=""; - while () { - last if (/^END/); - chomp; - my @octets = split(/\\/); - shift @octets; - for (@octets) { - $cka_value .= chr(oct); + } } } + next; } - if(/^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST/ && $valid) { + elsif(!$valid) { + next; + } + + chomp; + + if($main_block) { + if(/^CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE/) { + !$start_of_cert or die "Duplicate CKO_CERTIFICATE object"; + $start_of_cert = 1; + next; + } + elsif(!$start_of_cert) { + next; + } + elsif(/^CKA_LABEL UTF8 \"(.*)\"/) { + ($caname eq "") or die "Duplicate CKA_LABEL attribute"; + $caname = $1; + if($caname ne $main_block_name) { + die "caname \"$caname\" != cert name \"$main_block_name\""; + } + next; + } + elsif(/^CKA_VALUE MULTILINE_OCTAL/) { + ($cka_value eq "") or die "Duplicate CKA_VALUE attribute"; + while () { + last if (/^END/); + chomp; + my @octets = split(/\\/); + shift @octets; + for (@octets) { + $cka_value .= chr(oct); + } + } + next; + } + elsif (/^CKA_NSS_SERVER_DISTRUST_AFTER (CK_BBOOL CK_FALSE|MULTILINE_OCTAL)/) { + # Example: + # CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL + # \062\060\060\066\061\067\060\060\060\060\060\060\132 + # END + if($1 eq "MULTILINE_OCTAL") { + my @timestamp; + while () { + last if (/^END/); + chomp; + my @octets = split(/\\/); + shift @octets; + for (@octets) { + push @timestamp, chr(oct); + } + } + scalar(@timestamp) == 13 or die "Failed parsing timestamp"; + # A trailing Z in the timestamp signifies UTC + if($timestamp[12] ne "Z") { + report "distrust date stamp is not using UTC"; + } + # Example date: 200617000000Z + # Means 2020-06-17 00:00:00 UTC + my $distrustat = + timegm($timestamp[10] . $timestamp[11], # second + $timestamp[8] . $timestamp[9], # minute + $timestamp[6] . $timestamp[7], # hour + $timestamp[4] . $timestamp[5], # day + ($timestamp[2] . $timestamp[3]) - 1, # month + "20" . $timestamp[0] . $timestamp[1]); # year + if(time >= $distrustat) { + # not trusted anymore + $skipnum++; + report "Skipping: $main_block_name is not trusted anymore" if ($opt_v); + $valid = 0; + } + else { + # still trusted + } + } + next; + } + else { + next; + } + } + + if(!$trust_block || !$start_of_cert || $caname eq "" || $cka_value eq "") { + die "Certificate extraction failed"; + } + + my %trust_purposes_by_level; + + if(/^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST/) { # now scan the trust part to determine how we should trust this cert while () { - last if (/^#/); + if(/^\s*$/) { + $trust_block = 0; + last; + } if (/^CKA_TRUST_([A-Z_]+)\s+CK_TRUST\s+CKT_NSS_([A-Z_]+)\s*$/) { if ( !is_in_list($1,@valid_mozilla_trust_purposes) ) { report "Warning: Unrecognized trust purpose for cert: $caname. Trust purpose: $1. Trust Level: $2"; @@ -541,33 +621,42 @@ while () { } } + # Sanity check that an explicitly distrusted certificate only has trust + # purposes with a trust level of NOT_TRUSTED. + # + # Certificate objects that are explicitly distrusted are in a certificate + # block that starts # Certificate "Explicitly Distrust(ed) ", + # where "Explicitly Distrust(ed) " was prepended to the original cert name. + if($caname =~ /distrust/i || + $main_block_name =~ /distrust/i || + $trust_block_name =~ /distrust/i) { + my @levels = keys %trust_purposes_by_level; + if(scalar(@levels) != 1 || $levels[0] ne "NOT_TRUSTED") { + die "\"$caname\" must have all trust purposes at level NOT_TRUSTED."; + } + } + if ( !should_output_cert(%trust_purposes_by_level) ) { $skipnum ++; - report "Skipping: $caname" if ($opt_v); + report "Skipping: $caname lacks acceptable trust level" if ($opt_v); } else { - my $data = $cka_value; - $cka_value = ""; - - if(!length($data)) { - # if empty, skip - next; - } - my $encoded = MIME::Base64::encode_base64($data, ''); + my $encoded = MIME::Base64::encode_base64($cka_value, ''); $encoded =~ s/(.{1,${opt_w}})/$1\n/g; my $pem = "-----BEGIN CERTIFICATE-----\n" . $encoded . "-----END CERTIFICATE-----\n"; print CRT "\n$caname\n"; - print CRT @precert if($opt_m); my $maxStringLength = length(decode('UTF-8', $caname, Encode::FB_CROAK | Encode::LEAVE_SRC)); + print CRT ("=" x $maxStringLength . "\n"); if ($opt_t) { foreach my $key (sort 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_m) { + print CRT for @precert; + } if (!$opt_t) { print CRT $pem; } else { @@ -597,13 +686,10 @@ while () { open(CRT, ">>$crt.~") or die "Couldn't open $crt.~: $!"; } } - report "Parsing: $caname" if ($opt_v); + report "Processed: $caname" if ($opt_v); $certnum ++; - $start_of_cert = 0; } - undef @precert; } - } close(TXT) or die "Couldn't close $txt: $!\n"; close(CRT) or die "Couldn't close $crt.~: $!\n";