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