#!/usr/bin/env ruby

#
# = Synopsis
#
# Stand-alone certificate authority.  Capable of generating certificates
# but mostly meant for signing certificate requests from puppet clients.
#
# = Usage
#
#   puppetca [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose]
#               [-g|--generate] [-l|--list] [-s|--sign] [-r|--revoke]
#               [-p|--print] [-c|--clean] [--verify] [host]
#
# = Description
#
# Because the puppetmasterd daemon defaults to not signing client certificate
# requests, this script is available for signing outstanding requests.  It
# can be used to list outstanding requests and then either sign them individually
# or sign all of them.
#
# = Options
#
# Note that any configuration parameter that's valid in the configuration file
# is also a valid long argument.  For example, 'ssldir' is a valid configuration
# parameter, so you can specify '--ssldir <directory>' as an argument.
#
# See the configuration file documentation at
# http://reductivelabs.com/projects/puppet/reference/configref.html for
# the full list of acceptable parameters. A commented list of all
# configuration options can also be generated by running puppetca with
# '--genconfig'.
#
# all::
#   Operate on all items.  Currently only makes sense with '--sign',
#   '--clean', or '--list'.
#
# clean::
#    Remove all files related to a host from puppetca's storage. This is 
#    useful when rebuilding hosts, since new certificate signing requests
#    will only be honored if puppetca does not have a copy of a signed
#    certificate for that host. The certificate of the host remains valid.
#    If '--all' is specified then all host certificates, both signed and
#    unsigned, will be removed.
#
# debug::
#   Enable full debugging.
#
# generate::
#   Generate a certificate for a named client.  A certificate/keypair will be
#   generated for each client named on the command line.
#
# help::
#   Print this help message
#
# list::
#   List outstanding certificate requests.  If '--all' is specified, 
#   signed certificates are also listed, prefixed by '+'.
#
# print::
#   Print the full-text version of a host's certificate.
#
# revoke::
#   Revoke the certificate of a client. The certificate can be specified
#   either by its serial number, given as a decimal number or a hexadecimal
#   number prefixed by '0x', or by its hostname. The certificate is revoked
#   by adding it to the Certificate Revocation List given by the 'cacrl'
#   config parameter. Note that the puppetmasterd needs to be restarted
#   after revoking certificates.
#
# sign::
#   Sign an outstanding certificate request.  Unless '--all' is specified,
#   hosts must be listed after all flags.
#
# verbose::
#   Enable verbosity.
#
# version::
#   Print the puppet version number and exit.
#
# verify::
#   Verify the named certificate against the local CA certificate.
#
# = Example
#
#   $ puppetca -l
#   culain.madstop.com
#   $ puppetca -s culain.madstop.com
#
# = Author
#
# Luke Kanies
#
# = Copyright
#
# Copyright (c) 2005 Reductive Labs, LLC
# Licensed under the GNU Public License

require 'puppet'
require 'puppet/sslcertificates'
require 'getoptlong'

options = [
    [ "--all",      "-a",  GetoptLong::NO_ARGUMENT ],
    [ "--clean",    "-c",  GetoptLong::NO_ARGUMENT ],
    [ "--debug",    "-d",  GetoptLong::NO_ARGUMENT ],
    [ "--generate", "-g",  GetoptLong::NO_ARGUMENT ],
    [ "--help",     "-h",  GetoptLong::NO_ARGUMENT ],
    [ "--list",     "-l",  GetoptLong::NO_ARGUMENT ],
    [ "--print",    "-p",  GetoptLong::NO_ARGUMENT ],
    [ "--revoke",   "-r",  GetoptLong::NO_ARGUMENT ],
    [ "--sign",     "-s",  GetoptLong::NO_ARGUMENT ],
    [ "--verify",          GetoptLong::NO_ARGUMENT ],
	[ "--version",	"-V",  GetoptLong::NO_ARGUMENT ],
    [ "--verbose",  "-v",  GetoptLong::NO_ARGUMENT ]
]

# Add all of the config parameters as valid options.
Puppet.settings.addargs(options)

result = GetoptLong.new(*options)

mode = nil
all = false
generate = nil

modes = [:clean, :list, :revoke, :generate, :sign, :print, :verify]

begin
    result.each { |opt,arg|
        case opt
            when "--all"
                all = true
            when "--debug"
                Puppet::Util::Log.level = :debug
            when "--generate"
                generate = arg
                mode = :generate
            when "--help"
                if Puppet.features.usage?
                    RDoc::usage && exit
                else
                    puts "No help available unless you have RDoc::usage installed"
                    exit
                end
            when "--list"
                mode = :list
            when "--revoke"
                mode = :revoke
            when "--sign"
                mode = :sign
            when "--version"
                puts "%s" % Puppet.version
                exit
            when "--verbose"
                Puppet::Util::Log.level = :info
            else
                tmp = opt.sub("--", '').to_sym
                if modes.include?(tmp)
                    mode = tmp
                else
                    Puppet.settings.handlearg(opt, arg)
                end
        end
    }
rescue GetoptLong::InvalidOption => detail
    $stderr.puts "Try '#{$0} --help'"
    exit(1)
end

# Now parse the config
Puppet.parse_config

if Puppet.settings.print_configs?
    exit(Puppet.settings.print_configs ? 0 : 1)
end

begin
    ca = Puppet::SSLCertificates::CA.new()
rescue => detail
    if Puppet[:debug]
        puts detail.backtrace
    end
    puts detail.to_s
    exit(23)
end

unless mode
    $stderr.puts "You must specify a mode; see the output from --help"
    exit(12)
end

if [:verify, :print, :generate, :clean, :revoke, :list].include?(mode)
    hosts = ARGV.collect { |h| h.downcase }
end

if [:sign, :list].include?(mode)
    waiting = ca.list
    unless waiting.length > 0 or (mode == :list and all)
        puts "No certificates to sign"
        if ARGV.length > 0
            exit(17)
        else
            exit(0)
        end
    end
end

case mode
when :list
    waiting = ca.list
    if waiting.length > 0
        puts waiting.join("\n")
    end
    if all
        puts ca.list_signed.collect { |cert | cert.sub(/^/,"+ ") }.join("\n")
    end
when :clean
    if hosts.empty? and all == false
        $stderr.puts "You must specify one or more hosts to clean or --all to clean all host certificates"
        exit(24)
    end

    cleaned = false

    if all
        certs = ca.list
        certs |= ca.list_signed
            if certs.empty?
                $stderr.puts "No certificates to clean"
                exit(24)
            end
            certs.each do |c|
                ca.clean(c)
            end
        cleaned = true
    else
        hosts.each do |host|
          
            unless cert = ca.getclientcert(host)[0] || ca.getclientcsr(host)
                $stderr.puts "Could not find client certificate or request for %s" % host
                next
            end           
            
            ca.clean(host)
            cleaned = true
        end
    end
 
    unless cleaned
        exit(27)
    end
when :sign
    to_sign = ARGV.collect { |h| h.downcase }
    unless to_sign.length > 0 or all
        $stderr.puts(
            "You must specify one or more hosts to sign certificates for or --all to sign all certificates"
        )
        exit(24)
    end

    unless all
        to_sign.each { |host|
            unless waiting.include?(host)
                $stderr.puts "No waiting request for %s" % host
            end
        }
        waiting = waiting.find_all { |host|
            to_sign.include?(host)
        }
    end

    waiting.each { |host|
        begin
            csr = ca.getclientcsr(host)
        rescue => detail
            $stderr.puts "Could not retrieve request for %s: %s" % [host, detail]
        end

        begin
            ca.sign(csr)
            $stderr.puts "Signed %s" % host
        rescue => detail
            $stderr.puts "Could not sign request for %s: %s" % [host, detail]
        end

        begin
            ca.removeclientcsr(host)
        rescue => detail
            $stderr.puts "Could not remove request for %s: %s" % [host, detail]
        end
    }
when :generate
    # we need to generate a certificate for a host
    hosts.each { |host|
        puts "Generating certificate for %s" % host
        cert = Puppet::SSLCertificates::Certificate.new(
            :name => host
        )
        cert.mkcsr
        signedcert, cacert = ca.sign(cert.csr)

        cert.cert = signedcert
        cert.cacert = cacert
        cert.write
    }
when :print
    hosts.each { |h|
        cert = ca.getclientcert(h)[0]
        puts cert.to_text
    }
when :revoke
    hosts.each { |h|
        serial = nil
        if h =~ /^0x[0-9a-f]+$/
            serial = h.to_i(16)
        elsif h =~ /^[0-9]+$/
            serial = h.to_i
        else
            cert = ca.getclientcert(h)[0]
            if cert.nil?
                $stderr.puts "Could not find client certificate for %s" % h
            else
                serial = cert.serial
            end
        end
        unless serial.nil?
            ca.revoke(serial)
            puts "Revoked certificate with serial #{serial}"
        end
    }
when :verify
    unless ssl = %x{which openssl}.chomp
        raise "Can't verify certificates without the openssl binary and could not find one"
    end
    success = true

    cacert = Puppet[:localcacert]

    hosts.each do |host|
        print "%s: " % host
        file = ca.host2certfile(host)
        unless FileTest.exist?(file)
            puts "no certificate found"
            success = false
            next
        end


        command = %{#{ssl} verify -CAfile #{cacert} #{file}}
        output = %x{#{command}}
        if $? == 0
            puts "valid"
        else
            puts output
            success = false
        end
    end
else
    $stderr.puts "Invalid mode %s" % mode
    exit(42)
end

