1#!/usr/bin/env perl 2## 3## MAca-bundle.pl -- Regenerate ca-root-nss.crt from the Mozilla certdata.txt 4## 5## Rewritten in September 2011 by Matthias Andree to heed untrust 6## 7 8## Copyright (c) 2011, 2013 Matthias Andree <mandree@FreeBSD.org> 9## All rights reserved. 10## Copyright (c) 2018, Allan Jude <allanjude@FreeBSD.org> 11## 12## Redistribution and use in source and binary forms, with or without 13## modification, are permitted provided that the following conditions are 14## met: 15## 16## * Redistributions of source code must retain the above copyright 17## notice, this list of conditions and the following disclaimer. 18## 19## * Redistributions in binary form must reproduce the above copyright 20## notice, this list of conditions and the following disclaimer in the 21## documentation and/or other materials provided with the distribution. 22## 23## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26## FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27## COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 28## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 29## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 30## LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 31## CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 32## LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 33## ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34## POSSIBILITY OF SUCH DAMAGE. 35 36use strict; 37use Carp; 38use MIME::Base64; 39use Getopt::Long; 40 41my $VERSION = '$FreeBSD$'; 42my $generated = '@' . 'generated'; 43my $inputfh = *STDIN; 44my $debug = 0; 45my $infile; 46my $outputdir; 47my %labels; 48my %certs; 49my %trusts; 50 51$debug++ 52 if defined $ENV{'WITH_DEBUG'} 53 and $ENV{'WITH_DEBUG'} !~ m/(?i)^(no|0|false|)$/; 54 55GetOptions ( 56 "debug+" => \$debug, 57 "infile:s" => \$infile, 58 "outputdir:s" => \$outputdir) 59 or die("Error in command line arguments\n$0 [-d] [-i input-file] [-o output-dir]\n"); 60 61if ($infile) { 62 open($inputfh, "<", $infile) or die "Failed to open $infile"; 63} 64 65sub print_header($$) 66{ 67 my $dstfile = shift; 68 my $label = shift; 69 70 if ($outputdir) { 71 print $dstfile <<EOFH; 72## 73## $label 74## 75## This is a single X.509 certificate for a public Certificate 76## Authority (CA). It was automatically extracted from Mozilla's 77## root CA list (the file `certdata.txt' in security/nss). 78## 79## It contains a certificate trusted for server authentication. 80## 81## Extracted from nss 82## with $VERSION 83## 84## $generated 85## 86EOFH 87 } else { 88 print $dstfile <<EOH; 89## 90## ca-root-nss.crt -- Bundle of CA Root Certificates 91## 92## This is a bundle of X.509 certificates of public Certificate 93## Authorities (CA). These were automatically extracted from Mozilla's 94## root CA list (the file `certdata.txt'). 95## 96## It contains certificates trusted for server authentication. 97## 98## Extracted from nss 99## with $VERSION 100## 101## $generated 102## 103EOH 104 } 105} 106 107# returns a string like YYMMDDhhmmssZ of current time in GMT zone 108sub timenow() 109{ 110 my ($sec,$min,$hour,$mday,$mon,$year,undef,undef,undef) = gmtime(time); 111 return sprintf "%02d%02d%02d%02d%02d%02dZ", $year-100, $mon+1, $mday, $hour, $min, $sec; 112} 113 114sub printcert($$$) 115{ 116 my ($fh, $label, $certdata) = @_; 117 return unless $certdata; 118 open(OUT, "|openssl x509 -text -inform DER -fingerprint") 119 or die "could not pipe to openssl x509"; 120 print OUT $certdata; 121 close(OUT) or die "openssl x509 failed with exit code $?"; 122} 123 124# converts a datastream that is to be \177-style octal constants 125# from <> to a (binary) string and returns it 126sub graboct($) 127{ 128 my $ifh = shift; 129 my $data; 130 131 while (<$ifh>) { 132 last if /^END/; 133 my (undef,@oct) = split /\\/; 134 my @bin = map(chr(oct), @oct); 135 $data .= join('', @bin); 136 } 137 138 return $data; 139} 140 141sub grabcert($) 142{ 143 my $ifh = shift; 144 my $certdata; 145 my $cka_label = ''; 146 my $serial = 0; 147 my $distrust = 0; 148 149 while (<$ifh>) { 150 chomp; 151 last if ($_ eq ''); 152 153 if (/^CKA_LABEL UTF8 "([^"]+)"/) { 154 $cka_label = $1; 155 } 156 157 if (/^CKA_VALUE MULTILINE_OCTAL/) { 158 $certdata = graboct($ifh); 159 } 160 161 if (/^CKA_SERIAL_NUMBER MULTILINE_OCTAL/) { 162 $serial = graboct($ifh); 163 } 164 165 if (/^CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL/) 166 { 167 my $distrust_after = graboct($ifh); 168 my $time_now = timenow(); 169 if ($time_now >= $distrust_after) { $distrust = 1; } 170 if ($debug) { 171 printf STDERR "line $.: $cka_label ser #%d: distrust after %s, now: %s -> distrust $distrust\n", $serial, $distrust_after, timenow(); 172 } 173 if ($distrust) { 174 return undef; 175 } 176 } 177 } 178 return ($serial, $cka_label, $certdata); 179} 180 181sub grabtrust($) { 182 my $ifh = shift; 183 my $cka_label; 184 my $serial; 185 my $maytrust = 0; 186 my $distrust = 0; 187 188 while (<$ifh>) { 189 chomp; 190 last if ($_ eq ''); 191 192 if (/^CKA_LABEL UTF8 "([^"]+)"/) { 193 $cka_label = $1; 194 } 195 196 if (/^CKA_SERIAL_NUMBER MULTILINE_OCTAL/) { 197 $serial = graboct($ifh); 198 } 199 200 if (/^CKA_TRUST_SERVER_AUTH CK_TRUST (\S+)$/) 201 { 202 if ($1 eq 'CKT_NSS_NOT_TRUSTED') { 203 $distrust = 1; 204 } elsif ($1 eq 'CKT_NSS_TRUSTED_DELEGATOR') { 205 $maytrust = 1; 206 } elsif ($1 ne 'CKT_NSS_MUST_VERIFY_TRUST') { 207 confess "Unknown trust setting on line $.:\n" 208 . "$_\n" 209 . "Script must be updated:"; 210 } 211 } 212 } 213 214 if (!$maytrust && !$distrust && $debug) { 215 print STDERR "line $.: no explicit trust/distrust found for $cka_label\n"; 216 } 217 218 my $trust = ($maytrust and not $distrust); 219 return ($serial, $cka_label, $trust); 220} 221 222if (!$outputdir) { 223 print_header(*STDOUT, ""); 224} 225 226my $untrusted = 0; 227 228while (<$inputfh>) { 229 if (/^CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE/) { 230 my ($serial, $label, $certdata) = grabcert($inputfh); 231 if (defined $certs{$label."\0".$serial}) { 232 warn "Certificate $label duplicated!\n"; 233 } 234 if (defined $certdata) { 235 $certs{$label."\0".$serial} = $certdata; 236 # We store the label in a separate hash because truncating the key 237 # with \0 was causing garbage data after the end of the text. 238 $labels{$label."\0".$serial} = $label; 239 } else { # $certdata undefined? distrust_after in effect 240 $untrusted ++; 241 } 242 } elsif (/^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST/) { 243 my ($serial, $label, $trust) = grabtrust($inputfh); 244 if (defined $trusts{$label."\0".$serial}) { 245 warn "Trust for $label duplicated!\n"; 246 } 247 $trusts{$label."\0".$serial} = $trust; 248 $labels{$label."\0".$serial} = $label; 249 } elsif (/^CVS_ID.*Revision: ([^ ]*).*/) { 250 print "## Source: \"certdata.txt\" CVS revision $1\n##\n\n"; 251 } 252} 253 254sub label_to_filename(@) { 255 my @res = @_; 256 map { s/\0.*//; s/[^[:alnum:]\-]/_/g; $_ = "$_.pem"; } @res; 257 return wantarray ? @res : $res[0]; 258} 259 260# weed out untrusted certificates 261foreach my $it (keys %trusts) { 262 if (!$trusts{$it}) { 263 if (!exists($certs{$it})) { 264 warn "Found trust for nonexistent certificate $labels{$it}\n" if $debug; 265 } else { 266 delete $certs{$it}; 267 warn "Skipping untrusted $labels{$it}\n" if $debug; 268 $untrusted++; 269 } 270 } 271} 272 273if (!$outputdir) { 274 print "## Untrusted certificates omitted from this bundle: $untrusted\n\n"; 275} 276print STDERR "## Untrusted certificates omitted from this bundle: $untrusted\n"; 277 278my $certcount = 0; 279foreach my $it (sort {uc($a) cmp uc($b)} keys %certs) { 280 my $fh = *STDOUT; 281 my $filename; 282 if (!exists($trusts{$it})) { 283 die "Found certificate without trust block,\naborting"; 284 } 285 if ($outputdir) { 286 $filename = label_to_filename($labels{$it}); 287 open($fh, ">", "$outputdir/$filename") or die "Failed to open certificate $filename"; 288 print_header($fh, $labels{$it}); 289 } 290 printcert($fh, $labels{$it}, $certs{$it}); 291 if ($outputdir) { 292 close($fh) or die "Unable to close: $filename"; 293 } else { 294 print $fh "\n\n\n"; 295 } 296 $certcount++; 297 print STDERR "Trusting $certcount: $labels{$it}\n" if $debug; 298} 299 300if ($certcount < 25) { 301 die "Certificate count of $certcount is implausibly low.\nAbort"; 302} 303 304if (!$outputdir) { 305 print "## Number of certificates: $certcount\n"; 306 print "## End of file.\n"; 307} 308print STDERR "## Number of certificates: $certcount\n"; 309