#! /usr/bin/perl # Copyright (C) 2008 Nick Urbanik # 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 <', $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 <{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': $!";