diff --git a/lib/puppet/application/ssl.rb b/lib/puppet/application/ssl.rb index 1d1745d0b25..6a2b7ebb65e 100644 --- a/lib/puppet/application/ssl.rb +++ b/lib/puppet/application/ssl.rb @@ -60,6 +60,11 @@ def help the CSR. Otherwise a new key pair will be generated. If a CSR has already been submitted with the given `certname`, then the operation will fail. +* generate_request: + Generate a certificate signing request (CSR). If + a private and public key pair already exist, they will be used to generate + the CSR. Otherwise a new key pair will be generated. + * download_cert: Download a certificate for this host. If the current private key matches the downloaded certificate, then the certificate will be saved and used @@ -188,6 +193,26 @@ def submit_request(ssl_context) raise Puppet::Error.new(_("Failed to submit certificate request: %{message}") % { message: e.message }, e) end + def generate_request(ssl_context) + key = @cert_provider.load_private_key(Puppet[:certname]) + unless key + if Puppet[:key_type] == 'ec' + Puppet.info _("Creating a new EC SSL key for %{name} using curve %{curve}") % { name: Puppet[:certname], curve: Puppet[:named_curve] } + key = OpenSSL::PKey::EC.generate(Puppet[:named_curve]) + else + Puppet.info _("Creating a new SSL key for %{name}") % { name: Puppet[:certname] } + key = OpenSSL::PKey::RSA.new(Puppet[:keylength].to_i) + end + @cert_provider.save_private_key(Puppet[:certname], key) + end + + csr = @cert_provider.create_request(Puppet[:certname], key) + @cert_provider.save_request(Puppet[:certname], csr) + Puppet.notice _("Generated certificate request for '%{name}' at %{requestdir}") % { name: Puppet[:certname], requestdir: Puppet[:requestdir] } + rescue => e + raise Puppet::Error.new(_("Failed to generate certificate request: %{message}") % { message: e.message }, e) + end + def download_cert(ssl_context) key = @cert_provider.load_private_key(Puppet[:certname]) diff --git a/man/man8/puppet-ssl.8 b/man/man8/puppet-ssl.8 index b919bd60037..1e04b01701a 100644 --- a/man/man8/puppet-ssl.8 +++ b/man/man8/puppet-ssl.8 @@ -42,6 +42,10 @@ submit_request Generate a certificate signing request (CSR) and submit it to the CA\. If a private and public key pair already exist, they will be used to generate the CSR\. Otherwise a new key pair will be generated\. If a CSR has already been submitted with the given \fBcertname\fR, then the operation will fail\. . .TP +generate_request +Generate a certificate signing request (CSR)\. If a private and public key pair already exist, they will be used to generate the CSR\. Otherwise a new key pair will be generated\. +. +.TP download_cert Download a certificate for this host\. If the current private key matches the downloaded certificate, then the certificate will be saved and used for subsequent requests\. If there is already an existing certificate, it will be overwritten\. . diff --git a/spec/unit/application/ssl_spec.rb b/spec/unit/application/ssl_spec.rb index 4b6d1c8d6b1..90b0d6bf7cb 100644 --- a/spec/unit/application/ssl_spec.rb +++ b/spec/unit/application/ssl_spec.rb @@ -171,6 +171,76 @@ def expects_command_to_fail(message) end end + context 'when generating a CSR' do + let(:csr_path) { Puppet[:hostcsr] } + let(:requestdir) { Puppet[:requestdir] } + + before do + ssl.command_line.args << 'generate_request' + end + + it_behaves_like 'an ssl action' + + it 'generates an RSA private key' do + File.unlink(Puppet[:hostprivkey]) + + expects_command_to_pass(%r{Generated certificate request for '#{name}' at #{requestdir}}) + end + + it 'generates an EC private key' do + Puppet[:key_type] = 'ec' + File.unlink(Puppet[:hostprivkey]) + + expects_command_to_pass(%r{Generated certificate request for '#{name}' at #{requestdir}}) + end + + it 'registers OIDs' do + expect(Puppet::SSL::Oids).to receive(:register_puppet_oids) + + expects_command_to_pass(%r{Generated certificate request for '#{name}' at #{requestdir}}) + end + + it 'saves the CSR locally' do + expects_command_to_pass(%r{Generated certificate request for '#{name}' at #{requestdir}}) + + expect(Puppet::FileSystem).to be_exist(csr_path) + end + + it 'detects when a CSR with the same public key has already been submitted' do + stub_request(:put, %r{puppet-ca/v1/certificate_request/#{name}}).to_return(status: 200) + stub_request(:get, %r{puppet-ca/v1/certificate/#{name}}).to_return(status: 404) + + expects_command_to_pass(%r{Submitted certificate request for '#{name}' to https://.*}) + + stub_request(:get, %r{puppet-ca/v1/certificate/#{name}}).to_return(status: 404) + + expects_command_to_pass + end + + it 'downloads the certificate when autosigning is enabled' do + stub_request(:put, %r{puppet-ca/v1/certificate_request/#{name}}).to_return(status: 200) + stub_request(:get, %r{puppet-ca/v1/certificate/#{name}}).to_return(status: 200, body: @host[:cert].to_pem) + + expects_command_to_pass(%r{Submitted certificate request for '#{name}' to https://.*}) + + expect(Puppet::FileSystem).to be_exist(Puppet[:hostcert]) + expect(Puppet::FileSystem).to_not be_exist(csr_path) + end + + it 'accepts dns alt names' do + Puppet[:dns_alt_names] = 'majortom' + + stub_request(:put, %r{puppet-ca/v1/certificate_request/#{name}}).to_return(status: 200) + stub_request(:get, %r{puppet-ca/v1/certificate/#{name}}).to_return(status: 404) + + expects_command_to_pass + + csr = Puppet::SSL::CertificateRequest.new(name) + csr.read(csr_path) + expect(csr.subject_alt_names).to include('DNS:majortom') + end + end + context 'when downloading a certificate' do before do ssl.command_line.args << 'download_cert'