#!/usr/bin/perl # # Wrapper around the ca to make it easier to use # use strict; use warnings; use File::Copy; our ($ME) = ($0 =~ m|/([^/]+)$|); our $openssl = "openssl"; if(defined $ENV{'OPENSSL'}) { $openssl = $ENV{'OPENSSL'}; } else { $ENV{'OPENSSL'} = $openssl; } our $verbose = 1; our $OPENSSL_CONFIG = defined $ENV{'OPENSSL_CONFIG'} ? $ENV{'OPENSSL_CONFIG'} : ""; our $OPENSSL_CONFIG_WEBCLIENT = "-config /etc/pki/tls/openssl-webclient.cnf"; our $CERTTOP = defined $ENV{'CERTTOP'} ? $ENV{'CERTTOP'} : "/etc/pki/webclient"; our $DAYS = "-days 730"; our $CADAYS = "-days 3650"; our $REQ = "$openssl req"; our $CA = "$openssl ca"; our $VERIFY = "$openssl verify"; our $X509 = "$openssl x509"; our $PKCS12 = "$openssl pkcs12"; our $CNAMETXT = "My Certificate"; our $PASSOPT; # default openssl.cnf file has setup as per the following our $CATOP = defined $ENV{'CATOP'} ? $ENV{'CATOP'} : "/etc/pki/CA"; our $CAKEY = "cakey.pem"; our $CAREQ = "careq.pem"; our $CACERT = "cacert.pem"; our $CACRL = "crl.pem"; our $DIRMODE = 0755; our $NEWKEY = "newkey.key"; our $NEWREQ = "newreq.pem"; our $NEWCERT = "newcert.pem"; our $NEWP12 = "newcert.p12"; our $RET = 0; our $WHAT = shift @ARGV; our $FILE; sub help { print STDERR "usage: $ME -newcert|-newreq|-newreq-nodes|-newca|-sign|-verify\n"; print STDERR " $ME -pkcs12 [certname]\n"; print STDERR " $ME -crl|-revoke cert-filename [reason]\n"; print STDERR " $ME -newwebclient client-name [certname]\n"; print STDERR " $ME -revoke-webclient client-name [reason]\n"; } # See if reason for a CRL entry is valid; exit if not. sub crl_reason_ok { my $r = shift; if ($r eq 'unspecified' || $r eq 'keyCompromise' || $r eq 'CACompromise' || $r eq 'affiliationChanged' || $r eq 'superseded' || $r eq 'cessationOfOperation' || $r eq 'certificateHold' || $r eq 'removeFromCRL') { return 1; } print STDERR "Invalid CRL reason; must be one of:\n"; print STDERR " unspecified, keyCompromise, CACompromise,\n"; print STDERR " affiliationChanged, superseded, cessationOfOperation\n"; print STDERR " certificateHold, removeFromCRL"; exit 1; } # Copy a PEM-format file; return like exit status (zero means ok) sub copy_pemfile { my ($infile, $outfile, $bound) = @_; my $found = 0; open IN, $infile || die "Cannot open $infile, $!"; open OUT, ">$outfile" || die "Cannot write to $outfile, $!"; while () { $found = 1 if /^-----BEGIN.*$bound/; print OUT $_ if $found; $found = 2, last if /^-----END.*$bound/; } close IN; close OUT; return $found == 2 ? 0 : 1; } # Wrapper around system; useful for debugging. Returns just the exit status sub run { my $cmd = shift; print "====\n$cmd\n" if $verbose; my $status = system($cmd); print "==> $status\n====\n" if $verbose; return $status >> 8; } # Create a certificate sub newcert { $RET = run("$REQ $OPENSSL_CONFIG -new -x509" . " -keyout $NEWKEY -out $NEWCERT $DAYS"); print "Cert is in $NEWCERT, private key is in $NEWKEY\n" if $RET == 0; } # Create a certificate request along with a private key sub newreq { $RET = run("$REQ $OPENSSL_CONFIG -new -keyout $NEWKEY -out $NEWREQ $DAYS"); print "Request is in $NEWREQ, private key is in $NEWKEY\n" if $RET == 0; } # Create a certificate request with an unencrypted key sub newreq_nodes { $RET = run("$REQ $OPENSSL_CONFIG -new -nodes" . " -keyout $NEWKEY -out $NEWREQ $DAYS"); print "Request is in $NEWREQ, private key is in $NEWKEY\n" if $RET == 0; } # Build a certificate authority(CA) sub newca { # create the directory hierarchy unless (-d "$CATOP") { mkdir ${CATOP}, $DIRMODE; mkdir "${CATOP}/certs", $DIRMODE; mkdir "${CATOP}/crl", $DIRMODE ; mkdir "${CATOP}/newcerts", $DIRMODE; mkdir "${CATOP}/private", $DIRMODE; } open OUT, ">${CATOP}/index.txt"; close OUT; open OUT, ">${CATOP}/crlnumber"; print OUT "01\n"; close OUT; # ask user for existing CA certificate print "CA certificate filename (or enter to create)\n"; $FILE = "" unless defined($FILE = ); $FILE =~ s{\R$}{}; if ($FILE ne "") { copy_pemfile($FILE,"${CATOP}/private/$CAKEY", "PRIVATE"); copy_pemfile($FILE,"${CATOP}/$CACERT", "CERTIFICATE"); } else { print "Making CA certificate ...\n"; $RET = run("$REQ $OPENSSL_CONFIG -new -keyout" . " ${CATOP}/private/$CAKEY" . " -out ${CATOP}/$CAREQ"); $RET = run("$CA $OPENSSL_CONFIG -create_serial" . " -out ${CATOP}/$CACERT $CADAYS -batch" . " -keyfile ${CATOP}/private/$CAKEY -selfsign" . " -extensions v3_ca" . " -infiles ${CATOP}/$CAREQ") if $RET == 0; print "CA certificate is in ${CATOP}/$CACERT\n" if $RET == 0; } } # Create a PKCS#12 file from the signed cert and key sub pkcs12 { my $cname = shift; $cname = "$CNAMETXT" unless $cname; $RET = run("$PKCS12 -in $NEWCERT -inkey $NEWKEY" . " -certfile ${CATOP}/$CACERT" . " -out $NEWP12 $PASSOPT" . " -export -name \"$cname\""); print "PKCS#12 file is in $NEWP12\n" if $RET == 0; } # Sign a certificate based on a certificate request sub xsign { $RET = run("$CA $OPENSSL_CONFIG -policy policy_anything" . " -infiles $NEWREQ"); } # Same as above except the certificate is writen to STDOUT sub sign { $RET = run("$CA $OPENSSL_CONFIG -policy policy_anything" . " -out $NEWCERT -infiles $NEWREQ"); print "Signed certificate is in $NEWCERT\n" if $RET == 0; } # Same as sign except it uses configuration file section v3_ca sub signCA { $RET = run("$CA $OPENSSL_CONFIG -policy policy_anything" . " -out $NEWCERT -extensions v3_ca -infiles $NEWREQ"); print "Signed CA certificate is in $NEWCERT\n" if $RET == 0; } # Same as sign except it expects a self signed certificate present in newreq.pem sub signcert { $RET = run("$X509 -x509toreq -in $NEWREQ -signkey $NEWREQ" . " -out tmp.pem"); $RET = run("$CA $OPENSSL_CONFIG -policy policy_anything -out $NEWCERT" . " -infiles tmp.pem") if $RET == 0; print "Signed certificate is in $NEWCERT\n" if $RET == 0; } # Verify certificates against CA certificate for CATOP sub verify { my @files = @ARGV ? @ARGV : ( $NEWCERT ); my $file; foreach $file (@files) { my $status = run("$VERIFY \"-CAfile\" ${CATOP}/$CACERT $file"); $RET = $status if $status != 0; } } # Create a cert/key pair and PKCS#12 sub newwebclient { $OPENSSL_CONFIG = $OPENSSL_CONFIG_WEBCLIENT unless $OPENSSL_CONFIG; my ($cname,$cnametxt) = @_; unless ($cname) { print STDERR "Certificate name is required.\n"; exit 1; } $cnametxt = "$CNAMETXT" unless $cnametxt; # create the directory hierarchy unless (-d "$CERTTOP") { mkdir ${CERTTOP}, $DIRMODE; mkdir "${CERTTOP}/certs", $DIRMODE; mkdir "${CERTTOP}/newcerts", $DIRMODE; mkdir "${CERTTOP}/private", $DIRMODE; } if (-f "${CERTTOP}/certs/${cname}.pem") { print STDERR "Certificate file ${cname}.pem already exists!\n" . "Please run \'$ME -revoke-webclient $cname\' and then try again\n"; exit 1; } chdir "${CERTTOP}/newcerts"; # create a certificate request with a private key newreq_nodes(); if ($RET != 0) { print STDERR "Request (and private key) generation failed\n"; exit 1; } $RET = run("$CA $OPENSSL_CONFIG -out $NEWCERT $DAYS" . " -infiles $NEWREQ"); if ($RET != 0 || ! -f $NEWCERT || -z $NEWCERT) { print STDERR "Signing of certificate failed\n"; exit 1; } print "Signed certificate is in $NEWCERT\n"; if (-f "${CERTTOP}/private/exportpw") { $PASSOPT="-passout file:${CERTTOP}/private/exportpw"; } # create a PKCS#12 file from the signed cert and key pkcs12($cnametxt); if ($RET != 0) { print STDERR "Creation of PKCS#12 failed\n"; exit 1; } # tidy up the keys move($NEWP12,"${CERTTOP}/private/${cname}.p12"); move($NEWKEY,"${CERTTOP}/private/${cname}.key"); move($NEWCERT,"${CERTTOP}/certs/${cname}.pem"); unlink $NEWREQ; my $msg = <<"EOM"; Keys generated successfully: ${CERTTOP}/certs/${cname}.pem public key(signed) ${CERTTOP}/private/${cname}.key private key ${CERTTOP}/private/${cname}.p12 PKCS#12(the bag of above two) ${CATOP}/cacert.crt Private-CA public key EOM print $msg. "\n"; } # Update Certificate Revocation List sub gencrl { $RET = run("$CA $OPENSSL_CONFIG -gencrl -out ${CATOP}/crl/$CACRL"); if ($RET != 0) { print STDERR "Failed to Generated CRL\n"; exit 1; } print "Generated CRL is in ${CATOP}/crl/$CACRL\n"; # Place what to do after CRL update, e.g. # $RET = run("cd ${CERTTOP} && make"); } # Revoke a certificate sub revoke { # revoke a certificate my ($cname,$reason) = @_; unless ($cname) { print "Certificate filename is required; reason optional.\n"; exit 1; } $reason = "" unless defined $reason; if (length($reason) gt 0 && crl_reason_ok($reason)) { $reason = " -crl_reason $reason" } unless (-f "$cname") { print STDERR "No such file $cname\n"; exit 1; } $RET = run("$CA $OPENSSL_CONFIG -revoke \"$cname\"" . $reason); if ($RET != 0) { print STDERR "Failed to revoke $cname\n"; exit 1; } } # Revoke and delete a set of cert, key and PKCS#12 files sub revoke_webclient { $OPENSSL_CONFIG = $OPENSSL_CONFIG_WEBCLIENT unless $OPENSSL_CONFIG; my ($cname,$reason) = @_; unless ($cname) { print "Certificate name is required; reason optional.\n"; exit 1; } $reason = "" unless defined $reason; unless (-f "${CERTTOP}/certs/${cname}.pem") { print STDERR "No such file ${cname}.pem\n"; exit 1; } revoke("${CERTTOP}/certs/${cname}.pem",$reason); # remove the revoked key set run("rm -i ${CERTTOP}/certs/${cname}.pem"); run("rm -i ${CERTTOP}/private/${cname}.key"); run("rm -i ${CERTTOP}/private/${cname}.p12"); print "cname $cname successfully revoked.\n"; gencrl(); } unless ($WHAT) { help(); exit 0; } if ( $WHAT =~ /^(-\?|-h|-help)$/ ) { help(); exit 0; } if ($WHAT eq '-newcert' ) { } elsif ($WHAT eq '-newreq' ) { newreq(); } elsif ($WHAT eq '-newreq-nodes' ) { newreq_nodes(); } elsif ($WHAT eq '-newca' ) { newca(); } elsif ($WHAT eq '-pkcs12' ) { pkcs12($ARGV[0]); } elsif ($WHAT eq '-xsign' ) { xsign(); } elsif ($WHAT eq '-sign' ) { sign(); } elsif ($WHAT eq '-signCA' ) { signCA(); } elsif ($WHAT eq '-signcert' ) { signcert(); } elsif ($WHAT eq '-verify' ) { verify(); } elsif ($WHAT eq '-newwebclient' ) { newwebclient($ARGV[0],$ARGV[1]); } elsif ($WHAT eq '-crl' ) { gencrl(); } elsif ($WHAT eq '-revoke' ) { revoke($ARGV[0],$ARGV[1]); } elsif ($WHAT eq '-revoke-webclient' ) { revoke_webclient($ARGV[0],$ARGV[1]); } else { print STDERR "Unknown arg \"$WHAT\"\n"; print STDERR "Use -help for help.\n"; exit 1; } exit $RET;