diff options
-rwxr-xr-x | bin/puppetca | 187 | ||||
-rw-r--r-- | lib/puppet/ssl/certificate_authority.rb | 121 | ||||
-rw-r--r-- | lib/puppet/ssl/certificate_revocation_list.rb | 1 | ||||
-rwxr-xr-x | spec/unit/ssl/certificate_authority.rb | 284 | ||||
-rwxr-xr-x | spec/unit/ssl/certificate_revocation_list.rb | 4 | ||||
-rwxr-xr-x | test/executables/puppetca.rb | 115 |
6 files changed, 426 insertions, 286 deletions
diff --git a/bin/puppetca b/bin/puppetca index 3ad896b55..ef29942ae 100755 --- a/bin/puppetca +++ b/bin/puppetca @@ -95,7 +95,7 @@ # Licensed under the GNU Public License require 'puppet' -require 'puppet/sslcertificates' +require 'puppet/ssl/certificate_authority' require 'getoptlong' options = [ @@ -118,22 +118,20 @@ Puppet.settings.addargs(options) result = GetoptLong.new(*options) -mode = nil -all = false -generate = nil +modes = Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS -modes = [:clean, :list, :revoke, :generate, :sign, :print, :verify] +all = false +mode = nil begin result.each { |opt,arg| case opt + when "--clean" + mode = :destroy 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 @@ -141,12 +139,6 @@ begin 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 @@ -172,12 +164,12 @@ Puppet.parse_config Puppet.genconfig Puppet.genmanifest +Puppet::Util::Log.newdestination :console + begin - ca = Puppet::SSLCertificates::CA.new() + ca = Puppet::SSL::CertificateAuthority.new rescue => detail - if Puppet[:debug] - puts detail.backtrace - end + puts detail.backtrace if Puppet[:trace] puts detail.to_s exit(23) end @@ -187,157 +179,16 @@ unless mode exit(12) end -if [:verify, :print, :generate, :clean, :revoke, :list].include?(mode) +if all + hosts = :all +else 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? - $stderr.puts "You must specify one or more hosts to clean" - exit(24) - end - cleaned = false - hosts.each do |host| - cert = ca.getclientcert(host)[0] - if cert.nil? - $stderr.puts "Could not find client certificate for %s" % host - next - end - ca.clean(host) - cleaned = true - 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 to sign all certificates or you must specify hostnames" - ) - 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) +begin + ca.apply(mode, :to => hosts) +rescue => detail + puts detail.backtrace if Puppet[:trace] + puts detail.to_s + exit(24) end - diff --git a/lib/puppet/ssl/certificate_authority.rb b/lib/puppet/ssl/certificate_authority.rb index 2399c7204..8785ee5b0 100644 --- a/lib/puppet/ssl/certificate_authority.rb +++ b/lib/puppet/ssl/certificate_authority.rb @@ -14,8 +14,129 @@ class Puppet::SSL::CertificateAuthority require 'puppet/ssl/inventory' require 'puppet/ssl/certificate_revocation_list' + # This class is basically a hidden class that knows how to act + # on the CA. It's only used by the 'puppetca' executable, and its + # job is to provide a CLI-like interface to the CA class. + class Interface + INTERFACE_METHODS = [:destroy, :list, :revoke, :generate, :sign, :print, :verify] + + class InterfaceError < ArgumentError; end + + attr_reader :method, :subjects + + # Actually perform the work. + def apply(ca) + unless subjects or method == :list + raise ArgumentError, "You must provide hosts or :all when using %s" % method + end + + begin + if respond_to?(method) + return send(method, ca) + end + + (subjects == :all ? ca.list : subjects).each do |host| + ca.send(method, host) + end + rescue InterfaceError + raise + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not call %s: %s" % [method, detail] + end + end + + def generate(ca) + raise InterfaceError, "It makes no sense to generate all hosts; you must specify a list" if subjects == :all + + subjects.each do |host| + ca.generate(host) + end + end + + def initialize(method, subjects) + self.method = method + self.subjects = subjects + end + + # List the hosts. + def list(ca) + unless subjects + puts ca.waiting?.join("\n") + return nil + end + + signed = ca.list + requests = ca.waiting? + + if subjects == :all + hosts = [signed, requests].flatten + else + hosts = subjects + end + + hosts.uniq.sort.each do |host| + if signed.include?(host) + puts "+ " + host + else + puts host + end + end + end + + # Set the method to apply. + def method=(method) + raise ArgumentError, "Invalid method %s to apply" % method unless INTERFACE_METHODS.include?(method) + @method = method + end + + # Print certificate information. + def print(ca) + (subjects == :all ? ca.list : subjects).each do |host| + if value = ca.print(host) + puts value + else + Puppet.err "Could not find certificate for %s" % host + end + end + end + + # Sign a given certificate. + def sign(ca) + list = subjects == :all ? ca.waiting? : subjects + raise InterfaceError, "No waiting certificate requests to sign" if list.empty? + list.each do |host| + ca.sign(host) + end + end + + # Set the list of hosts we're operating on. Also supports keywords. + def subjects=(value) + unless value == :all or value.is_a?(Array) + raise ArgumentError, "Subjects must be an array or :all; not %s" % value + end + + if value.is_a?(Array) and value.empty? + value = nil + end + + @subjects = value + end + end + attr_reader :name, :host + # Create and run an applicator. I wanted to build an interface where you could do + # something like 'ca.apply(:generate).to(:all) but I don't think it's really possible. + def apply(method, options) + unless options[:to] + raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" + end + applier = Interface.new(method, options[:to]) + + applier.apply(self) + end + # Retrieve (or create, if necessary) the certificate revocation list. def crl unless defined?(@crl) diff --git a/lib/puppet/ssl/certificate_revocation_list.rb b/lib/puppet/ssl/certificate_revocation_list.rb index ca7b7db65..96b71c7a3 100644 --- a/lib/puppet/ssl/certificate_revocation_list.rb +++ b/lib/puppet/ssl/certificate_revocation_list.rb @@ -30,6 +30,7 @@ class Puppet::SSL::CertificateRevocationList < Puppet::SSL::Base # CA, then write the CRL back to disk. The REASON must be one of the # OpenSSL::OCSP::REVOKED_* reasons def revoke(serial, cakey, reason = OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE) + Puppet.notice "Revoked certificate with serial %s" % serial time = Time.now # Add our revocation to the CRL. diff --git a/spec/unit/ssl/certificate_authority.rb b/spec/unit/ssl/certificate_authority.rb index 50f8cec9a..5a4e2d5e1 100755 --- a/spec/unit/ssl/certificate_authority.rb +++ b/spec/unit/ssl/certificate_authority.rb @@ -4,6 +4,28 @@ require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/ssl/certificate_authority' +describe "a normal interface method", :shared => true do + it "should call the method on the CA for each host specified if an array was provided" do + @ca.expects(@method).with("host1") + @ca.expects(@method).with("host2") + + @applier = Puppet::SSL::CertificateAuthority::Interface.new(@method, %w{host1 host2}) + + @applier.apply(@ca) + end + + it "should call the method on the CA for all existing certificates if :all was provided" do + @ca.expects(:list).returns %w{host1 host2} + + @ca.expects(@method).with("host1") + @ca.expects(@method).with("host2") + + @applier = Puppet::SSL::CertificateAuthority::Interface.new(@method, :all) + + @applier.apply(@ca) + end +end + describe Puppet::SSL::CertificateAuthority do describe "when initializing" do before do @@ -381,7 +403,29 @@ describe Puppet::SSL::CertificateAuthority do @cacert.stubs(:content).returns "cacertificate" @ca = Puppet::SSL::CertificateAuthority.new end - + + it "should have a method for acting on the SSL files" do + @ca.should respond_to(:apply) + end + + describe "when applying a method to a set of hosts" do + it "should fail if no subjects have been specified" do + lambda { @ca.apply(:generate) }.should raise_error(ArgumentError) + end + + it "should create an Interface instance with the specified method and the subjects" do + Puppet::SSL::CertificateAuthority::Interface.expects(:new).with(:generate, :hosts).returns(stub('applier', :apply => nil)) + @ca.apply(:generate, :to => :hosts) + end + + it "should apply the Interface with itself as the argument" do + applier = stub('applier') + applier.expects(:apply).with(@ca) + Puppet::SSL::CertificateAuthority::Interface.expects(:new).returns applier + @ca.apply(:generate, :to => :whatever) + end + end + it "should be able to list waiting certificate requests" do req1 = stub 'req1', :name => "one" req2 = stub 'req2', :name => "two" @@ -565,3 +609,241 @@ describe Puppet::SSL::CertificateAuthority do end end end + +describe Puppet::SSL::CertificateAuthority::Interface do + before do + @class = Puppet::SSL::CertificateAuthority::Interface + end + describe "when initializing" do + it "should set its method using its settor" do + @class.any_instance.expects(:method=).with(:generate) + @class.new(:generate, :all) + end + + it "should set its subjects using the settor" do + @class.any_instance.expects(:subjects=).with(:all) + @class.new(:generate, :all) + end + end + + describe "when setting the method" do + it "should set the method" do + @class.new(:generate, :all).method.should == :generate + end + + it "should fail if the method isn't a member of the INTERFACE_METHODS array" do + Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS.expects(:include?).with(:thing).returns false + + lambda { @class.new(:thing, :all) }.should raise_error(ArgumentError) + end + end + + describe "when setting the subjects" do + it "should set the subjects" do + @class.new(:generate, :all).subjects.should == :all + end + + it "should fail if the subjects setting isn't :all or an array" do + lambda { @class.new(:generate, "other") }.should raise_error(ArgumentError) + end + end + + it "should have a method for triggering the application" do + @class.new(:generate, :all).should respond_to(:apply) + end + + describe "when applying" do + before do + # We use a real object here, because :verify can't be stubbed, apparently. + @ca = Object.new + end + + it "should raise InterfaceErrors" do + @applier = @class.new(:revoke, :all) + + @ca.expects(:list).raises Puppet::SSL::CertificateAuthority::Interface::InterfaceError + + lambda { @applier.apply(@ca) }.should raise_error(Puppet::SSL::CertificateAuthority::Interface::InterfaceError) + end + + it "should log non-Interface failures rather than failing" do + @applier = @class.new(:revoke, :all) + + @ca.expects(:list).raises ArgumentError + + Puppet.expects(:err) + + lambda { @applier.apply(@ca) }.should_not raise_error + end + + describe "with an empty array specified and the method is not list" do + it "should fail" do + @applier = @class.new(:sign, []) + lambda { @applier.apply(@ca) }.should raise_error(ArgumentError) + end + end + + describe ":generate" do + it "should fail if :all was specified" do + @applier = @class.new(:generate, :all) + lambda { @applier.apply(@ca) }.should raise_error(ArgumentError) + end + + it "should call :generate on the CA for each host specified" do + @applier = @class.new(:generate, %w{host1 host2}) + + @ca.expects(:generate).with("host1") + @ca.expects(:generate).with("host2") + + @applier.apply(@ca) + end + end + + describe ":verify" do + before { @method = :verify } + #it_should_behave_like "a normal interface method" + + it "should call the method on the CA for each host specified if an array was provided" do + # LAK:NOTE Mocha apparently doesn't allow you to mock :verify, but I'm confident this works in real life. + end + + it "should call the method on the CA for all existing certificates if :all was provided" do + # LAK:NOTE Mocha apparently doesn't allow you to mock :verify, but I'm confident this works in real life. + end + end + + describe ":destroy" do + before { @method = :destroy } + it_should_behave_like "a normal interface method" + end + + describe ":revoke" do + before { @method = :revoke } + it_should_behave_like "a normal interface method" + end + + describe ":sign" do + describe "and an array of names was provided" do + before do + @applier = @class.new(:sign, %w{host1 host2}) + end + + it "should sign the specified waiting certificate requests" do + @ca.expects(:sign).with("host1") + @ca.expects(:sign).with("host2") + + @applier.apply(@ca) + end + end + + describe "and :all was provided" do + it "should sign all waiting certificate requests" do + @ca.stubs(:waiting?).returns(%w{cert1 cert2}) + + @ca.expects(:sign).with("cert1") + @ca.expects(:sign).with("cert2") + + @applier = @class.new(:sign, :all) + @applier.apply(@ca) + end + + it "should fail if there are no waiting certificate requests" do + @ca.stubs(:waiting?).returns([]) + + @applier = @class.new(:sign, :all) + lambda { @applier.apply(@ca) }.should raise_error(Puppet::SSL::CertificateAuthority::Interface::InterfaceError) + end + end + end + + describe ":list" do + describe "and an empty array was provided" do + it "should print a string containing all certificate requests" do + @ca.expects(:waiting?).returns %w{host1 host2} + + @applier = @class.new(:list, []) + + @applier.expects(:puts).with "host1\nhost2" + + @applier.apply(@ca) + end + end + + describe "and :all was provided" do + it "should print a string containing all certificate requests and certificates" do + @ca.expects(:waiting?).returns %w{host1 host2} + @ca.expects(:list).returns %w{host3 host4} + + @applier = @class.new(:list, :all) + + @applier.expects(:puts).with "host1" + @applier.expects(:puts).with "host2" + @applier.expects(:puts).with "+ host3" + @applier.expects(:puts).with "+ host4" + + @applier.apply(@ca) + end + end + + describe "and an array of names was provided" do + it "should print a string of all named hosts that have a waiting request" do + @ca.expects(:waiting?).returns %w{host1 host2} + @ca.expects(:list).returns %w{host3 host4} + + @applier = @class.new(:list, %w{host1 host2 host3 host4}) + + @applier.expects(:puts).with "host1" + @applier.expects(:puts).with "host2" + @applier.expects(:puts).with "+ host3" + @applier.expects(:puts).with "+ host4" + + @applier.apply(@ca) + end + end + end + + describe ":print" do + describe "and :all was provided" do + it "should print all certificates" do + @ca.expects(:list).returns %w{host1 host2} + + @applier = @class.new(:print, :all) + + @ca.expects(:print).with("host1").returns "h1" + @applier.expects(:puts).with "h1" + + @ca.expects(:print).with("host2").returns "h2" + @applier.expects(:puts).with "h2" + + @applier.apply(@ca) + end + end + + describe "and an array of names was provided" do + it "should print each named certificate if found" do + @applier = @class.new(:print, %w{host1 host2}) + + @ca.expects(:print).with("host1").returns "h1" + @applier.expects(:puts).with "h1" + + @ca.expects(:print).with("host2").returns "h2" + @applier.expects(:puts).with "h2" + + @applier.apply(@ca) + end + + it "should log any named but not found certificates" do + @applier = @class.new(:print, %w{host1 host2}) + + @ca.expects(:print).with("host1").returns "h1" + @applier.expects(:puts).with "h1" + + @ca.expects(:print).with("host2").returns nil + Puppet.expects(:err).with { |msg| msg.include?("host2") } + + @applier.apply(@ca) + end + end + end + end +end diff --git a/spec/unit/ssl/certificate_revocation_list.rb b/spec/unit/ssl/certificate_revocation_list.rb index 042a12e15..2efdd187a 100755 --- a/spec/unit/ssl/certificate_revocation_list.rb +++ b/spec/unit/ssl/certificate_revocation_list.rb @@ -113,7 +113,7 @@ describe Puppet::SSL::CertificateRevocationList do it "should mark the CRL as updated" do time = Time.now - Time.expects(:now).returns time + Time.stubs(:now).returns time @crl.content.expects(:last_update=).with(time) @@ -122,7 +122,7 @@ describe Puppet::SSL::CertificateRevocationList do it "should mark the CRL valid for five years" do time = Time.now - Time.expects(:now).returns time + Time.stubs(:now).returns time @crl.content.expects(:next_update=).with(time + (5 * 365*24*60*60)) diff --git a/test/executables/puppetca.rb b/test/executables/puppetca.rb deleted file mode 100755 index cdc827079..000000000 --- a/test/executables/puppetca.rb +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env ruby - -require File.dirname(__FILE__) + '/../lib/puppettest' - -require 'puppettest' -require 'mocha' - -class TestPuppetCA < Test::Unit::TestCase - include PuppetTest::ExeTest - - def setup - super - Puppet::Util::SUIDManager.stubs(:asuser).yields - end - - def gen_cert(ca, host) - runca("-g #{host}") - ca.getclientcert(host)[0] - end - - def mkca - Puppet::Network::Handler.ca.new() - end - - def mkcert(hostname) - cert = nil - assert_nothing_raised { - cert = Puppet::SSLCertificates::Certificate.new( - :name => hostname - ) - cert.mkcsr - } - - return cert - end - - def runca(args) - debug = "" - if Puppet[:debug] - debug = "-d " - end - return %x{puppetca --user=#{Puppet[:user]} #{debug} --group=#{Puppet[:group]} --confdir=#{Puppet[:confdir]} --vardir=#{Puppet[:vardir]} #{args} 2>&1} - end - - def test_signing - ca = mkca - Puppet[:autosign] = false - - %w{host.test.com Other.Testing.Com}.each do |host| - cert = mkcert(host) - resp = nil - assert_nothing_raised { - # We need to use a fake name so it doesn't think the cert is from - # itself. Strangely, getcert stores the csr, because it's a server-side - # method, not client. - resp = ca.getcert(cert.csr.to_pem, host, "127.0.0.1") - } - assert_equal(["",""], resp) - - output = nil - assert_nothing_raised { - output = runca("--list").chomp.split("\n").reject { |line| line =~ /warning:/ } # stupid ssl.rb - } - assert_equal($?,0) - assert_equal([host.downcase], output) - assert_nothing_raised { - output = runca("--sign -a").chomp.split("\n") - } - - - assert_equal($?,0) - assert_equal(["Signed #{host.downcase}"], output) - - - signedfile = ca.ca.host2certfile(host) - assert(FileTest.exists?(signedfile), "cert does not exist") - assert(! FileTest.executable?(signedfile), "cert is executable") - - uid = Puppet::Util.uid(Puppet[:user]) - - if Puppet::Util::SUIDManager.uid == 0 - assert(! FileTest.owned?(signedfile), "cert is owned by root") - end - assert_nothing_raised { - output = runca("--list").chomp.split("\n") - } - assert_equal($?,0) - assert_equal(["No certificates to sign"], output) - end - end - - # This method takes a long time to run because of all of the external - # executable calls. - def test_revocation - ca = Puppet::SSLCertificates::CA.new() - host1 = gen_cert(ca, "host1.example.com") - host2 = gen_cert(ca, "host2.example.com") - host3 = gen_cert(ca, "host3.example.com") - runca("-r host1.example.com") - runca("-r #{host2.serial}") - runca("-r 0x#{host3.serial.to_s(16)}") - runca("-r 0xff") - - # Recreate CA to force reading of CRL - ca = Puppet::SSLCertificates::CA.new() - crl = ca.crl - revoked = crl.revoked.collect { |r| r.serial } - exp = [host1.serial, host2.serial, host3.serial, 255] - assert_equal(exp, revoked) - end - - def test_case_insensitive_sign - end -end - |