#! /usr/bin/perl

# Copyright (C) 2008  Nick Urbanik <nicku@nicku.org>

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

use warnings;
use strict;

use Getopt::Long;
use Readonly;
use Carp;

sub usage {
    ( my $prog = $0 ) =~ s{.*/}{};
    print <<END_USAGE;
$prog --certname=NAME --hostname=HOSTNAME --alt-name=ALT1,ALT2,... --valid-years=YEARS --ca-cert=CA-CERT-FILE --ca-key=CA-KEY-FILE [--passphrase=PASSPHRASE]

NAME is the initial part of the name that the server certificate,
server key and server config will share.

HOSTNAME is the main name that the CN of the server certificate
will be set to.

ALT1, ALT2, ALT3,... is a comma-separated list of alternate names
that this certificate will match.

Can also write this as:
--alt-name=ALT1 --alt-name=ALT2 --atl-name=ALT3

CA-CERT-FILE is the full path name of the CA certificate file.
CA-KEY-FILE  is the full path name of the CA's private key.

PASSPHRASE is to decrypt the CA-KEY-FILE.
For real-life use (as opposed to testing), DO NOT USE THE --passphrase
OPTION.  LET THE PROGRAM PROMPT YOU INSTEAD.
END_USAGE
    exit 1;
}

use Term::ReadKey;
# See Recipe 15.10 in Perl Cookbook, 1st edition, page 529
sub read_passphrase {
    my ( $prompt ) = @_;
    print STDERR $prompt;
    print STDERR "Pass phrase to decrypt CA key: ";
    ReadMode 'noecho';
    my $passphrase = ReadLine 0;
    ReadMode 'restore';
    print STDERR "\n";
    chomp $passphrase;
    return $passphrase;
}

# We pass openssl a filename rather than passphrase so
# other users don't see it with ps:
sub write_passphrase {
    my ( $passphrase ) = @_;
    ( my $fname = $0 ) =~ s{.*/}{};
    $fname .= $$;
    open my $pf_fh, '>', $fname or die "Cannot open '$fname': $!";
    print $pf_fh "$passphrase\n" or die "Cannot write to '$fname': $!";
    close $pf_fh or die "Cannot close '$fname': $!";
    return $fname;
}

# Based on OIE::Utils::x509_cert and /etc/pki/tls/misc/CA.pl
# with guidance from man ca, man openssl, /etc/pki/tls/openssl.cnf.
sub gen_server_cert {
    my ( $certname, $hostname, $ca_cert, $ca_key,
	 $valid_years, $passphrase_file, $alt_ref, $attr ) = @_;
    $attr ||= {};
    $alt_ref ||= [];

    my $servercert = "$certname.crt";
    my $serverkey  = "$certname.key";
    my $serverconfig = "./$certname.config";
    my $certrequest  = "$certname-req.pem";

    # Always put the hostname in subjectAltName as well as commonName.
    # CN is deprecated in favour of subjectAltName: see page 134
    # of "Network Security with OpenSSL", 2002.
    unshift @$alt_ref, $hostname unless grep { $_ eq $hostname } @$alt_ref;

    my $alt_name_conf
	= @$alt_ref
	? "subjectAltName           = "
	    . join( q{,}, map { "DNS:$_" } @$alt_ref )
	: q{};

    defined $certname and defined $hostname and defined $ca_cert
	and defined $ca_key and defined $valid_years
	and ref $alt_ref eq 'ARRAY' and ref $attr eq 'HASH'
	or croak "Usage: gen_server_cert( \$certname, \$hostname, \$ca_cert, ",
	    "\$ca_key, \$validity, \\\@alt_ref, \\\%attr )";

    $attr->{C}        ||= 'AU';
    $attr->{ST}       ||= 'New South Wales';
    $attr->{L}        ||= 'Sydney';
    $attr->{O}        ||= 'Nick Urbanik';
    $attr->{OU}       ||= 'nicku.org';
    $attr->{CN}       ||= $hostname;
    $attr->{validity} ||= $valid_years * 365;
    $attr->{emailAddress} ||= 'nicku@nicku.org';

    open my $cfg_fh, '>', $serverconfig
	or die "Cannot open $serverconfig for writing: $!";
    print $cfg_fh <<END_CONF or die "Failed to write config: $!";
HOME                     = .
RANDFILE                 = \$HOME/.rnd

[req]
prompt                   = no
distinguished_name       = req_dn
x509_extensions		 = req_extensions
default_bits		 = 1024
default_md		 = sha1

[req_dn]
C                        = $attr->{C}
ST                       = $attr->{ST}
L                        = $attr->{L}
O                        = $attr->{O}
OU                       = $attr->{OU}
CN                       = $attr->{CN}
#emailAddress             = $attr->{emailAddress}

[req_extensions]
basicConstraints         = CA:false
subjectKeyIdentifier     = hash
authorityKeyIdentifier   = keyid,issuer:always
$alt_name_conf
END_CONF
    close $cfg_fh or die "Cannot close $serverconfig: $!";

    $ENV{"SSLEAY_CONFIG"} = $serverconfig;

    my @cmd_req
	= (
	    qw( /usr/bin/openssl req -newkey rsa:1024 -nodes ),
	    -keyout => $serverkey,
	    -out => $certrequest,
	    -config => "$serverconfig",
	);
    system( @cmd_req ) == 0 or die "system @cmd_req failed: $?\n";

    print "Have created $serverkey and $certrequest\n";

    my @cmd_cert
	= (
	    qw( /usr/bin/openssl x509 -req ),
	    -extfile => $serverconfig,
	    -in => $certrequest,
	    -CA => $ca_cert,
	    -CAkey => $ca_key,
	    #'-CAcreateserial',
	    -set_serial => time,
	    -extensions => 'req_extensions',
	    -passin => "file:$passphrase_file",
	    -days => $attr->{validity},
	    -out => $servercert,
	);

    system( @cmd_cert ) == 0 or die "system @cmd_cert failed: $?\n";
}

my ( $hostname, $certname, $ca_cert, $ca_key, $valid_years, $passphrase, @altnames );

GetOptions(
    'hostname=s'    => \$hostname,
    'certname=s'    => \$certname,
    'alt-name=s'    => \@altnames,
    'ca-cert=s'     => \$ca_cert,
    'ca-key=s'      => \$ca_key,
    'valid-years=i' => \$valid_years,
    'passphrase=s'  => \$passphrase,
    help => sub { usage },
) or usage;

usage unless $valid_years and $hostname and $ca_cert and $ca_key and $certname;

warn "Cannot read CA certificate '$ca_cert'\n" and usage unless -r $ca_cert;
warn "Cannot read CA key '$ca_key'\n" and usage unless -r $ca_key;
@altnames = split /,/, join q{,}, @altnames;

$passphrase = read_passphrase 'Pass phrase to decrypt CA key: ' unless $passphrase;
warn "Need CA key passphrase\n" and usage unless $passphrase;

$SIG{$_} = 'IGNORE' foreach qw( INT QUIT PIPE HUP );
my $passphrase_file = write_passphrase $passphrase;
$SIG{__DIE__} = sub { unlink $passphrase_file; die @_ };

gen_server_cert $certname, $hostname, $ca_cert, $ca_key, $valid_years, $passphrase_file, \@altnames;

unlink $passphrase_file or die "Cannot unlink '$passphrase_file': $!";
