#!/usr/bin/perl

# Perl c_rehash script, scan all files in a directory
# and add symbolic links to their hash values.
#
# 2011-03-06 jed
#   - updated script to openssl v1.0.x
#   - applied compatibility patch so that old hashes can optionally be created:
#     http://rt.openssl.org/Ticket/Display.html?id=2272&user=guest&pass=guest
#   - modified script so that old hashes are created by default too.
#
# 2011-04-01 jed
#   - fixed error when creating crl hashes.
#
# 2015-01-20 jed
#   - delete a hash target before a new one is created to make sure symbolic
#     links are used and no files are copied.
#
# 2015-03-07 jed
#   - search path for openssl binary to prevent problems if a directory with
#     the same name exists in the current directory.

my $dir = "/etc/ssl";
my $prefix = "/usr";

my $openssl;
if(defined $ENV{OPENSSL}) {
    $openssl = $ENV{OPENSSL};
} else {
    use File::Which;                  # exports which()
    use File::Which qw(which where);  # exports which() and where()
    $openssl = which('openssl');

    if ($openssl eq "") {
        $openssl = "openssl";
    }

    $ENV{OPENSSL} = $openssl;
}

my $pwd;
eval "require Cwd";
if (defined(&Cwd::getcwd)) {
    $pwd=Cwd::getcwd();
} else {
    $pwd=`pwd`; chomp($pwd);
}
my $path_delim = ($pwd =~ /^[a-z]\:/i) ? ';' : ':'; # DOS/Win32 or Unix delimiter?

$ENV{PATH} = "$prefix/bin" . ($ENV{PATH} ? $path_delim . $ENV{PATH} : ""); # prefix our path

if(! -x $openssl) {
    my $found = 0;
    foreach (split /$path_delim/, $ENV{PATH}) {
        if(-x "$_/$openssl") {
            $found = 1;
            $openssl = "$_/$openssl";
            last;
        }
    }
    if($found == 0) {
        print STDERR "c_rehash: rehashing skipped ('openssl' program not available)\n";
        exit 0;
    }
}

# 2011-03-06 jed
my $compat_old;
my $compat_testresult;
$compat_testresult = `$openssl x509 --help 2>&1`;
if($compat_testresult =~ /-subject_hash_old/){
    $compat_old = 1;
} else {
    $compat_old = 0;
}

if(@ARGV) {
    @dirlist = @ARGV;
} elsif($ENV{SSL_CERT_DIR}) {
    @dirlist = split /$path_delim/, $ENV{SSL_CERT_DIR};
} else {
    $dirlist[0] = "$dir/certs";
}

if (-d $dirlist[0]) {
    chdir $dirlist[0];
    $openssl="$pwd/$openssl" if (!-x $openssl);
    chdir $pwd;
}

foreach (@dirlist) {
    if(-d $_ and -w $_) {
        hash_dir($_);
    }
}

sub hash_dir {
    my %hashlist;
    print "Doing $_[0]\n";
    chdir $_[0];
    opendir(DIR, ".");
    my @flist = readdir(DIR);
    # Delete any existing symbolic links
    foreach (grep {/^[\da-f]+\.r{0,1}\d+$/} @flist) {
        if(-l $_) {
            unlink $_;
        }
    }
    closedir DIR;
    FILE: foreach $fname (grep {/\.pem$/} @flist) {
        # Check to see if certificates and/or CRLs present.
        my ($cert, $crl) = check_file($fname);
        if(!$cert && !$crl) {
            print STDERR "WARNING: $fname does not contain a certificate or CRL: skipping\n";
            next;
        }
        if ($cert) {
            print "Creation of old hashes activated!\n" if $compat_old;
            link_hash_cert('-subject_hash', $fname);
            link_hash_cert('-subject_hash_old', $fname) if $compat_old;
        }
        if ($crl) {
            # 2011-04-01 jed
            link_hash_crl('-hash', $fname);
        }
    }
}

sub check_file {
    my ($is_cert, $is_crl) = (0,0);
    my $fname = $_[0];
    open IN, $fname;
    while(<IN>) {
        if(/^-----BEGIN (.*)-----/) {
            my $hdr = $1;
            if($hdr =~ /^(X509 |TRUSTED |)CERTIFICATE$/) {
                $is_cert = 1;
                last if($is_crl);
            } elsif($hdr eq "X509 CRL") {
                $is_crl = 1;
                last if($is_cert);
            }
        }
    }
    close IN;
    return ($is_cert, $is_crl);
}


# Link a certificate to its subject name hash value, each hash is of
# the form <hash>.<n> where n is an integer. If the hash value already exists
# then we need to up the value of n, unless its a duplicate in which
# case we skip the link. We check for duplicates by comparing the
# certificate fingerprints

sub link_hash_cert {
    my $hash = shift @_;
    my $fname = $_[0];
    $fname =~ s/'/'\\''/g;
    my ($hash, $fprint) = `"$openssl" x509 $hash -fingerprint -noout -in "$fname"`;
    chomp $hash;
    chomp $fprint;
    $fprint =~ s/^.*=//;
    $fprint =~ tr/://d;
    my $suffix = 0;
    # Search for an unused hash filename
    while(exists $hashlist{"$hash.$suffix"}) {
        # Hash matches: if fingerprint matches its a duplicate cert
        if($hashlist{"$hash.$suffix"} eq $fprint) {
            print STDERR "WARNING: Skipping duplicate certificate $fname\n";
            return;
        }
        $suffix++;
    }
    $hash .= ".$suffix";
    print "$fname => $hash\n";
    $symlink_exists=eval {symlink("",""); 1};
    if ($symlink_exists) {
        if (-e $hash) {
            # delete symlink/file if it exists
            unlink($hash);
        }
        symlink $fname, $hash;
    } else {
        open IN,"<$fname" or die "can't open $fname for read";
        open OUT,">$hash" or die "can't open $hash for write";
        print OUT <IN>; # does the job for small text files
        close OUT;
        close IN;
    }
    $hashlist{$hash} = $fprint;
}

# Same as above except for a CRL. CRL links are of the form <hash>.r<n>

sub link_hash_crl {
    my $hash = shift @_;
    my $fname = $_[0];
    $fname =~ s/'/'\\''/g;
    my ($hash, $fprint) = `"$openssl" crl $hash -fingerprint -noout -in '$fname'`;
    chomp $hash;
    chomp $fprint;
    $fprint =~ s/^.*=//;
    $fprint =~ tr/://d;
    my $suffix = 0;
    # Search for an unused hash filename
    while(exists $hashlist{"$hash.r$suffix"}) {
        # Hash matches: if fingerprint matches its a duplicate cert
        if($hashlist{"$hash.r$suffix"} eq $fprint) {
            print STDERR "WARNING: Skipping duplicate CRL $fname\n";
            return;
        }
        $suffix++;
    }
    $hash .= ".r$suffix";
    print "$fname => $hash\n";
    $symlink_exists=eval {symlink("",""); 1};
    if ($symlink_exists) {
        if (-e $hash) {
            # delete symlink/file if it exists
            unlink($hash);
        }
        symlink $fname, $hash;
    } else {
        system ("cp", $fname, $hash);
    }
    $hashlist{$hash} = $fprint;
}
