xref: /freebsd/secure/caroot/MAca-bundle.pl (revision 457c03b397c80d44da92684d417a58b3ca1fed02)
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;
40use Time::Local qw( timegm_posix );
41use POSIX qw( strftime );
42
43my $generated = '@' . 'generated';
44my $inputfh = *STDIN;
45my $debug = 0;
46my $infile;
47my $outputdir;
48my %labels;
49my %certs;
50my %trusts;
51
52$debug++
53    if defined $ENV{'WITH_DEBUG'}
54	and $ENV{'WITH_DEBUG'} !~ m/(?i)^(no|0|false|)$/;
55
56GetOptions (
57	"debug+" => \$debug,
58	"infile:s" => \$infile,
59	"outputdir:s" => \$outputdir)
60  or die("Error in command line arguments\n$0 [-d] [-i input-file] [-o output-dir]\n");
61
62if ($infile) {
63    open($inputfh, "<", $infile) or die "Failed to open $infile";
64}
65
66sub print_header($$)
67{
68    my $dstfile = shift;
69    my $label = shift;
70
71    if ($outputdir) {
72	print $dstfile <<EOFH;
73##
74##  $label
75##
76##  This is a single X.509 certificate for a public Certificate
77##  Authority (CA). It was automatically extracted from Mozilla's
78##  root CA list (the file `certdata.txt' in security/nss).
79##
80##  It contains a certificate trusted for server authentication.
81##
82##  Extracted from nss
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##
100##  $generated
101##
102EOH
103    }
104}
105
106sub printcert($$$)
107{
108    my ($fh, $label, $certdata) = @_;
109    return unless $certdata;
110    open(OUT, "|openssl x509 -text -inform DER -fingerprint")
111            or die "could not pipe to openssl x509";
112    print OUT $certdata;
113    close(OUT) or die "openssl x509 failed with exit code $?";
114}
115
116# converts a datastream that is to be \177-style octal constants
117# from <> to a (binary) string and returns it
118sub graboct($)
119{
120    my $ifh = shift;
121    my $data;
122
123    while (<$ifh>) {
124	last if /^END/;
125	my (undef,@oct) = split /\\/;
126	my @bin = map(chr(oct), @oct);
127	$data .= join('', @bin);
128    }
129
130    return $data;
131}
132
133sub grabcert($)
134{
135    my $ifh = shift;
136    my $certdata;
137    my $cka_label = '';
138    my $serial = 0;
139    my $distrust = 0;
140
141    while (<$ifh>) {
142	chomp;
143	last if ($_ eq '');
144
145	if (/^CKA_LABEL UTF8 "([^"]+)"/) {
146	    $cka_label = $1;
147	}
148
149	if (/^CKA_VALUE MULTILINE_OCTAL/) {
150	    $certdata = graboct($ifh);
151	}
152
153	if (/^CKA_SERIAL_NUMBER MULTILINE_OCTAL/) {
154	    $serial = graboct($ifh);
155	}
156
157	if (/^CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL/)
158	{
159	    my $distrust_after = graboct($ifh);
160	    my ($year, $mon, $mday, $hour, $min, $sec) = unpack "A2A2A2A2A2A2", $distrust_after;
161	    $distrust_after = timegm_posix( $sec, $min, $hour, $mday, $mon - 1, $year + 100);
162	    my $time_now = time;
163	    # When a CA is distrusted before its NotAfter date, issued certificates
164	    # are valid for a maximum of 398 days after that date.
165	    if ($time_now >= $distrust_after + 398 * 24 * 60 * 60) { $distrust = 1; }
166	    if ($debug) {
167	        printf STDERR "line $.: $cka_label ser #%d: distrust 398 days after %s, now: %s -> distrust $distrust\n", $serial,
168	                strftime("%FT%TZ", gmtime($distrust_after)), strftime("%FT%TZ", gmtime($time_now));
169	    }
170	    if ($distrust) {
171		return undef;
172	    }
173	}
174    }
175    return ($serial, $cka_label, $certdata);
176}
177
178sub grabtrust($) {
179    my $ifh = shift;
180    my $cka_label;
181    my $serial;
182    my $maytrust = 0;
183    my $distrust = 0;
184
185    while (<$ifh>) {
186	chomp;
187	last if ($_ eq '');
188
189	if (/^CKA_LABEL UTF8 "([^"]+)"/) {
190	    $cka_label = $1;
191	}
192
193	if (/^CKA_SERIAL_NUMBER MULTILINE_OCTAL/) {
194	    $serial = graboct($ifh);
195	}
196
197	if (/^CKA_TRUST_SERVER_AUTH CK_TRUST (\S+)$/)
198	{
199	    if ($1 eq      'CKT_NSS_NOT_TRUSTED') {
200		$distrust = 1;
201	    } elsif ($1 eq 'CKT_NSS_TRUSTED_DELEGATOR') {
202		$maytrust = 1;
203	    } elsif ($1 ne 'CKT_NSS_MUST_VERIFY_TRUST') {
204		confess "Unknown trust setting on line $.:\n"
205		. "$_\n"
206		. "Script must be updated:";
207	    }
208	}
209    }
210
211    if (!$maytrust && !$distrust && $debug) {
212	print STDERR "line $.: no explicit trust/distrust found for $cka_label\n";
213    }
214
215    my $trust = ($maytrust and not $distrust);
216    return ($serial, $cka_label, $trust);
217}
218
219if (!$outputdir) {
220	print_header(*STDOUT, "");
221}
222
223my $untrusted = 0;
224
225while (<$inputfh>) {
226    if (/^CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE/) {
227	my ($serial, $label, $certdata) = grabcert($inputfh);
228	if (defined $certs{$label."\0".$serial}) {
229	    warn "Certificate $label duplicated!\n";
230	}
231	if (defined $certdata) {
232		$certs{$label."\0".$serial} = $certdata;
233		# We store the label in a separate hash because truncating the key
234		# with \0 was causing garbage data after the end of the text.
235		$labels{$label."\0".$serial} = $label;
236	} else { # $certdata undefined? distrust_after in effect
237		$untrusted ++;
238	}
239    } elsif (/^CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST/) {
240	my ($serial, $label, $trust) = grabtrust($inputfh);
241	if (defined $trusts{$label."\0".$serial}) {
242	    warn "Trust for $label duplicated!\n";
243	}
244	$trusts{$label."\0".$serial} = $trust;
245	$labels{$label."\0".$serial} = $label;
246    } elsif (/^CVS_ID.*Revision: ([^ ]*).*/) {
247        print "##  Source: \"certdata.txt\" CVS revision $1\n##\n\n";
248    }
249}
250
251sub label_to_filename(@) {
252    my @res = @_;
253    map { s/\0.*//; s/[^[:alnum:]\-]/_/g; $_ = "$_.pem"; } @res;
254    return wantarray ? @res : $res[0];
255}
256
257# weed out untrusted certificates
258foreach my $it (keys %trusts) {
259    if (!$trusts{$it}) {
260	if (!exists($certs{$it})) {
261	    warn "Found trust for nonexistent certificate $labels{$it}\n" if $debug;
262	} else {
263	    delete $certs{$it};
264	    warn "Skipping untrusted $labels{$it}\n" if $debug;
265	    $untrusted++;
266	}
267    }
268}
269
270if (!$outputdir) {
271    print		"##  Untrusted certificates omitted from this bundle: $untrusted\n\n";
272}
273print STDERR	"##  Untrusted certificates omitted from this bundle: $untrusted\n";
274
275my $certcount = 0;
276foreach my $it (sort {uc($a) cmp uc($b)} keys %certs) {
277    my $fh = *STDOUT;
278    my $filename;
279    if (!exists($trusts{$it})) {
280	die "Found certificate without trust block,\naborting";
281    }
282    if ($outputdir) {
283	$filename = label_to_filename($labels{$it});
284	open($fh, ">", "$outputdir/$filename") or die "Failed to open certificate $filename";
285	print_header($fh, $labels{$it});
286    }
287    printcert($fh, $labels{$it}, $certs{$it});
288    if ($outputdir) {
289	close($fh) or die "Unable to close: $filename";
290    } else {
291	print $fh "\n\n\n";
292    }
293    $certcount++;
294    print STDERR "Trusting $certcount: $labels{$it}\n" if $debug;
295}
296
297if ($certcount < 25) {
298    die "Certificate count of $certcount is implausibly low.\nAbort";
299}
300
301if (!$outputdir) {
302    print "##  Number of certificates: $certcount\n";
303    print "##  End of file.\n";
304}
305print STDERR	"##  Number of certificates: $certcount\n";
306