#!/usr/bin/ruby
# ---------------------------------------------------------------------------
# Copyright (C) 2009 PGP Corporation
# All rights reserved.
#
# Certificate provisioning script for PGP Command Line with KMS
#
# $Id$
# ---------------------------------------------------------------------------
# --------------------------------------------------
# Config
# --------------------------------------------------
HTTPD_CONF='/etc/httpd/conf.d/*'
PGP='/opt/pgp/bin/pgp'
USP_SERVER='keys.senderdomain.com'
PGP_HOME="#{ENV['HOME']}/.pgp-demo"
TEMPDIR="#{PGP_HOME}/tmp"
PREFS_FILE="#{ENV['HOME']}/.pgp/PGPprefs.xml"
CERT_MIN_LIFETIME_DAYS=2
KEY_TYPE='RSA'
KEY_LENGTH=2048
VERBOSE=false
DEBUG=false
HTTPD_RESTART="/etc/init.d/httpd restart"
# --------------------------------------------------
# Dependencies
# --------------------------------------------------
require 'fileutils'
# --------------------------------------------------
# Non-configurable constants
# --------------------------------------------------
DATE_REGEX='([0-9][0-9][0-9][0-9])-([01][0-9])-([0-3][0-9])[ \t]*$'
# --------------------------------------------------
# Utility functions
# --------------------------------------------------
def dprint(string)
print "#{string}" if DEBUG
end
def vprint(string)
print "#{string}" if VERBOSE
end
def lprint(string)
print "#{string}"
end
def pgp(string)
str=`#{PGP} #{string} 2>&1`
if $?.exitstatus != 0 then
lprint "***pgp invocation failed!\n"
lprint "Executed '#{PGP} #{string}'\n"
lprint "Exit status #{$?.exitstatus}\n"
lprint "------------------------------------------------------------\n"
lprint " PGP Output:\n"
lprint "------------------------------------------------------------\n"
lprint "#{str}\n"
lprint "------------------------------------------------------------\n"
Process.exit $?.exitstatus
else
dprint "Executed '#{PGP} #{string}'\n"
dprint "------------------------------------------------------------\n"
dprint " PGP Output:\n"
dprint "------------------------------------------------------------\n"
dprint "#{str}\n"
dprint "------------------------------------------------------------\n"
end
return str
end
# --------------------------------------------------
# Basic objects
# --------------------------------------------------
class VirtualHost
attr_accessor :filename, :name, :cert_file, :key_file, :cert_chain#,:alt_names
#
# Should be called with a string consisting of a block like
#
#
# ...
#
#
def VirtualHost.from_conf_string(filename, str)
vhost = new
vhost.filename = filename
str.each do |line|
if /^[ \t]*SSLCertificateFile[ \t]+(.*)$/ =~ "#{line}"; then
vhost.cert_file = $1
end
if /^[ \t]*SSLCertificateKeyFile[ \t]+(.*)$/ =~ "#{line}"; then
vhost.key_file=$1
end
if /^[ \t]*ServerName[ \t]+([^:\n \t]+)/ =~ "#{line}"; then
vhost.name=$1
end
if /^[ \t]*SSLCertificateChainFile[ \t]+(.*)$/ =~ "#{line}"; then
vhost.cert_chain=$1
end
end
if(vhost.name.nil?) then
lprint("*** Configuration error in #{vhost.filename}:\n" +
"VirtualHost without a ServerName.\n")
return nil;
end
if(vhost.cert_file.nil?) then
return nil
end
if(vhost.key_file.nil?) then
lprint("*** Configuration error in #{vhost.filename}:\n" +
"SSLCertificateFile present, but no SSLCertificateKeyFile\n")
return nil
end
return vhost
end
def check_certificate_chain
return true
end
#
# Validity
#
def valid?
if ! FileTest.exists?(@cert_file) then
vprint "Cert file #{@cert_file} does not exist.\n"
return false
end
if ! FileTest.exists?(@key_file) then
vprint "Key file #{@key_file} does not exist.\n"
return false
end
if(@name.nil?)
vprint "No CN configured.\n"
return false
end
# Import cert to check validity
str = pgp "--import #{@cert_file}"
if /key imported as (0x[a-fA-F0-9]+)/ =~ "#{str}" then
cert_id = $1
else
lprint "Failed to import certificate!\n"
Process.exit 1
end
expiration = pgp "--list-sig-details #{cert_id}"
expiration.each do |line|
if /^[ \t]*Expires: #{DATE_REGEX}/ =~ "#{line}"; then
expiration = Time.mktime($1, $2, $3, 0, 0, 0)
dprint "Certificate in #{@cert_file} will expire on #{expiration}\n"
if(expiration <= (Time.now + (CERT_MIN_LIFETIME_DAYS * 24 * 60 * 60)))
vprint "Certificate in #{@cert_file} vaild for less than #{CERT_MIN_LIFETIME_DAYS} days, renewing.\n"
return false
end
end
end
if(!@cert_chain.nil? && !check_certificate_chain)
vprint "Certificate chain is not valid\n"
return false
end
# XXX - check expiration
return true
end
#
# Cenerate new cert, and place the cert and the key files in their
# corresponding location
#
def create_managed_cert
#
# Generate keypair
#
line = pgp "--gen-key --passphrase '' --key-type #{KEY_TYPE} \
--bits #{KEY_LENGTH} #{@name} --expiration-days 14"
if /^(0x[0-9A-F]+):generate key \(0:key successfully generated\)/ =~ line
then
key_id = $1
dprint "Key id: #{key_id}\n"
else
lprint "*** Failed to generate cert.\n"
return nil
end
#
# Export to file
#
pgp "--export --armor -o #{TEMPDIR}/#{@name}.pub.asc \
--overwrite remove #{key_id}"
#
# Create MAK from public key
#
line = pgp "--usp-server #{USP_SERVER} --import-mak \
--name \"#{@name}\" #{TEMPDIR}/#{@name}.pub.asc"
if /mported as ([^) ]+)/ =~ "#{line}" then
uuid = $1
else
return nil
end
# Export cert from USP server
pgp "--usp-server #{USP_SERVER} --export-mak \
--export-format x509-cert -o #{TEMPDIR}/#{@name}.crt \
--overwrite remove #{uuid}"
# Import cert into keyring, so we can convert it to pkcs #8
pgp "--import #{TEMPDIR}/#{@name}.crt"
pgp "--export --export-format pkcs8 -o #{TEMPDIR}/#{@name}.p8\
--overwrite remove #{key_id}"
#
# Move cert and key into place
#
FileUtils.mv("#{TEMPDIR}/#{@name}.crt", "#{@cert_file}")
FileUtils.mv("#{TEMPDIR}/#{@name}.p8", "#{@key_file}")
File.delete "#{TEMPDIR}/#{@name}.pub.asc"
return true
end
def to_s
"CN: #{@name}\n" "Key file: #{@key_file}\n" "Cert file: #{@cert_file}\n\n"
end
end
# --------------------------------------------------
# Main program
# --------------------------------------------------
def init
#
# This code is written using shell invocations instead of native ruby calls.
# Ruby versions 1.8.6 and below did not have good support for atomic creation
# of directories, and just testing for the directory first, and then creating
# it if it does not exist, is a security risk.
#
`rm -rf #{PGP_HOME}`
mkdir_out=`mkdir #{PGP_HOME} 2>&1`
if($?.exitstatus != 0) then
lprint "***error creating temporary PGP_HOME in #{PGP_HOME}:\n";
lprint " #{mkdir_out}"
lprint "Exiting.\n"
throw "initialization failed"
end
`cp #{PREFS_FILE} #{PGP_HOME}`
mkdir_out=`mkdir #{TEMPDIR}`
if($?.exitstatus != 0) then
lprint "***error creating #{TMP_DIR}:\n";
lprint " #{mkdir_out}"
lprint "Exiting.\n"
throw "initialization failed"
end
ENV['PGP_HOME_DIR'] = PGP_HOME
pgp("--create-keyrings")
end
def cleanup
`rm -rf #{PGP_HOME}`
end
begin
need_restart = false
init;
# For each apache config file (some Linux disistributions, e.g., Debian based
# distributions, usually split up virtual hosts in one per file)
Dir["#{HTTPD_CONF}"].each do |filename|
# Skip directories and special files
if(File.ftype(filename) == "file" || File.ftype(filename) == "link") then
str = File.read("#{filename}")
# There can be multiple virtual hosts per file.
# Each starts with
host_strs = str.split(/^[ \t]*\ 1 then
# Throw away the first string which contains everything before the first
#
host_strs[1..-1].each do |host_str|
# Technically, host_str can now contain extra information after
# , but we don't worry about it, as anything useful we
# might find there would be a syntax error for apache anyway
vhost = VirtualHost.from_conf_string(filename, host_str)
if(vhost) then
# We found a valid configuration. We don't yet know if it has an
# existing or valid certificate
if(vhost.valid?) then
vprint "[#{filename}]: Verified #{vhost.name}\n"
else
vhost.create_managed_cert
lprint "[#{filename}]: Generated new certificate and " +
"key for #{vhost.name}\n"
need_restart = true
end
dprint "#{vhost}\n"
end
end
end # host_strs.length
end # File.ftype
end # Dir.each
if(need_restart) then
dprint "Restarting httpd\n"
str=`#{HTTPD_RESTART} 2>&1`
if($?.exitstatus != 0) then
lprint "------------------------------------------------------------\n"
lprint "Unable to restart httpd:\n"
lprint "------------------------------------------------------------\n"
lprint "#{str}\n"
Process.exit $?.exitstatus
end
end
ensure
STDOUT.flush
cleanup
end