diff --git a/lib/puppet/application/ssl.rb b/lib/puppet/application/ssl.rb index 1d1745d0b25..aceacbbb778 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 @@ -137,6 +142,8 @@ def main unless cert raise Puppet::Error, _("The certificate for '%{name}' has not yet been signed") % { name: certname } end + when 'generate_request' + generate_request(certname) when 'verify' verify(certname) when 'clean' @@ -163,13 +170,7 @@ def show(certname) def submit_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 + key = create_key(Puppet[:certname]) @cert_provider.save_private_key(Puppet[:certname], key) end @@ -188,6 +189,20 @@ def submit_request(ssl_context) raise Puppet::Error.new(_("Failed to submit certificate request: %{message}") % { message: e.message }, e) end + def generate_request(certname) + key = @cert_provider.load_private_key(certname) + unless key + key = create_key(certname) + @cert_provider.save_private_key(certname, key) + end + + csr = @cert_provider.create_request(certname, key) + @cert_provider.save_request(certname, csr) + Puppet.notice _("Generated certificate request in '%{path}'") % { path: @cert_provider.to_path(Puppet[:requestdir], certname) } + 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]) @@ -286,4 +301,14 @@ def fingerprint(cert) def create_route(ssl_context) @session.route_to(:ca, ssl_context: ssl_context) end + + def create_key(certname) + if Puppet[:key_type] == 'ec' + Puppet.info _("Creating a new EC SSL key for %{name} using curve %{curve}") % { name: certname, curve: Puppet[:named_curve] } + OpenSSL::PKey::EC.generate(Puppet[:named_curve]) + else + Puppet.info _("Creating a new SSL key for %{name}") % { name: certname } + OpenSSL::PKey::RSA.new(Puppet[:keylength].to_i) + end + end end diff --git a/lib/puppet/x509/cert_provider.rb b/lib/puppet/x509/cert_provider.rb index e9c9ac8a2ac..f11e04ffdc0 100644 --- a/lib/puppet/x509/cert_provider.rb +++ b/lib/puppet/x509/cert_provider.rb @@ -376,13 +376,17 @@ def load_request_from_pem(pem) OpenSSL::X509::Request.new(pem) end - private - + # Return the path to the cert related object (key, CSR, cert, etc). + # + # @param base [String] base directory + # @param name [String] the name associated with the cert related object def to_path(base, name) raise _("Certname %{name} must not contain unprintable or non-ASCII characters") % { name: name.inspect } unless name =~ VALID_CERTNAME File.join(base, "#{name.downcase}.pem") end + private + def permissions_for_setting(name) setting = Puppet.settings.setting(name) perm = { mode: setting.mode.to_i(8) } diff --git a/man/man8/puppet-ssl.8 b/man/man8/puppet-ssl.8 index 3ec444ea957..a78ac6a7409 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..b790e70d065 100644 --- a/spec/unit/application/ssl_spec.rb +++ b/spec/unit/application/ssl_spec.rb @@ -171,6 +171,50 @@ 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 'generates an RSA private key' do + File.unlink(Puppet[:hostprivkey]) + + expects_command_to_pass(%r{Generated certificate request in '#{csr_path}'}) + end + + it 'generates an EC private key' do + Puppet[:key_type] = 'ec' + File.unlink(Puppet[:hostprivkey]) + + expects_command_to_pass(%r{Generated certificate request in '#{csr_path}'}) + end + + it 'registers OIDs' do + expect(Puppet::SSL::Oids).to receive(:register_puppet_oids) + + expects_command_to_pass(%r{Generated certificate request in '#{csr_path}'}) + end + + it 'saves the CSR locally' do + expects_command_to_pass(%r{Generated certificate request in '#{csr_path}'}) + + expect(Puppet::FileSystem).to be_exist(csr_path) + end + + it 'accepts dns alt names' do + Puppet[:dns_alt_names] = 'majortom' + + 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' diff --git a/spec/unit/ssl/certificate_signer_spec.rb b/spec/unit/ssl/certificate_signer_spec.rb new file mode 100644 index 00000000000..45f0108da15 --- /dev/null +++ b/spec/unit/ssl/certificate_signer_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Puppet::SSL::CertificateSigner do + include PuppetSpec::Files + + let(:wrong_key) { OpenSSL::PKey::RSA.new(512) } + let(:client_cert) { cert_fixture('signed.pem') } + + # jruby-openssl >= 0.13.0 (JRuby >= 9.3.5.0) raises an error when signing a + # certificate when there is a discrepancy between the certificate and key. + it 'raises if client cert signature is invalid', if: Puppet::Util::Platform.jruby? && RUBY_VERSION.to_f >= 2.6 do + expect { + client_cert.sign(wrong_key, OpenSSL::Digest::SHA256.new) + }.to raise_error(OpenSSL::X509::CertificateError, + 'invalid public key data') + end +end diff --git a/spec/unit/ssl/ssl_provider_spec.rb b/spec/unit/ssl/ssl_provider_spec.rb index f319275e4be..b83672e421f 100644 --- a/spec/unit/ssl/ssl_provider_spec.rb +++ b/spec/unit/ssl/ssl_provider_spec.rb @@ -338,7 +338,7 @@ end end - it 'raises if intermediate CA signature is invalid' do + it 'raises if intermediate CA signature is invalid', unless: Puppet::Util::Platform.jruby? && RUBY_VERSION.to_f >= 2.6 do int = global_cacerts.last int.public_key = wrong_key.public_key if Puppet::Util::Platform.jruby? int.sign(wrong_key, OpenSSL::Digest::SHA256.new)