Skip to content

Commit 8c41acb

Browse files
committed
(PUP-11522) Include client cert if requested
It's now possible to create a `system` context that performs mutual TLS and trusts certs in the system store and/or the external store, based on `Puppet[:ssl_trust_store]`. If the agent doesn't have a client cert yet, then it will be ignored. For example, when running `puppet apply` without a client certificate and trying to apply a file resource with an "https" source.
1 parent cad2fc9 commit 8c41acb

File tree

2 files changed

+55
-3
lines changed

2 files changed

+55
-3
lines changed

lib/puppet/ssl/ssl_provider.rb

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,18 @@ def create_root_context(cacerts:, crls: [], revocation: Puppet[:certificate_revo
4242
# refers to the cacerts bundle in the puppet-agent package.
4343
#
4444
# Connections made from the returned context will authenticate the server,
45-
# i.e. `VERIFY_PEER`, but will not use a client certificate and will not
46-
# perform revocation checking.
45+
# i.e. `VERIFY_PEER`, but will not use a client certificate (unless requested)
46+
# and will not perform revocation checking.
4747
#
4848
# @param cacerts [Array<OpenSSL::X509::Certificate>] Array of trusted CA certs
4949
# @param path [String, nil] A file containing additional trusted CA certs.
50+
# @param include_client_cert [true, false] If true, the client cert will be added to the context
51+
# allowing mutual TLS authentication. The default is false. If the client cert doesn't exist
52+
# then the option will be ignored.
5053
# @return [Puppet::SSL::SSLContext] A context to use to create connections
5154
# @raise (see #create_context)
5255
# @api private
53-
def create_system_context(cacerts:, path: Puppet[:ssl_trust_store])
56+
def create_system_context(cacerts:, path: Puppet[:ssl_trust_store], include_client_cert: false)
5457
store = create_x509_store(cacerts, [], false, include_system_store: true)
5558

5659
if path
@@ -71,6 +74,30 @@ def create_system_context(cacerts:, path: Puppet[:ssl_trust_store])
7174
end
7275
end
7376

77+
if include_client_cert
78+
cert_provider = Puppet::X509::CertProvider.new
79+
private_key = cert_provider.load_private_key(Puppet[:certname], required: false)
80+
client_cert = cert_provider.load_client_cert(Puppet[:certname], required: false)
81+
82+
if private_key && client_cert
83+
client_chain = verify_cert_with_store(store, client_cert)
84+
85+
if !private_key.is_a?(OpenSSL::PKey::RSA) && !private_key.is_a?(OpenSSL::PKey::EC)
86+
raise Puppet::SSL::SSLError, _("Unsupported key '%{type}'") % { type: private_key.class.name }
87+
end
88+
89+
unless client_cert.check_private_key(private_key)
90+
raise Puppet::SSL::SSLError, _("The certificate for '%{name}' does not match its private key") % { name: subject(client_cert) }
91+
end
92+
93+
return Puppet::SSL::SSLContext.new(
94+
store: store, cacerts: cacerts, crls: [],
95+
private_key: private_key, client_cert: client_cert, client_chain: client_chain,
96+
revocation: false
97+
).freeze
98+
end
99+
end
100+
74101
Puppet::SSL::SSLContext.new(store: store, cacerts: cacerts, crls: [], revocation: false).freeze
75102
end
76103

spec/unit/ssl/ssl_provider_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,31 @@
144144
expect(sslctx.private_key).to be_nil
145145
end
146146

147+
it 'includes the client cert and private key when requested' do
148+
Puppet[:hostcert] = fixtures('ssl/signed.pem')
149+
Puppet[:hostprivkey] = fixtures('ssl/signed-key.pem')
150+
sslctx = subject.create_system_context(cacerts: [], include_client_cert: true)
151+
expect(sslctx.client_cert).to be_an(OpenSSL::X509::Certificate)
152+
expect(sslctx.private_key).to be_an(OpenSSL::PKey::RSA)
153+
end
154+
155+
it 'ignores non-existent client cert and private key when requested' do
156+
Puppet[:certname] = 'doesnotexist'
157+
sslctx = subject.create_system_context(cacerts: [], include_client_cert: true)
158+
expect(sslctx.client_cert).to be_nil
159+
expect(sslctx.private_key).to be_nil
160+
end
161+
162+
it 'raises if client cert and private key are mismatched' do
163+
Puppet[:hostcert] = fixtures('ssl/signed.pem')
164+
Puppet[:hostprivkey] = fixtures('ssl/127.0.0.1-key.pem')
165+
166+
expect {
167+
subject.create_system_context(cacerts: [], include_client_cert: true)
168+
}.to raise_error(Puppet::SSL::SSLError,
169+
"The certificate for 'CN=signed' does not match its private key")
170+
end
171+
147172
it 'trusts additional system certs' do
148173
path = tmpfile('system_cacerts')
149174
File.write(path, cert_fixture('ca.pem').to_pem)

0 commit comments

Comments
 (0)