diff --git a/lib/puppet/face/ca.rb b/lib/puppet/face/ca.rb index 00591d637..6688886ab 100644 --- a/lib/puppet/face/ca.rb +++ b/lib/puppet/face/ca.rb @@ -1,233 +1,242 @@ require 'puppet/face' Puppet::Face.define(:ca, '0.1.0') do copyright "Puppet Labs", 2011 license "Apache 2 license; see COPYING" summary "Local Puppet Certificate Authority management." description <<-TEXT This provides local management of the Puppet Certificate Authority. You can use this subcommand to sign outstanding certificate requests, list and manage local certificates, and inspect the state of the CA. TEXT action :list do summary "List certificates and/or certificate requests." description <<-TEXT This will list the current certificates and certificate signing requests in the Puppet CA. You will also get the fingerprint, and any certificate verification failure reported. TEXT option "--[no-]all" do summary "Include all certificates and requests." end option "--[no-]pending" do summary "Include pending certificate signing requests." end option "--[no-]signed" do summary "Include signed certificates." end option "--subject PATTERN" do summary "Only list if the subject matches PATTERN." description <<-TEXT Only include certificates or requests where subject matches PATTERN. PATTERN is interpreted as a regular expression, allowing complex filtering of the content. TEXT end when_invoked do |options| raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca? unless ca = Puppet::SSL::CertificateAuthority.instance raise "Unable to fetch the CA" end pattern = options[:subject].nil? ? nil : Regexp.new(options[:subject], Regexp::IGNORECASE) pending = options[:pending].nil? ? options[:all] : options[:pending] signed = options[:signed].nil? ? options[:all] : options[:signed] # By default we list pending, so if nothing at all was requested... unless pending or signed then pending = true end hosts = [] pending and hosts += ca.waiting? signed and hosts += ca.list pattern and hosts = hosts.select {|hostname| pattern.match hostname } hosts.sort.map {|host| Puppet::SSL::Host.new(host) } end when_rendering :console do |hosts| unless ca = Puppet::SSL::CertificateAuthority.instance raise "Unable to fetch the CA" end length = hosts.map{|x| x.name.length }.max + 1 hosts.map do |host| name = host.name.ljust(length) if host.certificate_request then " #{name} (#{host.certificate_request.fingerprint})" else begin ca.verify(host.certificate) "+ #{name} (#{host.certificate.fingerprint})" rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError => e "- #{name} (#{host.certificate.fingerprint}) (#{e.to_s})" end end end.join("\n") end end action :destroy do when_invoked do |host, options| raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca? unless ca = Puppet::SSL::CertificateAuthority.instance raise "Unable to fetch the CA" end ca.destroy host end end action :revoke do when_invoked do |host, options| raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca? unless ca = Puppet::SSL::CertificateAuthority.instance raise "Unable to fetch the CA" end begin ca.revoke host rescue ArgumentError => e # This is a bit naff, but it makes the behaviour consistent with the # destroy action. The underlying tools could be nicer for that sort # of thing; they have fairly inconsistent reporting of failures. raise unless e.to_s =~ /Could not find a serial number for / "Nothing was revoked" end end end action :generate do + option "--dns-alt-names NAMES" do + summary "Additional DNS names to add to the certificate request" + description Puppet.settings.setting(:dns_alt_names).desc + end + when_invoked do |host, options| raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca? unless ca = Puppet::SSL::CertificateAuthority.instance raise "Unable to fetch the CA" end begin - ca.generate host + ca.generate(host, :dns_alt_names => options[:dns_alt_names]) rescue RuntimeError => e if e.to_s =~ /already has a requested certificate/ "#{host} already has a certificate request; use sign instead" else raise end rescue ArgumentError => e if e.to_s =~ /A Certificate already exists for / "#{host} already has a certificate" else raise end end end end action :sign do + option("--[no-]allow-dns-alt-names") do + summary "Whether or not to accept DNS alt names in the certificate request" + end + when_invoked do |host, options| raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca? unless ca = Puppet::SSL::CertificateAuthority.instance raise "Unable to fetch the CA" end begin - ca.sign host + ca.sign(host, options[:allow_dns_alt_names]) rescue ArgumentError => e if e.to_s =~ /Could not find certificate request/ e.to_s else raise end end end end action :print do when_invoked do |host, options| raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca? unless ca = Puppet::SSL::CertificateAuthority.instance raise "Unable to fetch the CA" end ca.print host end end action :fingerprint do option "--digest ALGORITHM" do summary "The hash algorithm to use when displaying the fingerprint" end when_invoked do |host, options| raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca? unless ca = Puppet::SSL::CertificateAuthority.instance raise "Unable to fetch the CA" end begin # I want the default from the CA, not to duplicate it, but passing # 'nil' explicitly means that we don't get that. This works... if options.has_key? :digest ca.fingerprint host, options[:digest] else ca.fingerprint host end rescue ArgumentError => e raise unless e.to_s =~ /Could not find a certificate or csr for/ nil end end end action :verify do when_invoked do |host, options| raise "Not a CA" unless Puppet::SSL::CertificateAuthority.ca? unless ca = Puppet::SSL::CertificateAuthority.instance raise "Unable to fetch the CA" end begin ca.verify host { :host => host, :valid => true } rescue ArgumentError => e raise unless e.to_s =~ /Could not find a certificate for/ { :host => host, :valid => false, :error => e.to_s } rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError => e { :host => host, :valid => false, :error => e.to_s } end end when_rendering :console do |value| if value[:valid] nil else "Could not verify #{value[:host]}: #{value[:error]}" end end end end diff --git a/spec/unit/face/ca_spec.rb b/spec/unit/face/ca_spec.rb index 1df4d7c53..445c91ecc 100755 --- a/spec/unit/face/ca_spec.rb +++ b/spec/unit/face/ca_spec.rb @@ -1,355 +1,388 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/face' describe Puppet::Face[:ca, '0.1.0'], :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files before :each do Puppet.run_mode.stubs(:master?).returns(true) Puppet[:ca] = true Puppet[:ssldir] = tmpdir("face-ca-ssldir") Puppet::SSL::Host.ca_location = :only Puppet[:certificate_revocation] = true # This is way more intimate than I want to be with the implementation, but # there doesn't seem any other way to test this. --daniel 2011-07-18 Puppet::SSL::CertificateAuthority.stubs(:instance).returns( # ...and this actually does the directory creation, etc. Puppet::SSL::CertificateAuthority.new ) end def make_certs(csr_names, crt_names) Array(csr_names).map do |name| Puppet::SSL::Host.new(name).generate_certificate_request end Array(crt_names).map do |name| Puppet::SSL::Host.new(name).generate end end context "#verify" do let :action do Puppet::Face[:ca, '0.1.0'].get_action(:verify) end it "should not explode if there is no certificate" do expect { subject.verify('random-host').should == { :host => 'random-host', :valid => false, :error => 'Could not find a certificate for random-host' } }.should_not raise_error end it "should not explode if there is only a CSR" do make_certs('random-host', []) expect { subject.verify('random-host').should == { :host => 'random-host', :valid => false, :error => 'Could not find a certificate for random-host' } }.should_not raise_error end it "should verify a signed certificate" do make_certs([], 'random-host') subject.verify('random-host').should == { :host => 'random-host', :valid => true } end it "should not verify a revoked certificate" do make_certs([], 'random-host') subject.revoke('random-host') expect { subject.verify('random-host').should == { :host => 'random-host', :valid => false, :error => 'certificate revoked' } }.should_not raise_error end it "should verify a revoked certificate if CRL use was turned off" do make_certs([], 'random-host') subject.revoke('random-host') Puppet[:certificate_revocation] = false subject.verify('random-host').should == { :host => 'random-host', :valid => true } end end context "#fingerprint" do let :action do Puppet::Face[:ca, '0.1.0'].get_action(:fingerprint) end it "should have a 'digest' option" do action.should be_option :digest end it "should not explode if there is no certificate" do expect { subject.fingerprint('random-host').should be_nil }.should_not raise_error end it "should fingerprint a CSR" do make_certs('random-host', []) expect { subject.fingerprint('random-host').should =~ /^[0-9A-F:]+$/ }.should_not raise_error end it "should fingerprint a certificate" do make_certs([], 'random-host') subject.fingerprint('random-host').should =~ /^[0-9A-F:]+$/ end %w{md5 MD5 sha1 ShA1 SHA1 RIPEMD160 sha256 sha512}.each do |digest| it "should fingerprint with #{digest.inspect}" do make_certs([], 'random-host') subject.fingerprint('random-host', :digest => digest).should =~ /^[0-9A-F:]+$/ end it "should fingerprint with #{digest.to_sym} as a symbol" do make_certs([], 'random-host') subject.fingerprint('random-host', :digest => digest.to_sym). should =~ /^[0-9A-F:]+$/ end end end context "#print" do let :action do Puppet::Face[:ca, '0.1.0'].get_action(:print) end it "should not explode if there is no certificate" do expect { subject.print('random-host').should be_nil }.should_not raise_error end it "should return nothing if there is only a CSR" do make_certs('random-host', []) expect { subject.print('random-host').should be_nil }.should_not raise_error end it "should return the certificate content if there is a cert" do make_certs([], 'random-host') text = subject.print('random-host') text.should be_an_instance_of String text.should =~ /^Certificate:/ text.should =~ /Issuer: CN=Puppet CA: / text.should =~ /Subject: CN=random-host$/ end end context "#sign" do let :action do Puppet::Face[:ca, '0.1.0'].get_action(:sign) end it "should not explode if there is no CSR" do expect { subject.sign('random-host'). should == 'Could not find certificate request for random-host' }.should_not raise_error end it "should not explode if there is a signed cert" do make_certs([], 'random-host') expect { subject.sign('random-host'). should == 'Could not find certificate request for random-host' }.should_not raise_error end it "should sign a CSR if one exists" do make_certs('random-host', []) subject.sign('random-host').should be_an_instance_of Puppet::SSL::Certificate list = subject.list(:signed => true) list.length.should == 1 list.first.name.should == 'random-host' end + + describe "when the CSR specifies DNS alt names" do + let(:host) { Puppet::SSL::Host.new('someone') } + + before :each do + host.generate_certificate_request(:dns_alt_names => 'some,alt,names') + end + + it "should sign the CSR if DNS alt names are allowed" do + subject.sign('someone', :allow_dns_alt_names => true) + + host.certificate.should be_a(Puppet::SSL::Certificate) + end + + it "should refuse to sign the CSR if DNS alt names are not allowed" do + expect do + subject.sign('someone') + end.to raise_error(Puppet::SSL::CertificateAuthority::CertificateSigningError, /CSR contained subject alternative names (.*), which are disallowed./) + + host.certificate.should be_nil + end + end end context "#generate" do let :action do Puppet::Face[:ca, '0.1.0'].get_action(:generate) end it "should generate a certificate if requested" do subject.list(:all => true).should == [] subject.generate('random-host') list = subject.list(:signed => true) list.length.should == 1 list.first.name.should == 'random-host' end it "should not explode if a CSR with that name already exists" do make_certs('random-host', []) expect { subject.generate('random-host').should =~ /already has a certificate request/ }.should_not raise_error end it "should not explode if the certificate with that name already exists" do make_certs([], 'random-host') expect { subject.generate('random-host').should =~ /already has a certificate/ }.should_not raise_error end + + it "should include the specified DNS alt names" do + subject.generate('some-host', :dns_alt_names => 'some,alt,names') + + host = subject.list(:signed => true).first + + host.name.should == 'some-host' + host.certificate.subject_alt_names.should =~ %w[DNS:some DNS:alt DNS:names DNS:some-host] + + subject.list(:pending => true).should be_empty + end end context "#revoke" do let :action do Puppet::Face[:ca, '0.1.0'].get_action(:revoke) end it "should not explode when asked to revoke something that doesn't exist" do expect { subject.revoke('nonesuch') }.should_not raise_error end it "should let the user know what went wrong" do subject.revoke('nonesuch').should == 'Nothing was revoked' end it "should revoke a certificate" do make_certs([], 'random-host') found = subject.list(:all => true, :subject => 'random-host') subject.get_action(:list).when_rendering(:console).call(found). should =~ /^\+ random-host/ subject.revoke('random-host') found = subject.list(:all => true, :subject => 'random-host') subject.get_action(:list).when_rendering(:console).call(found). should =~ /^- random-host \([:0-9A-F]+\) \(certificate revoked\)/ end end context "#destroy" do let :action do Puppet::Face[:ca, '0.1.0'].get_action(:destroy) end it "should not explode when asked to delete something that doesn't exist" do expect { subject.destroy('nonesuch') }.should_not raise_error end it "should let the user know if nothing was deleted" do subject.destroy('nonesuch').should == "Nothing was deleted" end it "should destroy a CSR, if we have one" do make_certs('random-host', []) subject.list(:pending => true, :subject => 'random-host').should_not == [] subject.destroy('random-host') subject.list(:pending => true, :subject => 'random-host').should == [] end it "should destroy a certificate, if we have one" do make_certs([], 'random-host') subject.list(:signed => true, :subject => 'random-host').should_not == [] subject.destroy('random-host') subject.list(:signed => true, :subject => 'random-host').should == [] end it "should tell the user something was deleted" do make_certs([], 'random-host') subject.list(:signed => true, :subject => 'random-host').should_not == [] subject.destroy('random-host'). should == "Deleted for random-host: Puppet::SSL::Certificate, Puppet::SSL::Key" end end context "#list" do let :action do Puppet::Face[:ca, '0.1.0'].get_action(:list) end context "options" do subject { Puppet::Face[:ca, '0.1.0'].get_action(:list) } it { should be_option :pending } it { should be_option :signed } it { should be_option :all } it { should be_option :subject } end context "with no hosts in CA" do [:pending, :signed, :all].each do |type| it "should return nothing for #{type}" do subject.list(type => true).should == [] end it "should not fail when a matcher is passed" do expect { subject.list(type => true, :subject => '.').should == [] }.should_not raise_error end end end context "with some hosts" do csr_names = (1..3).map {|n| "csr-#{n}" } crt_names = (1..3).map {|n| "crt-#{n}" } all_names = csr_names + crt_names { {} => csr_names, { :pending => true } => csr_names, { :signed => true } => crt_names, { :all => true } => all_names, { :pending => true, :signed => true } => all_names, }.each do |input, expect| it "should map #{input.inspect} to #{expect.inspect}" do make_certs(csr_names, crt_names) subject.list(input).map(&:name).should =~ expect end ['', '.', '2', 'none'].each do |pattern| filtered = expect.select {|x| Regexp.new(pattern).match(x) } it "should filter all hosts matching #{pattern.inspect} to #{filtered.inspect}" do make_certs(csr_names, crt_names) subject.list(input.merge :subject => pattern).map(&:name).should =~ filtered end end end context "when_rendering :console" do { [["csr1.local"], []] => '^ csr1.local ', [[], ["crt1.local"]] => '^\+ crt1.local ', [["csr2"], ["crt2"]] => ['^ csr2 ', '^\+ crt2 '] }.each do |input, pattern| it "should render #{input.inspect} to match #{pattern.inspect}" do make_certs(*input) text = action.when_rendering(:console).call(subject.list(:all => true)) Array(pattern).each do |item| text.should =~ Regexp.new(item) end end end end end end actions = %w{destroy list revoke generate sign print verify fingerprint} actions.each do |action| it { should be_action action } it "should fail #{action} when not a CA" do Puppet[:ca] = false expect { case subject.method(action).arity when -1 then subject.send(action) when -2 then subject.send(action, 'dummy') else raise "#{action} has arity #{subject.method(action).arity}" end }.should raise_error(/Not a CA/) end end end