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