require 'spec_helper' describe Puppet::SSL::SSLProvider do include PuppetSpec::Files let(:global_cacerts) { [ cert_fixture('ca.pem'), cert_fixture('intermediate.pem') ] } let(:global_crls) { [ crl_fixture('crl.pem'), crl_fixture('intermediate-crl.pem') ] } let(:wrong_key) { OpenSSL::PKey::RSA.new(512) } context 'when creating an insecure context' do let(:sslctx) { subject.create_insecure_context } it 'has an empty list of trusted certs' do expect(sslctx.cacerts).to eq([]) end it 'has an empty list of crls' do expect(sslctx.crls).to eq([]) end it 'has an empty chain' do expect(sslctx.client_chain).to eq([]) end it 'has a nil private key and cert' do expect(sslctx.private_key).to be_nil expect(sslctx.client_cert).to be_nil end it 'does not authenticate the server' do expect(sslctx.verify_peer).to eq(false) end it 'raises if the frozen context is modified' do expect { sslctx.cacerts = [] }.to raise_error(/can't modify frozen/) end end context 'when creating an root ssl context with CA certs' do let(:config) { { cacerts: [], crls: [], revocation: false } } it 'accepts empty list of certs and crls' do sslctx = subject.create_root_context(**config) expect(sslctx.cacerts).to eq([]) expect(sslctx.crls).to eq([]) end it 'accepts valid root certs' do certs = [cert_fixture('ca.pem')] sslctx = subject.create_root_context(**config.merge(cacerts: certs)) expect(sslctx.cacerts).to eq(certs) end it 'accepts valid intermediate certs' do certs = [cert_fixture('ca.pem'), cert_fixture('intermediate.pem')] sslctx = subject.create_root_context(**config.merge(cacerts: certs)) expect(sslctx.cacerts).to eq(certs) end it 'accepts expired CA certs' do expired = [cert_fixture('ca.pem'), cert_fixture('intermediate.pem')] expired.each { |x509| x509.not_after = Time.at(0) } sslctx = subject.create_root_context(**config.merge(cacerts: expired)) expect(sslctx.cacerts).to eq(expired) end it 'raises if the frozen context is modified' do sslctx = subject.create_root_context(**config) expect { sslctx.verify_peer = false }.to raise_error(/can't modify frozen/) end it 'verifies peer' do sslctx = subject.create_root_context(**config) expect(sslctx.verify_peer).to eq(true) end end context 'when creating a system ssl context' do it 'accepts empty list of CA certs' do sslctx = subject.create_system_context(cacerts: []) expect(sslctx.cacerts).to eq([]) end it 'accepts valid root certs' do certs = [cert_fixture('ca.pem')] sslctx = subject.create_system_context(cacerts: certs) expect(sslctx.cacerts).to eq(certs) end it 'accepts valid intermediate certs' do certs = [cert_fixture('ca.pem'), cert_fixture('intermediate.pem')] sslctx = subject.create_system_context(cacerts: certs) expect(sslctx.cacerts).to eq(certs) end it 'accepts expired CA certs' do expired = [cert_fixture('ca.pem'), cert_fixture('intermediate.pem')] expired.each { |x509| x509.not_after = Time.at(0) } sslctx = subject.create_system_context(cacerts: expired) expect(sslctx.cacerts).to eq(expired) end it 'raises if the frozen context is modified' do sslctx = subject.create_system_context(cacerts: []) expect { sslctx.verify_peer = false }.to raise_error(/can't modify frozen/) end it 'trusts system ca store' do expect_any_instance_of(OpenSSL::X509::Store).to receive(:set_default_paths) subject.create_system_context(cacerts: []) end it 'verifies peer' do sslctx = subject.create_system_context(cacerts: []) expect(sslctx.verify_peer).to eq(true) end it 'disable revocation' do sslctx = subject.create_system_context(cacerts: []) expect(sslctx.revocation).to eq(false) end it 'sets client cert and private key to nil' do sslctx = subject.create_system_context(cacerts: []) expect(sslctx.client_cert).to be_nil expect(sslctx.private_key).to be_nil end it 'trusts additional system certs' do path = tmpfile('system_cacerts') File.write(path, cert_fixture('ca.pem').to_pem) expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_file).with(path) subject.create_system_context(cacerts: [], path: path) end it 'ignores empty files' do path = tmpfile('system_cacerts') FileUtils.touch(path) subject.create_system_context(cacerts: [], path: path) expect(@logs).to eq([]) end it 'prints an error if it is not a file' do path = tmpdir('system_cacerts') subject.create_system_context(cacerts: [], path: path) expect(@logs).to include(an_object_having_attributes(level: :warning, message: /^The 'ssl_trust_store' setting does not refer to a file and will be ignored/)) end end context 'when creating an ssl context with crls' do let(:config) { { cacerts: global_cacerts, crls: global_crls} } it 'accepts valid CRLs' do certs = [cert_fixture('ca.pem')] crls = [crl_fixture('crl.pem')] sslctx = subject.create_root_context(**config.merge(cacerts: certs, crls: crls)) expect(sslctx.crls).to eq(crls) end it 'accepts valid CRLs for intermediate certs' do certs = [cert_fixture('ca.pem'), cert_fixture('intermediate.pem')] crls = [crl_fixture('crl.pem'), crl_fixture('intermediate-crl.pem')] sslctx = subject.create_root_context(**config.merge(cacerts: certs, crls: crls)) expect(sslctx.crls).to eq(crls) end it 'accepts expired CRLs' do expired = [crl_fixture('crl.pem'), crl_fixture('intermediate-crl.pem')] expired.each { |x509| x509.last_update = Time.at(0) } sslctx = subject.create_root_context(**config.merge(crls: expired)) expect(sslctx.crls).to eq(expired) end it 'verifies peer' do sslctx = subject.create_root_context(**config) expect(sslctx.verify_peer).to eq(true) end end context 'when creating an ssl context with client certs' do let(:client_cert) { cert_fixture('signed.pem') } let(:private_key) { key_fixture('signed-key.pem') } let(:config) { { cacerts: global_cacerts, crls: global_crls, client_cert: client_cert, private_key: private_key } } it 'raises if CA certs are missing' do expect { subject.create_context(**config.merge(cacerts: nil)) }.to raise_error(ArgumentError, /CA certs are missing/) end it 'raises if CRLs are are missing' do expect { subject.create_context(**config.merge(crls: nil)) }.to raise_error(ArgumentError, /CRLs are missing/) end it 'raises if private key is missing' do expect { subject.create_context(**config.merge(private_key: nil)) }.to raise_error(ArgumentError, /Private key is missing/) end it 'raises if client cert is missing' do expect { subject.create_context(**config.merge(client_cert: nil)) }.to raise_error(ArgumentError, /Client cert is missing/) end it 'accepts RSA keys' do sslctx = subject.create_context(**config) expect(sslctx.private_key).to eq(private_key) end it 'accepts EC keys' do ec_key = ec_key_fixture('ec-key.pem') ec_cert = cert_fixture('ec.pem') sslctx = subject.create_context(**config.merge(client_cert: ec_cert, private_key: ec_key)) expect(sslctx.private_key).to eq(ec_key) end it 'raises if private key is unsupported' do dsa_key = OpenSSL::PKey::DSA.new expect { subject.create_context(**config.merge(private_key: dsa_key)) }.to raise_error(Puppet::SSL::SSLError, /Unsupported key 'OpenSSL::PKey::DSA'/) end it 'resolves the client chain from leaf to root' do sslctx = subject.create_context(**config) expect( sslctx.client_chain.map(&:subject).map(&:to_utf8) ).to eq(['CN=signed', 'CN=Test CA Subauthority', 'CN=Test CA']) end it 'raises if client cert signature is invalid' do client_cert.sign(wrong_key, OpenSSL::Digest::SHA256.new) expect { subject.create_context(**config.merge(client_cert: client_cert)) }.to raise_error(Puppet::SSL::CertVerifyError, "Invalid signature for certificate 'CN=signed'") end it 'raises if client cert and private key are mismatched' do expect { subject.create_context(**config.merge(private_key: wrong_key)) }.to raise_error(Puppet::SSL::SSLError, "The certificate for 'CN=signed' does not match its private key") end it "raises if client cert's public key has been replaced" do expect { subject.create_context(**config.merge(client_cert: cert_fixture('tampered-cert.pem'))) }.to raise_error(Puppet::SSL::CertVerifyError, "Invalid signature for certificate 'CN=signed'") end # This option is only available in openssl 1.1 it 'raises if root cert signature is invalid', if: defined?(OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE) do ca = global_cacerts.first ca.sign(wrong_key, OpenSSL::Digest::SHA256.new) expect { subject.create_context(**config.merge(cacerts: global_cacerts)) }.to raise_error(Puppet::SSL::CertVerifyError, "Invalid signature for certificate 'CN=Test CA'") end it 'raises if intermediate CA signature is invalid' do int = global_cacerts.last int.sign(wrong_key, OpenSSL::Digest::SHA256.new) expect { subject.create_context(**config.merge(cacerts: global_cacerts)) }.to raise_error(Puppet::SSL::CertVerifyError, "Invalid signature for certificate 'CN=Test CA Subauthority'") end it 'raises if CRL signature for root CA is invalid', unless: Puppet::Util::Platform.jruby? do crl = global_crls.first crl.sign(wrong_key, OpenSSL::Digest::SHA256.new) expect { subject.create_context(**config.merge(crls: global_crls)) }.to raise_error(Puppet::SSL::CertVerifyError, "Invalid signature for CRL issued by 'CN=Test CA'") end it 'raises if CRL signature for intermediate CA is invalid', unless: Puppet::Util::Platform.jruby? do crl = global_crls.last crl.sign(wrong_key, OpenSSL::Digest::SHA256.new) expect { subject.create_context(**config.merge(crls: global_crls)) }.to raise_error(Puppet::SSL::CertVerifyError, "Invalid signature for CRL issued by 'CN=Test CA Subauthority'") end it 'raises if client cert is revoked' do expect { subject.create_context(**config.merge(private_key: key_fixture('revoked-key.pem'), client_cert: cert_fixture('revoked.pem'))) }.to raise_error(Puppet::SSL::CertVerifyError, "Certificate 'CN=revoked' is revoked") end it 'warns if intermediate issuer is missing' do expect(Puppet).to receive(:warning).with("The issuer 'CN=Test CA Subauthority' of certificate 'CN=signed' cannot be found locally") subject.create_context(**config.merge(cacerts: [cert_fixture('ca.pem')])) end it 'raises if root issuer is missing' do expect { subject.create_context(**config.merge(cacerts: [cert_fixture('intermediate.pem')])) }.to raise_error(Puppet::SSL::CertVerifyError, "The issuer 'CN=Test CA' of certificate 'CN=Test CA Subauthority' is missing") end it 'raises if cert is not valid yet', unless: Puppet::Util::Platform.jruby? do client_cert.not_before = Time.now + (5 * 60 * 60) expect { subject.create_context(**config.merge(client_cert: client_cert)) }.to raise_error(Puppet::SSL::CertVerifyError, "The certificate 'CN=signed' is not yet valid, verify time is synchronized") end it 'raises if cert is expired', unless: Puppet::Util::Platform.jruby? do client_cert.not_after = Time.at(0) expect { subject.create_context(**config.merge(client_cert: client_cert)) }.to raise_error(Puppet::SSL::CertVerifyError, "The certificate 'CN=signed' has expired, verify time is synchronized") end it 'raises if crl is not valid yet', unless: Puppet::Util::Platform.jruby? do future_crls = global_crls # invalidate the CRL issued by the root future_crls.first.last_update = Time.now + (5 * 60 * 60) expect { subject.create_context(**config.merge(crls: future_crls)) }.to raise_error(Puppet::SSL::CertVerifyError, "The CRL issued by 'CN=Test CA' is not yet valid, verify time is synchronized") end it 'raises if crl is expired', unless: Puppet::Util::Platform.jruby? do past_crls = global_crls # invalidate the CRL issued by the root past_crls.first.next_update = Time.at(0) expect { subject.create_context(**config.merge(crls: past_crls)) }.to raise_error(Puppet::SSL::CertVerifyError, "The CRL issued by 'CN=Test CA' has expired, verify time is synchronized") end it 'raises if the root CRL is missing' do crls = [crl_fixture('intermediate-crl.pem')] expect { subject.create_context(**config.merge(crls: crls, revocation: :chain)) }.to raise_error(Puppet::SSL::CertVerifyError, "The CRL issued by 'CN=Test CA' is missing") end it 'raises if the intermediate CRL is missing' do crls = [crl_fixture('crl.pem')] expect { subject.create_context(**config.merge(crls: crls)) }.to raise_error(Puppet::SSL::CertVerifyError, "The CRL issued by 'CN=Test CA Subauthority' is missing") end it "doesn't raise if the root CRL is missing and we're just checking the leaf" do crls = [crl_fixture('intermediate-crl.pem')] subject.create_context(**config.merge(crls: crls, revocation: :leaf)) end it "doesn't raise if the intermediate CRL is missing and revocation checking is disabled" do crls = [crl_fixture('crl.pem')] subject.create_context(**config.merge(crls: crls, revocation: false)) end it "doesn't raise if both CRLs are missing and revocation checking is disabled" do subject.create_context(**config.merge(crls: [], revocation: false)) end # OpenSSL < 1.1 does not verify basicConstraints it "raises if root CA's isCA basic constraint is false", unless: Puppet::Util::Platform.jruby? || OpenSSL::OPENSSL_VERSION_NUMBER < 0x10100000 do certs = [cert_fixture('bad-basic-constraints.pem'), cert_fixture('intermediate.pem')] expect { subject.create_context(**config.merge(cacerts: certs, crls: [], revocation: false)) }.to raise_error(Puppet::SSL::CertVerifyError, "Certificate 'CN=Test CA' failed verification (24): invalid CA certificate") end # OpenSSL < 1.1 does not verify basicConstraints it "raises if intermediate CA's isCA basic constraint is false", unless: Puppet::Util::Platform.jruby? || OpenSSL::OPENSSL_VERSION_NUMBER < 0x10100000 do certs = [cert_fixture('ca.pem'), cert_fixture('bad-int-basic-constraints.pem')] expect { subject.create_context(**config.merge(cacerts: certs, crls: [], revocation: false)) }.to raise_error(Puppet::SSL::CertVerifyError, "Certificate 'CN=Test CA Subauthority' failed verification (24): invalid CA certificate") end it 'accepts CA certs in any order' do sslctx = subject.create_context(**config.merge(cacerts: global_cacerts.reverse)) # certs in ruby+openssl 1.0.x are not comparable, so compare subjects expect(sslctx.client_chain.map(&:subject).map(&:to_utf8)).to contain_exactly('CN=Test CA', 'CN=Test CA Subauthority', 'CN=signed') end it 'accepts CRLs in any order' do sslctx = subject.create_context(**config.merge(crls: global_crls.reverse)) # certs in ruby+openssl 1.0.x are not comparable, so compare subjects expect(sslctx.client_chain.map(&:subject).map(&:to_utf8)).to contain_exactly('CN=Test CA', 'CN=Test CA Subauthority', 'CN=signed') end it 'raises if the frozen context is modified' do sslctx = subject.create_context(**config) expect { sslctx.verify_peer = false }.to raise_error(/can't modify frozen/) end it 'verifies peer' do sslctx = subject.create_context(**config) expect(sslctx.verify_peer).to eq(true) end end context 'when loading an ssl context' do let(:client_cert) { cert_fixture('signed.pem') } let(:private_key) { key_fixture('signed-key.pem') } let(:doesnt_exist) { '/does/not/exist' } before :each do Puppet[:localcacert] = file_containing('global_cacerts', global_cacerts.first.to_pem) Puppet[:hostcrl] = file_containing('global_crls', global_crls.first.to_pem) Puppet[:certname] = 'signed' Puppet[:privatekeydir] = tmpdir('privatekeydir') File.write(File.join(Puppet[:privatekeydir], 'signed.pem'), private_key.to_pem) Puppet[:certdir] = tmpdir('privatekeydir') File.write(File.join(Puppet[:certdir], 'signed.pem'), client_cert.to_pem) end it 'raises if CA certs are missing' do Puppet[:localcacert] = doesnt_exist expect { subject.load_context }.to raise_error(Puppet::Error, /The CA certificates are missing from/) end it 'raises if the CRL is missing' do Puppet[:hostcrl] = doesnt_exist expect { subject.load_context }.to raise_error(Puppet::Error, /The CRL is missing from/) end it 'does not raise if the CRL is missing and revocation is disabled' do Puppet[:hostcrl] = doesnt_exist subject.load_context(revocation: false) end it 'raises if the private key is missing' do Puppet[:privatekeydir] = doesnt_exist expect { subject.load_context }.to raise_error(Puppet::Error, /The private key is missing from/) end it 'raises if the client cert is missing' do Puppet[:certdir] = doesnt_exist expect { subject.load_context }.to raise_error(Puppet::Error, /The client certificate is missing from/) end it 'loads the private key and client cert' do ssl_context = subject.load_context expect(ssl_context.private_key).to be_an(OpenSSL::PKey::RSA) expect(ssl_context.client_cert).to be_an(OpenSSL::X509::Certificate) end it 'loads a password protected key and client cert' do FileUtils.cp(File.join(PuppetSpec::FIXTURE_DIR, 'ssl', 'encrypted-key.pem'), File.join(Puppet[:privatekeydir], 'signed.pem')) ssl_context = subject.load_context(password: '74695716c8b6') expect(ssl_context.private_key).to be_an(OpenSSL::PKey::RSA) expect(ssl_context.client_cert).to be_an(OpenSSL::X509::Certificate) end it 'raises if the password is incorrect' do FileUtils.cp(File.join(PuppetSpec::FIXTURE_DIR, 'ssl', 'encrypted-key.pem'), File.join(Puppet[:privatekeydir], 'signed.pem')) expect { subject.load_context(password: 'wrongpassword') }.to raise_error(Puppet::SSL::SSLError, /Failed to load private key for host 'signed': Could not parse PKey/) end end context 'when verifying requests' do let(:csr) { request_fixture('request.pem') } it 'accepts valid requests' do private_key = key_fixture('request-key.pem') expect(subject.verify_request(csr, private_key.public_key)).to eq(csr) end it "raises if the CSR was signed by a private key that doesn't match public key" do expect { subject.verify_request(csr, wrong_key.public_key) }.to raise_error(Puppet::SSL::SSLError, "The CSR for host 'CN=pending' does not match the public key") end it "raises if the CSR was tampered with" do csr = request_fixture('tampered-csr.pem') expect { subject.verify_request(csr, csr.public_key) }.to raise_error(Puppet::SSL::SSLError, "The CSR for host 'CN=signed' does not match the public key") end end end