#! /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;

Readonly my $BASE_NAME => 'nicku-ca';
Readonly my $CERTNAME => "$BASE_NAME.crt";
Readonly my $KEYNAME => "$BASE_NAME.key";
Readonly my $CONFNAME => "$BASE_NAME.config";
Readonly my $CAREQNAME => "$BASE_NAME-req.pem";
Readonly my $DIRMODE => 0777;

sub usage {
    ( my $prog = $0 ) =~ s{.*/}{};
    print <<END_USAGE;
$prog --valid-years=YEARS  [--passphrase=PASSPHRASE]

Prompts once for a passphrase and reads it if one is not provided
on the command line.
For real-life use (as opposed to testing), DO NOT USE THE --passphrase
OPTION.  LET THE PROGRAM PROMPT YOU INSTEAD.

DANGER!  OVERWRITES MOST OF WHAT WAS HERE BEFORE!
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;
    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;
}

sub create_directories {
    # This directory structure is required for the ca command:
    foreach my $dir qw( certs crl newcerts private ) {
	-d $dir or mkdir $dir, $DIRMODE or die "Cannot make directory '$dir': $!";
    }
    foreach my $file qw( index.txt index.txt.attr ) {
	open my $out, '>', $file or die "cannot create $file: $!";
	close $out;
    }
    open my $out, '>', 'crlnumber' or die "Cannot create crlnumber: $!";
    print $out "01\n";
    close $out;

    # We want the serial numbers to always increase: use time():
    open $out, '>', 'serial' or die "Cannot create file 'serial': $!";
    printf $out "%X\n", time;
    close $out;
}

# 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.
# Note that gpgsm insists that the issuer and subject dns are identical,
# so that having an Email field in the Issuer but not in the Subject
# DN will cause gpgsm to reject that CA file and refuse to import it.

sub gen_ca_cert {
    my ( $valid_years, $passphrase_file, $attr ) = @_;
    $attr ||= {};

    defined  $valid_years and defined $passphrase_file and ref $attr eq 'HASH'
		or croak "Usage: gen_ca_cert( \$valid_years, ",
		    "\$passphrase_file, \%attr )";

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

    create_directories;

    open my $cfg_fh, '>', $CONFNAME
	or die "Cannot open $CONFNAME for writing: $!";
    print $cfg_fh <<END_CONF;
HOME                     = .
RANDFILE                 = \$HOME/private/.rnd

[req]
prompt                   = no
distinguished_name       = req_dn
default_bits		 = 2048
default_md		 = sha1

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

[ca]
default_ca               = ca_default

[ca_default]
C                        = $attr->{C}
ST                       = $attr->{ST}
L                        = $attr->{L}
O                        = $attr->{O}
OU                       = $attr->{OU}
CN                       = $attr->{CN}

dir                      = .
database                 = \$dir/index.txt
new_certs_dir            = \$dir/newcerts
unique_subject	         = no
email_in_dn              = no

certificate              = \$dir/$CERTNAME
serial                   = \$dir/serial
private_key              = \$dir/private/$KEYNAME

default_md               = sha1
policy		         = policy_match

name_opt 	         = ca_default
cert_opt 	         = ca_default

[ policy_match ]
countryName		 = match
stateOrProvinceName	 = match
organizationName	 = match
organizationalUnitName	 = optional
commonName		 = supplied
emailAddress		 = optional

[ v3_ca ]
subjectKeyIdentifier     = hash
authorityKeyIdentifier   = keyid:always,issuer:always
basicConstraints         = CA:true
END_CONF
    close $cfg_fh or die "Cannot close $CONFNAME: $!";

    #$ENV{"SSLEAY_CONFIG"} = "./$CONFNAME";

    my @cmd_req
	= (
	    qw( /usr/bin/openssl req -new ),
	    -keyout => "private/$KEYNAME", -out => $CAREQNAME,
	    -config => "./$CONFNAME", -passout => "file:$passphrase_file",
	);
    system( @cmd_req ) == 0 or die "system @cmd_req failed: $?\n";

    print "Have created private/$KEYNAME and $CAREQNAME\n";

    # Note: -infiles must go last; everything after is an argument to it.
    my @cmd_ca
	= (
	    qw( /usr/bin/openssl ca -create_serial -batch -selfsign
		-extensions v3_ca ),
	    -config => "./$CONFNAME",
	    -out => $CERTNAME, -days => $attr->{validity},
	    -keyfile => "private/$KEYNAME",
	    -passin => "file:$passphrase_file",
	    -infiles => $CAREQNAME,
	);
    system( @cmd_ca ) == 0 or die "system @cmd_ca failed: $?\n";
}

my ( $valid_years, $passphrase );

GetOptions(
    'valid-years=i' => \$valid_years,
    'passphrase=s' => \$passphrase,
    help => sub { usage },
) or usage;


usage unless $valid_years;
if ( not $passphrase ) {
    my $pass2 = q{};
    do {
	$passphrase = read_passphrase 'Pass phrase: ';
	$pass2      = read_passphrase 'Pass phrase again: ';
    } while ( $passphrase ne $pass2 );
}
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_ca_cert $valid_years, $passphrase_file;

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