Skip to content

Commit bdee364

Browse files
committed
Add support for certificate_content and private_key_content parameters
The current implementation only allows you to pass a file name to the certificate and private_key parameters. When you are fetching certificates from vault or another secure store, you'll first have to save them to a file. This is very innconveniant. This PR add's the parameters certificate_content and private_key_content. These parameters are mutually exclusive from their file counterparts. With this change, you can now fetch a certificate and/or a password from vault (through a hiera lookup for example) and use it directly on the type. Because these values can be sensitive, both of the new parameters support passing the value as a sensitive data type.
1 parent 12d0564 commit bdee364

File tree

5 files changed

+184
-29
lines changed

5 files changed

+184
-29
lines changed

lib/puppet/provider/java_ks/keytool.rb

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,36 @@ def update
280280
end
281281

282282
def certificate
283-
@resource[:certificate]
283+
return @resource[:certificate] if @resource[:certificate]
284+
285+
# When no certificate file is specified, we infer the usage of
286+
# certificate content and create a tempfile containing this value.
287+
# we leave it to to the tempfile to clean it up after the pupet run exists.
288+
file = Tempfile.new('certificate')
289+
# Check if the specified value is a Sensitive data type. If so, unwrap it and use
290+
# the value.
291+
content = @resource[:certificate_content].respond_to?(:unwrap) ? @resource[:certificate_content].unwrap : @resource[:certificate_content]
292+
file.write(content)
293+
file.close
294+
file.path
284295
end
285296

286297
def private_key
287-
@resource[:private_key]
298+
return @resource[:private_key] if @resource[:private_key]
299+
if @resource[:private_key_content]
300+
301+
302+
# When no private key file is specified, we infer the usage of
303+
# private key content and create a tempfile containing this value.
304+
# we leave it to to the tempfile to clean it up after the pupet run exists.
305+
file = Tempfile.new('private_key')
306+
# Check if the specified value is a Sensitive data type. If so, unwrap it and use
307+
# the value.
308+
content = @resource[:private_key_content].respond_to?(:unwrap) ? @resource[:private_key_content].unwrap : @resource[:private_key_content]
309+
file.write(content)
310+
file.close
311+
file.path
312+
end
288313
end
289314

290315
def private_key_type

lib/puppet/type/java_ks.rb

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,13 @@ def insync?(is)
6666
end
6767

6868
newparam(:certificate) do
69-
desc 'A server certificate, followed by zero or more intermediate certificate authorities.
70-
All certificates will be placed in the keystore. This will autorequire the specified file.'
69+
desc 'A file containing a server certificate, followed by zero or more intermediate certificate authorities.
70+
All certificates will be placed in the keystore. This will autorequire the specified file.'
71+
end
7172

72-
isrequired
73+
newparam(:certificate_content) do
74+
desc 'A string containing a server certificate, followed by zero or more intermediate certificate authorities.
75+
All certificates will be placed in the keystore.'
7376
end
7477

7578
newparam(:storetype) do
@@ -82,7 +85,16 @@ def insync?(is)
8285
newparam(:private_key) do
8386
desc 'If you want an application to be a server and encrypt traffic,
8487
you will need a private key. Private key entries in a keystore must be
85-
accompanied by a signed certificate for the keytool provider. This will autorequire the specified file.'
88+
accompanied by a signed certificate for the keytool provider. This parameter
89+
allows you to specify the file name containing the private key. This will autorequire
90+
the specified file.'
91+
end
92+
93+
newparam(:private_key_content) do
94+
desc 'If you want an application to be a server and encrypt traffic,
95+
you will need a private key. Private key entries in a keystore must be
96+
accompanied by a signed certificate for the keytool provider. This parameter allows you to specify the content
97+
of the private key.'
8698
end
8799

88100
newparam(:private_key_type) do
@@ -228,6 +240,18 @@ def self.title_patterns
228240
end
229241

230242
validate do
243+
unless value(:certificate) || value(:certificate_content)
244+
raise Puppet::Error, "You must pass one of 'certificate' or 'certificate_content'"
245+
end
246+
247+
if value(:certificate) && value(:certificate_content)
248+
raise Puppet::Error, "You must pass either 'certificate' or 'certificate_content', not both."
249+
end
250+
251+
if value(:private_key) && value(:private_key_content)
252+
raise Puppet::Error, "You must pass either 'private_key' or 'private_key_content', not both."
253+
end
254+
231255
if value(:password) && value(:password_file)
232256
raise Puppet::Error, "You must pass either 'password' or 'password_file', not both."
233257
end

spec/acceptance/content_spec.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper_acceptance'
4+
5+
RSpec.shared_examples 'a private key creator' do |sensitive|
6+
it 'creates a private key' do
7+
pp = if sensitive
8+
<<-MANIFEST
9+
java_ks { 'broker.example.com:#{temp_dir}private_key.ts':
10+
ensure => #{@ensure_ks},
11+
certificate_content => "#{ca_content}",
12+
private_key_content => "#{priv_key_content}",
13+
password => 'puppet',
14+
path => #{@resource_path},
15+
}
16+
MANIFEST
17+
else
18+
<<-MANIFEST
19+
java_ks { 'broker.example.com:#{temp_dir}private_key.ts':
20+
ensure => #{@ensure_ks},
21+
certificate_content => Sensitive("#{ca_content}"),
22+
private_key_content => Sensitive("#{priv_key_content}"),
23+
password => 'puppet',
24+
path => #{@resource_path},
25+
}
26+
MANIFEST
27+
end
28+
idempotent_apply(pp)
29+
end
30+
31+
expectations = [
32+
%r{Alias name: broker\.example\.com},
33+
%r{Entry type: (keyEntry|PrivateKeyEntry)},
34+
%r{CN=Test CA},
35+
]
36+
it 'verifies the private key' do
37+
run_shell(keytool_command("-list -v -keystore #{temp_dir}private_key.ts -storepass puppet"), expect_failures: true) do |r|
38+
expectations.each do |expect|
39+
expect(r.stdout).to match(expect)
40+
end
41+
end
42+
end
43+
end
44+
45+
describe 'using certificate_content and private_key_content' do
46+
include_context 'common variables'
47+
let(:ca_content) { File.read('spec/acceptance/certs/ca.pem') }
48+
let(:priv_key_content) { File.read('spec/acceptance/certs/privkey.pem') }
49+
50+
context 'Using data type String' do
51+
it_behaves_like 'a private key creator', false
52+
end
53+
54+
context 'Using data type Sensitive' do
55+
it_behaves_like 'a private key creator', true
56+
end
57+
end

spec/unit/puppet/provider/java_ks/keytool_spec.rb

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
write: true,
4747
flush: true,
4848
close!: true,
49+
close: true,
4950
path: "#{temp_dir}testing.stuff")
5051
allow(Tempfile).to receive(:new).and_return(tempfile)
5152
end
@@ -97,29 +98,53 @@
9798

9899
describe 'when importing a private key and certifcate' do
99100
describe '#to_pkcs12' do
100-
it 'converts a certificate to a pkcs12 file' do
101-
sleep 0.1 # due to https://github.com/mitchellh/vagrant/issues/5056
102-
testing_key = OpenSSL::PKey::RSA.new 1024
103-
testing_ca = OpenSSL::X509::Certificate.new
104-
testing_ca.serial = 1
105-
testing_ca.public_key = testing_key.public_key
106-
testing_subj = '/CN=Test CA/ST=Denial/L=Springfield/O=Dis/CN=www.example.com'
107-
testing_ca.subject = OpenSSL::X509::Name.parse testing_subj
108-
testing_ca.issuer = testing_ca.subject
109-
testing_ca.not_before = Time.now
110-
testing_ca.not_after = testing_ca.not_before + 360
111-
testing_ca.sign(testing_key, OpenSSL::Digest::SHA256.new)
112-
113-
allow(provider).to receive(:password).and_return(resource[:password])
114-
allow(File).to receive(:read).with(resource[:private_key]).and_return('private key')
115-
allow(File).to receive(:read).with(resource[:certificate], hash_including(encoding: 'ISO-8859-1')).and_return(testing_ca.to_pem)
116-
expect(OpenSSL::PKey::RSA).to receive(:new).with('private key', 'puppet').and_return('priv_obj')
117-
expect(OpenSSL::X509::Certificate).to receive(:new).with(testing_ca.to_pem.chomp).and_return('cert_obj')
118-
119-
pkcs_double = BogusPkcs.new
120-
expect(pkcs_double).to receive(:to_der)
121-
expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', []).and_return(pkcs_double)
122-
provider.to_pkcs12("#{temp_dir}testing.stuff")
101+
sleep 0.1 # due to https://github.com/mitchellh/vagrant/issues/5056
102+
testing_key = OpenSSL::PKey::RSA.new 1024
103+
testing_ca = OpenSSL::X509::Certificate.new
104+
testing_ca.serial = 1
105+
testing_ca.public_key = testing_key.public_key
106+
testing_subj = '/CN=Test CA/ST=Denial/L=Springfield/O=Dis/CN=www.example.com'
107+
testing_ca.subject = OpenSSL::X509::Name.parse testing_subj
108+
testing_ca.issuer = testing_ca.subject
109+
testing_ca.not_before = Time.now
110+
testing_ca.not_after = testing_ca.not_before + 360
111+
testing_ca.sign(testing_key, OpenSSL::Digest::SHA256.new)
112+
113+
context "Using the file based parameters for certificate and private_key" do
114+
it 'converts a certificate to a pkcs12 file' do
115+
allow(provider).to receive(:password).and_return(resource[:password])
116+
allow(File).to receive(:read).with(resource[:private_key]).and_return('private key')
117+
allow(File).to receive(:read).with(resource[:certificate], hash_including(encoding: 'ISO-8859-1')).and_return(testing_ca.to_pem)
118+
expect(OpenSSL::PKey::RSA).to receive(:new).with('private key', 'puppet').and_return('priv_obj')
119+
expect(OpenSSL::X509::Certificate).to receive(:new).with(testing_ca.to_pem.chomp).and_return('cert_obj')
120+
121+
pkcs_double = BogusPkcs.new
122+
expect(pkcs_double).to receive(:to_der)
123+
expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', []).and_return(pkcs_double)
124+
provider.to_pkcs12("#{temp_dir}testing.stuff")
125+
end
126+
end
127+
128+
context "Using content based parameters for certificate and private_key" do
129+
let(:params) {
130+
global_params.tap {|h| [:certificate, :private_key].each {|k| h.delete(k)}}.merge(
131+
:private_key_content => 'private_key',
132+
:certificate_content => testing_ca.to_pem,
133+
)
134+
}
135+
136+
it 'converts a certificate to a pkcs12 file' do
137+
allow(provider).to receive(:password).and_return(resource[:password])
138+
allow(File).to receive(:read).with('/tmp/testing.stuff').ordered.and_return('private key')
139+
allow(File).to receive(:read).with('/tmp/testing.stuff', hash_including(encoding: 'ISO-8859-1')).ordered.and_return(testing_ca.to_pem)
140+
expect(OpenSSL::PKey::RSA).to receive(:new).with('private key', 'puppet').and_return('priv_obj')
141+
expect(OpenSSL::X509::Certificate).to receive(:new).with(testing_ca.to_pem.chomp).and_return('cert_obj')
142+
143+
pkcs_double = BogusPkcs.new
144+
expect(pkcs_double).to receive(:to_der)
145+
expect(OpenSSL::PKCS12).to receive(:create).with(resource[:password], resource[:name], 'priv_obj', 'cert_obj', []).and_return(pkcs_double)
146+
provider.to_pkcs12("#{temp_dir}testing.stuff")
147+
end
123148
end
124149
end
125150

spec/unit/puppet/type/java_ks_spec.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,30 @@
117117
}.to raise_error(Puppet::Error)
118118
end
119119

120+
it 'fails if both :certificate and :certificate_content are provided' do
121+
jks = jks_resource.dup
122+
jks[:certificate_content] = 'certificate_content'
123+
expect {
124+
described_class.new(jks)
125+
}.to raise_error(Puppet::Error, %r{You must pass either})
126+
end
127+
128+
it 'fails if neither :certificate or :certificate_content is provided' do
129+
jks = jks_resource.dup
130+
jks.delete(:certificate)
131+
expect {
132+
described_class.new(jks)
133+
}.to raise_error(Puppet::Error, %r{You must pass one of})
134+
end
135+
136+
it 'fails if both :private_key and :private_key_content are provided' do
137+
jks = jks_resource.dup
138+
jks[:private_key_content] = 'private_content'
139+
expect {
140+
described_class.new(jks)
141+
}.to raise_error(Puppet::Error, %r{You must pass either})
142+
end
143+
120144
it 'fails if both :password and :password_file are provided' do
121145
jks = jks_resource.dup
122146
jks[:password_file] = '/path/to/password_file'

0 commit comments

Comments
 (0)