From 800b32cbe2fa9182304c8facc466560a55a7040c Mon Sep 17 00:00:00 2001 From: Nick Germany Date: Sat, 29 Jul 2023 16:03:28 -0400 Subject: [PATCH 1/3] Add a generate_request option to puppet ssl --- lib/puppet/application/ssl.rb | 25 +++++++++++++++++++++++++ man/man8/puppet-ssl.8 | 4 ++++ 2 files changed, 29 insertions(+) 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\. . From 2b3fb63a51f307047ee2f2591c05067ecb29e5af Mon Sep 17 00:00:00 2001 From: Nick Germany Date: Sat, 29 Jul 2023 16:11:04 -0400 Subject: [PATCH 2/3] Add a test for generate_request --- spec/unit/application/ssl_spec.rb | 79 +++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/spec/unit/application/ssl_spec.rb b/spec/unit/application/ssl_spec.rb index 4b6d1c8d6b1..2128cc07d41 100644 --- a/spec/unit/application/ssl_spec.rb +++ b/spec/unit/application/ssl_spec.rb @@ -171,6 +171,85 @@ 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]) + + 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://.*}) + end + + it 'generates an EC private key' do + Puppet[:key_type] = 'ec' + File.unlink(Puppet[:hostprivkey]) + + 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://.*}) + end + + it 'registers OIDs' 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) + + expect(Puppet::SSL::Oids).to receive(:register_puppet_oids) + expects_command_to_pass(%r{Submitted certificate request for '#{name}' to https://.*}) + 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' From ddabf6d49e264fc5701ff01362354b7697f943c0 Mon Sep 17 00:00:00 2001 From: Nick Germany Date: Sat, 29 Jul 2023 16:34:57 -0400 Subject: [PATCH 3/3] Update tests to not do stubs for communicating with CA --- spec/unit/application/ssl_spec.rb | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/spec/unit/application/ssl_spec.rb b/spec/unit/application/ssl_spec.rb index 2128cc07d41..90b0d6bf7cb 100644 --- a/spec/unit/application/ssl_spec.rb +++ b/spec/unit/application/ssl_spec.rb @@ -184,32 +184,23 @@ def expects_command_to_fail(message) it 'generates an RSA private key' do File.unlink(Puppet[:hostprivkey]) - 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://.*}) + 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]) - 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://.*}) + expects_command_to_pass(%r{Generated certificate request for '#{name}' at #{requestdir}}) end it 'registers OIDs' 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) - expect(Puppet::SSL::Oids).to receive(:register_puppet_oids) - expects_command_to_pass(%r{Submitted certificate request for '#{name}' to https://.*}) + + 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)