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##  Extracted from nss
80##  with $VERSION
81##
82##  $generated
83##
84EOFH
85    } else {
86	print $dstfile <<EOH;
87##
88##  ca-root-nss.crt -- Bundle of CA Root Certificates
89##
90##  This is a bundle of X.509 certificates of public Certificate
91##  Authorities (CA). These were automatically extracted from Mozilla's
92##  root CA list (the file `certdata.txt').
93##
94##  Extracted from nss
95##  with $VERSION
96##
97##  $generated
98##
99EOH
100    }
101}
102
103sub printcert($$$)
104{
105    my ($fh, $label, $certdata) = @_;
106    return unless $certdata;
107    open(OUT, "|openssl x509 -text -inform DER -fingerprint")
108            or die "could not pipe to openssl x509";
109    print OUT $certdata;
110    close(OUT) or die "openssl x509 failed with exit code $?";
111}
112
113sub graboct($)
114{
115    my $ifh = shift;
116    my $data;
117
118    while (<$ifh>) {
119	last if /^END/;
120	my (undef,@oct) = split /\\/;
121	my @bin = map(chr(oct), @oct);
122	$data .= join('', @bin);
123    }
124
125    return $data;
126}
127
128
129sub grabcert($)
130{
131    my $ifh = shift;
132    my $certdata;
133    my $cka_label;
134    my $serial;
135
136    while (<$ifh>) {
137	chomp;
138	last if ($_ eq '');
139
140	if (/^CKA_LABEL UTF8 "([^"]+)"/) {
141	    $cka_label = $1;
142	}
143
144	if (/^CKA_VALUE MULTILINE_OCTAL/) {
145	    $certdata = graboct($ifh);
146	}
147
148	if (/^CKA_SERIAL_NUMBER MULTILINE_OCTAL/) {
149	    $serial = graboct($ifh);
150	}
151    }
152    return ($serial, $cka_label, $certdata);
153}
154
155sub grabtrust($) {
156    my $ifh = shift;
157    my $cka_label;
158    my $serial;
159    my $maytrust = 0;
160    my $distrust = 0;
161
162    while (<$ifh>) {
163	chomp;
164	last if ($_ eq '');
165
166	if (/^CKA_LABEL UTF8 "([^"]+)"/) {
167	    $cka_label = $1;
168	}
169
170	if (/^CKA_SERIAL_NUMBER MULTILINE_OCTAL/) {
171	    $serial = graboct($ifh);
172	}
173
174	if (/^CKA_TRUST_(SERVER_AUTH|EMAIL_PROTECTION|CODE_SIGNING) CK_TRUST (\S+)$/)
175	{
176	    if ($2 eq      'CKT_NSS_NOT_TRUSTED') {
177		$distrust = 1;
178	    } elsif ($2 eq 'CKT_NSS_TRUSTED_DELEGATOR') {
179		$maytrust = 1;
180	    } elsif ($2 ne 'CKT_NSS_MUST_VERIFY_TRUST') {
181		confess "Unknown trust setting on line $.:\n"
182		. "$_\n"
183		. "Script must be updated:";
184	    }
185	}
186    }
187
188    if (!$maytrust && !$distrust && $debug) {
189	print STDERR "line $.: no explicit trust/distrust found for $cka_label\n";
190    }
191
192    my $trust = ($maytrust and not $distrust);
193    return ($serial, $cka_label, $trust);
194}
195
196if (!$outputdir) {
197	print_header(*STDOUT, "");
198}
199
200while (<$inputfh>) {
201    if (/^CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE/) {
202	my ($serial, $label, $certdata) = grabcert($inputfh);
203	if (defined $certs{$label."\0".$serial}) {
204	    warn "Certificate $label duplicated!\n";
205	}
206	$certs{$label."\0".$serial} = $certdata;
207	# We store the label in a separate hash because truncating the key
208	# with \0 was causing garbage data after the end of the text.
209	$labels{$label."\0".$serial} = $label;
210    } elsif (/^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST/) {
211	my ($serial, $label, $trust) = grabtrust($inputfh);
212	if (defined $trusts{$label."\0".$serial}) {
213	    warn "Trust for $label duplicated!\n";
214	}
215	$trusts{$label."\0".$serial} = $trust;
216	$labels{$label."\0".$serial} = $label;
217    } elsif (/^CVS_ID.*Revision: ([^ ]*).*/) {
218        print "##  Source: \"certdata.txt\" CVS revision $1\n##\n\n";
219    }
220}
221
222sub label_to_filename(@) {
223    my @res = @_;
224    map { s/\0.*//; s/[^[:alnum:]\-]/_/g; $_ = "$_.pem"; } @res;
225    return wantarray ? @res : $res[0];
226}
227
228# weed out untrusted certificates
229my $untrusted = 0;
230foreach my $it (keys %trusts) {
231    if (!$trusts{$it}) {
232	if (!exists($certs{$it})) {
233	    warn "Found trust for nonexistent certificate $labels{$it}\n" if $debug;
234	} else {
235	    delete $certs{$it};
236	    warn "Skipping untrusted $labels{$it}\n" if $debug;
237	    $untrusted++;
238	}
239    }
240}
241
242if (!$outputdir) {
243    print		"##  Untrusted certificates omitted from this bundle: $untrusted\n\n";
244}
245print STDERR	"##  Untrusted certificates omitted from this bundle: $untrusted\n";
246
247my $certcount = 0;
248foreach my $it (sort {uc($a) cmp uc($b)} keys %certs) {
249    my $fh = *STDOUT;
250    my $filename;
251    if (!exists($trusts{$it})) {
252	die "Found certificate without trust block,\naborting";
253    }
254    if ($outputdir) {
255	$filename = label_to_filename($labels{$it});
256	open($fh, ">", "$outputdir/$filename") or die "Failed to open certificate $filename";
257	print_header($fh, $labels{$it});
258    }
259    printcert($fh, $labels{$it}, $certs{$it});
260    if ($outputdir) {
261	close($fh) or die "Unable to close: $filename";
262    } else {
263	print $fh "\n\n\n";
264    }
265    $certcount++;
266    print STDERR "Trusting $certcount: $labels{$it}\n" if $debug;
267}
268
269if ($certcount < 25) {
270    die "Certificate count of $certcount is implausibly low.\nAbort";
271}
272
273if (!$outputdir) {
274    print "##  Number of certificates: $certcount\n";
275    print "##  End of file.\n";
276}
277print STDERR	"##  Number of certificates: $certcount\n";
278