Created
April 28, 2019 11:55
-
-
Save dtonhofer/01018844971235b511d241b537c332ee to your computer and use it in GitHub Desktop.
Split a certificate bundle like "/etc/pki/tls/certs/ca-bundle.crt" into individual certificates labeled by issuer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
use strict; | |
use warnings; | |
# === | |
# Synopsis: | |
# | |
# sbin/split_ca_bundle.pl /etc/pki/tls/certs/ca-bundle.crt | |
# sbin/split_ca_bundle.pl /etc/pki/tls/certs/ca-bundle.crt | |
# | |
# The result of splitting the CA bundle goes to a new directory created in the | |
# current working directory. | |
# === | |
# Description: | |
# | |
# Split "certificate bundles" like those found in /etc/pki/tls/certs into | |
# individual files and append the X509 cleartext description to each file. | |
# | |
# The file to split is given on the command line or piped via STDIN. | |
# | |
# Files are written to a newly created directory. This directory is created | |
# in the current working directory. | |
# | |
# Created files are named "certificate.XXX" or "trusted-certificate.XXX", | |
# with XX an index value. The issuer/subject name (which should be the same | |
# as these are self-signed certificates) is append to the name. | |
# | |
# This works for bundles of both trusted and non-trusted certificates. | |
# | |
# See http://tygerclan.net/?q=node/49 for another program of this kind, | |
# which sets the name of the split-off files in function of the subject | |
# | |
# ------- | |
# Author: David Tonhofer | |
# License: Public Domain | |
# ------- | |
use File::Temp qw(tempdir); # Perl core module | |
use File::Spec::Functions qw(catfile); # Perl core module | |
# --- | |
# Try to create a temporary directory to be filled with certificates; | |
# The directory is created in tmpdir(); use "DIR => $dir" as additional | |
# argument to change that. | |
# --- | |
# my $tgdir = tempdir("explode_bundle_XXXX", TMPDIR => 1); | |
my $tgdir = tempdir("explode_bundle_XXXX"); # explode in current directory | |
if (!$tgdir) { | |
die "Could not create temporary directory: $!\n" | |
} | |
else { | |
print STDERR "Created temporary directory '$tgdir' into which result will be written.\n" | |
} | |
chdir $tgdir || die "Could not change working directory to '$tgdir': $!\n"; | |
# --- | |
# Read the file with certificates in a single slupr (from STDIN or from name of cmdline | |
# --- | |
my @lines = <> or die "Could not 'slurp' input (file or STDIN): $!\n"; | |
# --- | |
# Read and split using a simple state machine | |
# --- | |
my $state = "outside"; # reader state machine state | |
my $count = 0; # index of the certificate file we create | |
my $fh; # file handle of the certificate file we create | |
my $fn; # file name of the certificate file we create | |
my $trusted; # either undef or "TRUSTED" depend on type of certificate | |
for my $line (@lines) { | |
chomp $line; | |
if ($state eq "outside") { | |
if ($line =~ /^(-----BEGIN (TRUSTED )?CERTIFICATE-----)\s*$/) { | |
$trusted = $2; | |
($fn,$fh) = createCertFile($line,$count); | |
$state = "inside"; | |
$count++ | |
} | |
else { | |
print STDERR "Skipping line '$line'\n" | |
} | |
} | |
else { | |
if ($line =~ /^(-----END (TRUSTED )?CERTIFICATE-----)\s*$/) { | |
my $marker = $1; | |
my $trustedCheck = $2; | |
if (!((($trusted && $trustedCheck) || (!$trusted && !$trustedCheck)))) { | |
die "Trusted flag difference detected\n" | |
} | |
$state = "outside"; | |
print $fh "$marker\n"; | |
print STDERR "Closing file '$fn'\n"; | |
close $fh or die "Could not close file '$fn': $!\n"; | |
# | |
# Append x509 cleartext output by calling openssl tool | |
# | |
`openssl x509 -noout -text -in '$fn' >> '$fn'`; | |
if ($? != 0) { | |
die "Could not run 'openssl x509' command: $!\n"; | |
} | |
# | |
# Append Issuer and Subject to filename | |
# | |
my ($issuer,$subject,$selfsigned) = getIssuerAndSubject($fn); | |
my $fnewname; | |
if ($selfsigned) { | |
$fnewname = "$fn ($subject)" # can leave out the issuer | |
} | |
else { | |
$fnewname = "$fn ($issuer ---> $subject)" | |
} | |
rename($fn,$fnewname) or die "Could not rename '$fn' to '$fnewname': $!\n"; | |
} | |
else { | |
print $fh "$line\n" | |
} | |
} | |
} | |
if ($state eq "inside") { | |
die "Last certificate was not properly terminated\n" | |
} | |
print STDERR "Done. Everything can be found in temporary directory '$tgdir'.\n"; | |
# --- | |
# Create new file to accept certificate data | |
# --- | |
sub createCertFile { | |
my($line,$count) = @_; | |
$line =~ /^(-----BEGIN (TRUSTED )?CERTIFICATE-----)\s*$/ || die "Not-matching line '$line'\n"; | |
my $marker = $1; | |
my $trusted = $2; | |
my $prefix = ""; | |
$prefix = "trusted-" if ($trusted); | |
my $xcount = sprintf("%03d",$count); #prefix count with 0s | |
my $fn = "${prefix}certificate.$xcount"; | |
die "File '$fn' exists!" if (-e $fn); | |
print STDERR "Certificate data goes to file '$fn'\n"; | |
open($fh, '>:encoding(UTF-8)', $fn) || die "Could not create file '$fn': $!\n"; | |
print $fh "$marker\n"; | |
return ($fn,$fh) | |
} | |
# --- | |
# Grep for Issuer and Subject in just created file (is it in UTF-8?) | |
# --- | |
sub getIssuerAndSubject { | |
my($fn) = @_; | |
my $issuer; | |
my $subject; | |
my $fullIssuer; | |
my $fullSubject; | |
my $rest; | |
my $ref; # a reference to either $issuer or $subject | |
open(my $refh, '<:encoding(UTF-8)', $fn) || die "Could not open file '$fn' for reading: $!\n"; | |
while (my $line = <$refh>) { | |
# We see things like: "Issuer: C = PL, O = Krajowa Izba Rozliczeniowa S.A., CN = SZAFIR ROOT CA2" | |
if ($line =~ /^\s+Issuer:\s+(.+)$/) { | |
die "There are two 'Issuer' lines in fn'\n" if ($fullIssuer); | |
$rest = $1; | |
$fullIssuer = $1; | |
$ref = \$issuer # change issuer below | |
} | |
elsif ($line =~ /^\s+Subject:\s+(.+)$/) { | |
die "There are two 'Subject' lines in fn'\n" if ($fullSubject); | |
$rest = $1; | |
$fullSubject = $1; | |
$ref = \$subject # change subject below | |
} | |
else { | |
$rest = undef | |
} | |
if ($rest) { | |
die "ref undefined while rest defined" unless $ref; | |
if ($rest =~ /\bCN\s*=\s*(.+?)\s*(,|$)/) { | |
$$ref = $1 | |
} | |
else { | |
# Sometimes there is no CN, let's use the OU then | |
if ($rest =~ /\bOU\s*=\s*(.+?)\s*(,|$)/) { | |
$$ref = $1 | |
} | |
else { | |
# Sometimes there is no OU, let's use the C and then | |
# This is the case of Taiwan | |
my $country = '?'; | |
my $org = '?'; | |
if ($rest =~ /\bC\s*=\s*(.+?)\s*(,|$)/) { | |
$country = $1 | |
} | |
if ($rest =~ /\bO\s*=\s*(.+?)\s*(,|$)/) { | |
$org = $1 | |
} | |
$$ref = "$country $org" | |
} | |
} | |
} | |
} | |
close $refh || die "Could not close file '$fn': $!\n"; | |
my $selfsigned = ($issuer eq $subject); | |
return ($issuer,$subject,$selfsigned) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment