xref: /freebsd/secure/caroot/ca-extract.pl (revision 0886019bf853bd30b70aa4b8cdb059830ca6aaad)
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