trocla 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.travis.yml +3 -7
- data/CHANGELOG.md +83 -0
- data/Gemfile +7 -16
- data/README.md +63 -50
- data/bin/trocla +35 -14
- data/ext/redhat/rubygem-trocla.spec +120 -0
- data/lib/VERSION +1 -1
- data/lib/trocla.rb +49 -10
- data/lib/trocla/default_config.yaml +10 -0
- data/lib/trocla/formats.rb +14 -0
- data/lib/trocla/formats/bcrypt.rb +2 -1
- data/lib/trocla/formats/sshkey.rb +46 -0
- data/lib/trocla/formats/x509.rb +37 -10
- data/lib/trocla/store.rb +16 -0
- data/lib/trocla/stores/memory.rb +9 -0
- data/lib/trocla/stores/moneta.rb +30 -0
- data/lib/trocla/stores/vault.rb +50 -0
- data/lib/trocla/util.rb +4 -0
- data/spec/spec_helper.rb +19 -1
- data/spec/trocla/formats/sshkey_spec.rb +52 -0
- data/spec/trocla/formats/x509_spec.rb +107 -20
- data/spec/trocla/util_spec.rb +8 -0
- data/spec/trocla_spec.rb +227 -100
- data/trocla.gemspec +39 -38
- metadata +44 -12
data/lib/trocla/util.rb
CHANGED
@@ -24,6 +24,7 @@ class Trocla
|
|
24
24
|
'numeric' => numeric,
|
25
25
|
'hexadecimal' => hexadecimal,
|
26
26
|
'consolesafe' => consolesafe,
|
27
|
+
'typesafe' => typesafe,
|
27
28
|
}
|
28
29
|
h.each { |k, v| h[k] = v.uniq }
|
29
30
|
end
|
@@ -50,6 +51,9 @@ class Trocla
|
|
50
51
|
def numeric
|
51
52
|
@numeric ||= ('0'..'9').to_a
|
52
53
|
end
|
54
|
+
def typesafe
|
55
|
+
@typesafe ||= ('a'..'x').to_a - ['i'] - ['l'] + ('A'..'X').to_a - ['I'] - ['L'] + ('1'..'9').to_a
|
56
|
+
end
|
53
57
|
def special_chars
|
54
58
|
@special_chars ||= "*()&![]{}-".split(//)
|
55
59
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
2
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'jruby' if Object.const_defined?(:RUBY_ENGINE) and RUBY_ENGINE == 'jruby'
|
3
4
|
require 'rspec'
|
4
5
|
require 'rspec/pending_for'
|
5
6
|
require 'yaml'
|
@@ -35,6 +36,11 @@ RSpec.shared_examples "encryption_basics" do
|
|
35
36
|
expect(@trocla.get_password('some_pass', 'plain')).to eql('super secret')
|
36
37
|
end
|
37
38
|
|
39
|
+
it "resets passwords" do
|
40
|
+
@trocla.set_password('some_pass', 'plain', 'super secret')
|
41
|
+
expect(@trocla.reset_password('some_pass', 'plain')).not_to eql('super secret')
|
42
|
+
end
|
43
|
+
|
38
44
|
end
|
39
45
|
describe 'deleting' do
|
40
46
|
it "plain" do
|
@@ -225,7 +231,13 @@ RSpec.shared_examples 'store_validation' do |store|
|
|
225
231
|
end
|
226
232
|
|
227
233
|
def default_config
|
228
|
-
@default_config ||=
|
234
|
+
@default_config ||= begin
|
235
|
+
config_path = [
|
236
|
+
File.expand_path(base_dir+'/lib/trocla/default_config.yaml'),
|
237
|
+
File.expand_path(File.dirname($LOADED_FEATURES.grep(/trocla.rb/)[0])+'/trocla/default_config.yaml'),
|
238
|
+
].find { |p| File.exists?(p) }
|
239
|
+
YAML.load(File.read(config_path))
|
240
|
+
end
|
229
241
|
end
|
230
242
|
|
231
243
|
def test_config
|
@@ -282,3 +294,9 @@ end
|
|
282
294
|
def remove_yaml_store
|
283
295
|
File.unlink(trocla_yaml_file)
|
284
296
|
end
|
297
|
+
class Trocla::Formats::Sleep < Trocla::Formats::Base
|
298
|
+
def format(plain_password,options={})
|
299
|
+
sleep options['sleep'] ||= 0
|
300
|
+
(options['sleep'] + 1 ).times.collect{ plain_password }.join(' ')
|
301
|
+
end
|
302
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe "Trocla::Format::Sshkey" do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
|
7
|
+
@trocla = Trocla.new
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:sshkey_options) do
|
11
|
+
{
|
12
|
+
'type' => 'rsa',
|
13
|
+
'bits' => 4096,
|
14
|
+
'comment' => 'My ssh key'
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "sshkey" do
|
19
|
+
it "is able to create an ssh keypair without options" do
|
20
|
+
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', {})
|
21
|
+
expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
|
22
|
+
expect(sshkey).to match(/ssh-/)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "is able to create an ssh keypair with options" do
|
26
|
+
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', sshkey_options)
|
27
|
+
expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
|
28
|
+
expect(sshkey).to match(/ssh-/)
|
29
|
+
expect(sshkey).to end_with('My ssh key')
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'supports fetching only the priv key' do
|
33
|
+
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'render' => {'privonly' => true }})
|
34
|
+
expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
|
35
|
+
expect(sshkey).not_to match(/ssh-/)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'supports fetching only the pub key' do
|
39
|
+
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'render' => {'pubonly' => true }})
|
40
|
+
expect(sshkey).to start_with('ssh-rsa')
|
41
|
+
expect(sshkey).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "is able to create an ssh keypair with a passphrase" do
|
45
|
+
sshkey = @trocla.password('my_ssh_keypair', 'sshkey', { 'passphrase' => 'spec' })
|
46
|
+
expect(sshkey).to start_with('-----BEGIN RSA PRIVATE KEY-----')
|
47
|
+
expect(sshkey).to match(/ssh-/)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -42,9 +42,11 @@ describe "Trocla::Format::X509" do
|
|
42
42
|
# default size
|
43
43
|
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
|
44
44
|
expect(cert.public_key.n.num_bytes * 8).to eq(4096)
|
45
|
-
expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
|
45
|
+
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
46
46
|
# it's a self signed cert and NOT a CA
|
47
|
-
|
47
|
+
# Change of behavior on openssl side: https://github.com/openssl/openssl/issues/15146
|
48
|
+
validates_self_even_if_no_ca = Gem::Version.new(%x{openssl version}.split(' ')[1]) > Gem::Version.new('1.1.1g')
|
49
|
+
expect(verify(cert,cert)).to be validates_self_even_if_no_ca
|
48
50
|
|
49
51
|
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
|
50
52
|
expect(v).to eq('CA:FALSE')
|
@@ -60,7 +62,7 @@ describe "Trocla::Format::X509" do
|
|
60
62
|
ca = OpenSSL::X509::Certificate.new(ca_str)
|
61
63
|
# selfsigned?
|
62
64
|
expect(ca.issuer.to_s).to eq(ca.subject.to_s)
|
63
|
-
expect((Date.parse(ca.not_after.to_s) - Date.today).to_i).to eq(365)
|
65
|
+
expect((Date.parse(ca.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
64
66
|
expect(verify(ca,ca)).to be true
|
65
67
|
|
66
68
|
v = ca.extensions.find{|e| e.oid == 'basicConstraints' }.value
|
@@ -69,6 +71,47 @@ describe "Trocla::Format::X509" do
|
|
69
71
|
expect(ku).to match(/Certificate Sign/)
|
70
72
|
expect(ku).to match(/CRL Sign/)
|
71
73
|
end
|
74
|
+
it "is able to create a self signed cert without any keyUsage restrictions" do
|
75
|
+
cert_str = @trocla.password('my_shiny_selfsigned_without restrictions', 'x509', {
|
76
|
+
'CN' => 'This is my self-signed certificate',
|
77
|
+
'key_usages' => [],
|
78
|
+
})
|
79
|
+
cert = OpenSSL::X509::Certificate.new(cert_str)
|
80
|
+
# selfsigned?
|
81
|
+
expect(cert.issuer.to_s).to eq(cert.subject.to_s)
|
82
|
+
# default size
|
83
|
+
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
|
84
|
+
expect(cert.public_key.n.num_bytes * 8).to eq(4096)
|
85
|
+
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
86
|
+
# it's a self signed cert and NOT a CA, but has no keyUsage limitation
|
87
|
+
expect(verify(cert,cert)).to be true
|
88
|
+
|
89
|
+
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
|
90
|
+
expect(v).to_not eq('CA:TRUE')
|
91
|
+
expect(cert.extensions.find{|e| e.oid == 'keyUsage' }).to be_nil
|
92
|
+
end
|
93
|
+
|
94
|
+
it "is able to create a self signed cert with custom keyUsage restrictions" do
|
95
|
+
cert_str = @trocla.password('my_shiny_selfsigned_without restrictions', 'x509', {
|
96
|
+
'CN' => 'This is my self-signed certificate',
|
97
|
+
'key_usages' => [ 'cRLSign', ],
|
98
|
+
})
|
99
|
+
cert = OpenSSL::X509::Certificate.new(cert_str)
|
100
|
+
# selfsigned?
|
101
|
+
expect(cert.issuer.to_s).to eq(cert.subject.to_s)
|
102
|
+
# default size
|
103
|
+
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
|
104
|
+
expect(cert.public_key.n.num_bytes * 8).to eq(4096)
|
105
|
+
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
106
|
+
# it's a self signed cert and NOT a CA, as it's key is restricted to only CRL Sign
|
107
|
+
expect(verify(cert,cert)).to be false
|
108
|
+
|
109
|
+
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
|
110
|
+
expect(v).to_not eq('CA:TRUE')
|
111
|
+
ku = cert.extensions.find{|e| e.oid == 'keyUsage' }.value
|
112
|
+
expect(ku).to match(/CRL Sign/)
|
113
|
+
expect(ku).not_to match(/Certificate Sign/)
|
114
|
+
end
|
72
115
|
|
73
116
|
end
|
74
117
|
describe "x509 signed by a ca" do
|
@@ -80,7 +123,7 @@ describe "Trocla::Format::X509" do
|
|
80
123
|
cert_str = @trocla.password('mycert', 'x509', cert_options)
|
81
124
|
cert = OpenSSL::X509::Certificate.new(cert_str)
|
82
125
|
expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
|
83
|
-
expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
|
126
|
+
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
84
127
|
expect(verify(@ca,cert)).to be true
|
85
128
|
|
86
129
|
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
|
@@ -90,6 +133,30 @@ describe "Trocla::Format::X509" do
|
|
90
133
|
expect(ku).not_to match(/CRL Sign/)
|
91
134
|
end
|
92
135
|
|
136
|
+
it 'supports fetching only the key' do
|
137
|
+
cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'keyonly' => true }))
|
138
|
+
expect(cert_str).not_to match(/-----BEGIN CERTIFICATE-----/)
|
139
|
+
expect(cert_str).to match(/-----BEGIN RSA PRIVATE KEY-----/)
|
140
|
+
end
|
141
|
+
it 'supports fetching only the publickey' do
|
142
|
+
pkey_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'publickeyonly' => true }))
|
143
|
+
expect(pkey_str).not_to match(/-----BEGIN CERTIFICATE-----/)
|
144
|
+
expect(pkey_str).to match(/-----BEGIN PUBLIC KEY-----/)
|
145
|
+
end
|
146
|
+
it 'supports fetching only the cert' do
|
147
|
+
cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
|
148
|
+
expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
|
149
|
+
expect(cert_str).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
|
150
|
+
end
|
151
|
+
it 'supports fetching only the cert even a second time' do
|
152
|
+
cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
|
153
|
+
expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
|
154
|
+
expect(cert_str).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
|
155
|
+
cert_str = @trocla.password('mycert', 'x509', cert_options.merge('render' => {'certonly' => true }))
|
156
|
+
expect(cert_str).to match(/-----BEGIN CERTIFICATE-----/)
|
157
|
+
expect(cert_str).not_to match(/-----BEGIN RSA PRIVATE KEY-----/)
|
158
|
+
end
|
159
|
+
|
93
160
|
it 'does not simply increment the serial' do
|
94
161
|
cert_str = @trocla.password('mycert', 'x509', cert_options)
|
95
162
|
cert1 = OpenSSL::X509::Certificate.new(cert_str)
|
@@ -107,7 +174,7 @@ describe "Trocla::Format::X509" do
|
|
107
174
|
}))
|
108
175
|
cert = OpenSSL::X509::Certificate.new(cert_str)
|
109
176
|
expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
|
110
|
-
expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
|
177
|
+
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
111
178
|
expect(verify(@ca,cert)).to be true
|
112
179
|
|
113
180
|
expect(cert.extensions.find{|e| e.oid == 'basicConstraints' }.value).to eq('CA:TRUE')
|
@@ -123,7 +190,7 @@ describe "Trocla::Format::X509" do
|
|
123
190
|
}))
|
124
191
|
ca2 = OpenSSL::X509::Certificate.new(ca2_str)
|
125
192
|
expect(ca2.issuer.to_s).to eq(@ca.subject.to_s)
|
126
|
-
expect((Date.parse(ca2.not_after.to_s) - Date.today).to_i).to eq(365)
|
193
|
+
expect((Date.parse(ca2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
127
194
|
pending_for(:engine => 'jruby',:reason => 'NameConstraints verification seem to be broken in jRuby: https://github.com/jruby/jruby/issues/3502') do
|
128
195
|
expect(verify(@ca,ca2)).to be true
|
129
196
|
end
|
@@ -143,7 +210,7 @@ describe "Trocla::Format::X509" do
|
|
143
210
|
valid_cert = OpenSSL::X509::Certificate.new(valid_cert_str)
|
144
211
|
expect(valid_cert.issuer.to_s).to eq(ca2.subject.to_s)
|
145
212
|
expect(verify([@ca,ca2],valid_cert)).to be true
|
146
|
-
expect((Date.parse(valid_cert.not_after.to_s) - Date.today).to_i).to eq(365)
|
213
|
+
expect((Date.parse(valid_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
147
214
|
|
148
215
|
false_cert_str = @trocla.password('myfalseexamplecert','x509', {
|
149
216
|
'subject' => '/C=ZZ/O=Trocla Inc./CN=foo.example.net/emailAddress=example@example.com',
|
@@ -153,7 +220,7 @@ describe "Trocla::Format::X509" do
|
|
153
220
|
false_cert = OpenSSL::X509::Certificate.new(false_cert_str)
|
154
221
|
expect(false_cert.issuer.to_s).to eq(ca2.subject.to_s)
|
155
222
|
expect(verify([@ca,ca2],false_cert)).to be false
|
156
|
-
expect((Date.parse(false_cert.not_after.to_s) - Date.today).to_i).to eq(365)
|
223
|
+
expect((Date.parse(false_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
157
224
|
end
|
158
225
|
|
159
226
|
it 'supports simple name constraints for CAs with leading dots' do
|
@@ -163,7 +230,7 @@ describe "Trocla::Format::X509" do
|
|
163
230
|
}))
|
164
231
|
ca2 = OpenSSL::X509::Certificate.new(ca2_str)
|
165
232
|
expect(ca2.issuer.to_s).to eq(@ca.subject.to_s)
|
166
|
-
expect((Date.parse(ca2.not_after.to_s) - Date.today).to_i).to eq(365)
|
233
|
+
expect((Date.parse(ca2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
167
234
|
pending_for(:engine => 'jruby',:reason => 'NameConstraints verification seem to be broken in jRuby: https://github.com/jruby/jruby/issues/3502') do
|
168
235
|
expect(verify(@ca,ca2)).to be true
|
169
236
|
end
|
@@ -180,14 +247,14 @@ describe "Trocla::Format::X509" do
|
|
180
247
|
})
|
181
248
|
valid_cert = OpenSSL::X509::Certificate.new(valid_cert_str)
|
182
249
|
expect(valid_cert.issuer.to_s).to eq(ca2.subject.to_s)
|
183
|
-
expect((Date.parse(valid_cert.not_after.to_s) - Date.today).to_i).to eq(365)
|
250
|
+
expect((Date.parse(valid_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
184
251
|
# workaround broken openssl
|
185
|
-
if %x{openssl version}
|
186
|
-
expect(verify([@ca,ca2],valid_cert)).to be true
|
187
|
-
else
|
252
|
+
if Gem::Version.new(%x{openssl version}.split(' ')[1]) < Gem::Version.new('1.0.2')
|
188
253
|
skip_for(:engine => 'ruby',:reason => 'NameConstraints verification is broken on older openssl versions https://rt.openssl.org/Ticket/Display.html?id=3562') do
|
189
254
|
expect(verify([@ca,ca2],valid_cert)).to be true
|
190
255
|
end
|
256
|
+
else
|
257
|
+
expect(verify([@ca,ca2],valid_cert)).to be true
|
191
258
|
end
|
192
259
|
|
193
260
|
false_cert_str = @trocla.password('myfalseexamplecert','x509', {
|
@@ -196,7 +263,7 @@ describe "Trocla::Format::X509" do
|
|
196
263
|
})
|
197
264
|
false_cert = OpenSSL::X509::Certificate.new(false_cert_str)
|
198
265
|
expect(false_cert.issuer.to_s).to eq(ca2.subject.to_s)
|
199
|
-
expect((Date.parse(false_cert.not_after.to_s) - Date.today).to_i).to eq(365)
|
266
|
+
expect((Date.parse(false_cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
200
267
|
expect(verify([@ca,ca2],false_cert)).to be false
|
201
268
|
end
|
202
269
|
|
@@ -206,7 +273,7 @@ describe "Trocla::Format::X509" do
|
|
206
273
|
}))
|
207
274
|
ca2 = OpenSSL::X509::Certificate.new(ca2_str)
|
208
275
|
expect(ca2.issuer.to_s).to eq(@ca.subject.to_s)
|
209
|
-
expect((Date.parse(ca2.not_after.to_s) - Date.today).to_i).to eq(365)
|
276
|
+
expect((Date.parse(ca2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
210
277
|
expect(verify(@ca,ca2)).to be true
|
211
278
|
|
212
279
|
cert2_str = @trocla.password('mycert', 'x509', {
|
@@ -216,7 +283,7 @@ describe "Trocla::Format::X509" do
|
|
216
283
|
})
|
217
284
|
cert2 = OpenSSL::X509::Certificate.new(cert2_str)
|
218
285
|
expect(cert2.issuer.to_s).to eq(ca2.subject.to_s)
|
219
|
-
expect((Date.parse(cert2.not_after.to_s) - Date.today).to_i).to eq(365)
|
286
|
+
expect((Date.parse(cert2.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
220
287
|
skip_for(:engine => 'jruby',:reason => 'Chained CA validation seems to be broken on jruby atm.') do
|
221
288
|
expect(verify([@ca,ca2],cert2)).to be true
|
222
289
|
end
|
@@ -247,11 +314,13 @@ describe "Trocla::Format::X509" do
|
|
247
314
|
hash_match = (defined?(RUBY_ENGINE) &&RUBY_ENGINE == 'jruby') ? 'RSA-SHA1' : 'sha1WithRSAEncryption'
|
248
315
|
expect(cert.signature_algorithm).to eq(hash_match)
|
249
316
|
expect(cert.not_before).to be < Time.now
|
250
|
-
expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(3650)
|
317
|
+
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(3650)
|
251
318
|
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
|
252
319
|
expect(cert.public_key.n.num_bytes * 8).to eq(2048)
|
253
320
|
expect(verify(@ca,cert)).to be true
|
254
|
-
|
321
|
+
skip_for(:engine => 'jruby',:reason => 'subjectAltName represenation is broken in jruby-openssl -> https://github.com/jruby/jruby-openssl/pull/123') do
|
322
|
+
expect(cert.extensions.find{|e| e.oid == 'subjectAltName' }.value).to eq('DNS:www.test, DNS:test, DNS:test1, DNS:test2, DNS:test3')
|
323
|
+
end
|
255
324
|
|
256
325
|
expect(cert.extensions.find{|e| e.oid == 'basicConstraints' }.value).to eq('CA:FALSE')
|
257
326
|
ku = cert.extensions.find{|e| e.oid == 'keyUsage' }.value
|
@@ -267,7 +336,7 @@ describe "Trocla::Format::X509" do
|
|
267
336
|
cert_str = @trocla.password('mycert', 'x509', co)
|
268
337
|
cert = OpenSSL::X509::Certificate.new(cert_str)
|
269
338
|
expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
|
270
|
-
expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
|
339
|
+
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
271
340
|
expect(verify(@ca,cert)).to be true
|
272
341
|
expect(cert.extensions.find{|e| e.oid == 'subjectAltName' }).to be_nil
|
273
342
|
end
|
@@ -288,8 +357,26 @@ describe "Trocla::Format::X509" do
|
|
288
357
|
expect(cert.subject.to_s).not_to match(/#{field}=#{co[field]}/)
|
289
358
|
end
|
290
359
|
expect(cert.subject.to_s).not_to match(/(Email|emailAddress)=#{co['emailAddress']}/)
|
291
|
-
expect((Date.parse(cert.not_after.to_s) - Date.today).to_i).to eq(365)
|
360
|
+
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
292
361
|
expect(verify(@ca,cert)).to be true
|
293
362
|
end
|
363
|
+
it "is able to create a signed cert with custom keyUsage restrictions" do
|
364
|
+
cert_str = @trocla.password('mycert_without_restrictions', 'x509', cert_options.merge({
|
365
|
+
'CN' => 'sign only test',
|
366
|
+
'key_usages' => [ ],
|
367
|
+
}))
|
368
|
+
cert = OpenSSL::X509::Certificate.new(cert_str)
|
369
|
+
# default size
|
370
|
+
# https://stackoverflow.com/questions/13747212/determine-key-size-from-public-key-pem-format
|
371
|
+
expect(cert.public_key.n.num_bytes * 8).to eq(4096)
|
372
|
+
expect((Date.parse(cert.not_after.localtime.to_s) - Date.today).to_i).to eq(365)
|
373
|
+
expect(cert.issuer.to_s).to eq(@ca.subject.to_s)
|
374
|
+
expect(verify(@ca,cert)).to be true
|
375
|
+
|
376
|
+
v = cert.extensions.find{|e| e.oid == 'basicConstraints' }.value
|
377
|
+
expect(v).to_not eq('CA:TRUE')
|
378
|
+
expect(cert.extensions.find{|e| e.oid == 'keyUsage' }).to be_nil
|
379
|
+
end
|
380
|
+
|
294
381
|
end
|
295
382
|
end
|
data/spec/trocla/util_spec.rb
CHANGED
@@ -36,6 +36,14 @@ describe "Trocla::Util" do
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
+
describe :typesafe_generator do
|
40
|
+
10.times.each do |i|
|
41
|
+
it "creates random typesafe password #{i}" do
|
42
|
+
expect(Trocla::Util.random_str(12, 'typesafe')).to match(/^[1-9a-hj-km-xA-HJ-KM-X]{12}$/)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
39
47
|
describe :salt do
|
40
48
|
10.times.each do |i|
|
41
49
|
it "contains only characters and numbers #{i}" do
|
data/spec/trocla_spec.rb
CHANGED
@@ -1,147 +1,274 @@
|
|
1
|
+
# -- encoding : utf-8 --
|
1
2
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
3
|
|
3
4
|
describe "Trocla" do
|
4
5
|
|
5
6
|
before(:each) do
|
6
7
|
expect_any_instance_of(Trocla).to receive(:read_config).and_return(test_config)
|
7
|
-
@trocla = Trocla.new
|
8
8
|
end
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
context 'in normal usage with' do
|
10
|
+
before(:each) do
|
11
|
+
@trocla = Trocla.new
|
12
|
+
@trocla.password('init','plain')
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
describe "password" do
|
16
|
+
it "generates random passwords by default" do
|
17
|
+
expect(@trocla.password('random1','plain')).not_to eq(@trocla.password('random2','plain'))
|
18
|
+
end
|
19
|
+
|
20
|
+
it "generates passwords of length #{default_config['options']['length']}" do
|
21
|
+
expect(@trocla.password('random1','plain').length).to eq(default_config['options']['length'])
|
22
|
+
end
|
23
|
+
|
24
|
+
Trocla::Formats.all.each do |format|
|
25
|
+
describe "#{format} password format" do
|
26
|
+
it "retursn a password hashed in the #{format} format" do
|
27
|
+
expect(@trocla.password('some_test',format,format_options[format])).not_to be_empty
|
28
|
+
end
|
29
|
+
|
30
|
+
it "returns the same hashed for the #{format} format on multiple invocations" do
|
31
|
+
expect(round1=@trocla.password('some_test',format,format_options[format])).not_to be_empty
|
32
|
+
expect(@trocla.password('some_test',format,format_options[format])).to eq(round1)
|
33
|
+
end
|
18
34
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
35
|
+
it "also stores the plain password by default" do
|
36
|
+
pwd = @trocla.password('some_test','plain')
|
37
|
+
expect(pwd).not_to be_empty
|
38
|
+
expect(pwd.length).to eq(16)
|
39
|
+
end
|
23
40
|
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Trocla::Formats.all.reject{|f| f == 'plain' }.each do |format|
|
44
|
+
it "raises an exception if not a random password is asked but plain password is not present for format #{format}" do
|
45
|
+
expect{@trocla.password('not_random',format, 'random' => false)}.to raise_error(/Password must be present as plaintext/)
|
46
|
+
end
|
47
|
+
end
|
24
48
|
|
25
|
-
|
26
|
-
|
27
|
-
expect
|
49
|
+
describe 'with profiles' do
|
50
|
+
it 'raises an exception on unknown profile' do
|
51
|
+
expect{@trocla.password('no profile known','plain',
|
52
|
+
'profiles' => 'unknown_profile') }.to raise_error(/No such profile unknown_profile defined/)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'takes a profile and merge its options' do
|
56
|
+
pwd = @trocla.password('some_test','plain', 'profiles' => 'rootpw')
|
57
|
+
expect(pwd).not_to be_empty
|
58
|
+
expect(pwd.length).to eq(32)
|
59
|
+
expect(pwd).to_not match(/[={}\[\]\?%\*()&!]+/)
|
28
60
|
end
|
29
61
|
|
30
|
-
it
|
31
|
-
pwd = @trocla.password('
|
62
|
+
it 'is possible to combine profiles but first profile wins' do
|
63
|
+
pwd = @trocla.password('some_test1','plain', 'profiles' => ['rootpw','login'])
|
64
|
+
expect(pwd).not_to be_empty
|
65
|
+
expect(pwd.length).to eq(32)
|
66
|
+
expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
|
67
|
+
end
|
68
|
+
it 'is possible to combine profiles but first profile wins 2' do
|
69
|
+
pwd = @trocla.password('some_test2','plain', 'profiles' => ['login','mysql'])
|
32
70
|
expect(pwd).not_to be_empty
|
33
71
|
expect(pwd.length).to eq(16)
|
72
|
+
expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
|
73
|
+
end
|
74
|
+
it 'is possible to combine profiles but first profile wins 3' do
|
75
|
+
pwd = @trocla.password('some_test3','plain', 'profiles' => ['mysql','login'])
|
76
|
+
expect(pwd).not_to be_empty
|
77
|
+
expect(pwd.length).to eq(32)
|
78
|
+
expect(pwd).to match(/[+%\/@=\?_.,:]+/)
|
34
79
|
end
|
35
80
|
end
|
36
81
|
end
|
37
82
|
|
38
|
-
|
39
|
-
it "
|
40
|
-
expect
|
41
|
-
|
42
|
-
|
83
|
+
describe "set_password" do
|
84
|
+
it "resets hashed passwords on a new plain password" do
|
85
|
+
expect(@trocla.password('set_test','mysql')).not_to be_empty
|
86
|
+
expect(@trocla.get_password('set_test','mysql')).not_to be_nil
|
87
|
+
expect(old_plain=@trocla.password('set_test','mysql')).not_to be_empty
|
43
88
|
|
44
|
-
|
45
|
-
|
46
|
-
expect{@trocla.password('no profile known','plain',
|
47
|
-
'profiles' => 'unknown_profile') }.to raise_error(/No such profile unknown_profile defined/)
|
89
|
+
expect(@trocla.set_password('set_test','plain','foobar')).not_to eq(old_plain)
|
90
|
+
expect(@trocla.get_password('set_test','mysql')).to be_nil
|
48
91
|
end
|
49
92
|
|
50
|
-
it
|
51
|
-
|
52
|
-
expect(
|
53
|
-
expect(
|
54
|
-
expect(pwd).to_not match(/[={}\[\]\?%\*()&!]+/)
|
55
|
-
end
|
93
|
+
it "otherwise updates only the hash" do
|
94
|
+
expect(mysql = @trocla.password('set_test2','mysql')).not_to be_empty
|
95
|
+
expect(md5crypt = @trocla.password('set_test2','md5crypt')).not_to be_empty
|
96
|
+
expect(plain = @trocla.get_password('set_test2','plain')).not_to be_empty
|
56
97
|
|
57
|
-
|
58
|
-
|
59
|
-
expect(
|
60
|
-
expect(
|
61
|
-
expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
|
98
|
+
expect(new_mysql = @trocla.set_password('set_test2','mysql','foo')).not_to eql(mysql)
|
99
|
+
expect(@trocla.get_password('set_test2','mysql')).to eq(new_mysql)
|
100
|
+
expect(@trocla.get_password('set_test2','md5crypt')).to eq(md5crypt)
|
101
|
+
expect(@trocla.get_password('set_test2','plain')).to eq(plain)
|
62
102
|
end
|
63
|
-
|
64
|
-
|
65
|
-
expect(
|
66
|
-
expect(
|
67
|
-
expect(pwd).not_to match(/[={}\[\]\?%\*()&!]+/)
|
68
|
-
end
|
69
|
-
it 'is possible to combine profiles but first profile wins 3' do
|
70
|
-
pwd = @trocla.password('some_test','plain', 'profiles' => ['mysql','login'])
|
71
|
-
expect(pwd).not_to be_empty
|
72
|
-
expect(pwd.length).to eq(32)
|
73
|
-
expect(pwd).to match(/[+%\/@=\?_.,:]+/)
|
103
|
+
|
104
|
+
it 'is able to set password with umlauts and other UTF-8 charcters' do
|
105
|
+
expect(myumlaut = @trocla.set_password('set_test_umlaut','plain','Tütü')).to eql('Tütü')
|
106
|
+
expect(@trocla.get_password('set_test_umlaut','plain','Tütü')).to eql('Tütü')
|
74
107
|
end
|
75
108
|
end
|
76
|
-
end
|
77
109
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
expect(old_plain=@trocla.password('set_test','mysql')).not_to be_empty
|
110
|
+
describe "reset_password" do
|
111
|
+
it "resets a password" do
|
112
|
+
plain1 = @trocla.password('reset_pwd','plain')
|
113
|
+
plain2 = @trocla.reset_password('reset_pwd','plain')
|
83
114
|
|
84
|
-
|
85
|
-
|
86
|
-
end
|
115
|
+
expect(plain1).not_to eq(plain2)
|
116
|
+
end
|
87
117
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
expect(plain = @trocla.get_password('set_test2','plain')).not_to be_empty
|
118
|
+
it "does not reset other formats" do
|
119
|
+
expect(mysql = @trocla.password('reset_pwd2','mysql')).not_to be_empty
|
120
|
+
expect(md5crypt1 = @trocla.password('reset_pwd2','md5crypt')).not_to be_empty
|
92
121
|
|
93
|
-
|
94
|
-
|
95
|
-
expect(@trocla.get_password('set_test2','md5crypt')).to eq(md5crypt)
|
96
|
-
expect(@trocla.get_password('set_test2','plain')).to eq(plain)
|
97
|
-
end
|
98
|
-
end
|
122
|
+
expect(md5crypt2 = @trocla.reset_password('reset_pwd2','md5crypt')).not_to be_empty
|
123
|
+
expect(md5crypt2).not_to eq(md5crypt1)
|
99
124
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
plain2 = @trocla.reset_password('reset_pwd','plain')
|
125
|
+
expect(@trocla.get_password('reset_pwd2','mysql')).to eq(mysql)
|
126
|
+
end
|
127
|
+
end
|
104
128
|
|
105
|
-
|
129
|
+
describe "search_key" do
|
130
|
+
it "search a specific key" do
|
131
|
+
keys = ['search_key','search_key1','key_search','key_search2']
|
132
|
+
keys.each do |k|
|
133
|
+
@trocla.password(k,'plain')
|
134
|
+
end
|
135
|
+
expect(@trocla.search_key('search_key1').length).to eq(1)
|
136
|
+
end
|
137
|
+
it "ensure search regex is ok" do
|
138
|
+
keys = ['search_key2','search_key3','key_search2','key_search4']
|
139
|
+
keys.each do |k|
|
140
|
+
@trocla.password(k,'plain')
|
141
|
+
end
|
142
|
+
expect(@trocla.search_key('key').length).to eq(4)
|
143
|
+
expect(@trocla.search_key('^search').length).to eq(2)
|
144
|
+
expect(@trocla.search_key('ch.*3').length).to eq(1)
|
145
|
+
expect(@trocla.search_key('ch.*[3-4]$').length).to eq(2)
|
146
|
+
expect(@trocla.search_key('ch.*1')).to be_nil
|
147
|
+
end
|
106
148
|
end
|
107
149
|
|
108
|
-
|
109
|
-
|
110
|
-
|
150
|
+
describe "list_format" do
|
151
|
+
it "list available formats for key" do
|
152
|
+
formats = ['plain','mysql']
|
153
|
+
formats.each do |f|
|
154
|
+
@trocla.password('list_key',f)
|
155
|
+
end
|
156
|
+
expect(@trocla.available_format('list_key')).to eq(formats)
|
157
|
+
end
|
158
|
+
it "no return if key doesn't exist" do
|
159
|
+
expect(@trocla.available_format('list_key1')).to be_nil
|
160
|
+
end
|
161
|
+
end
|
111
162
|
|
112
|
-
|
113
|
-
|
163
|
+
describe "delete_password" do
|
164
|
+
it "deletes all passwords if no format is given" do
|
165
|
+
expect(@trocla.password('delete_test1','mysql')).not_to be_nil
|
166
|
+
expect(@trocla.get_password('delete_test1','plain')).not_to be_nil
|
114
167
|
|
115
|
-
|
116
|
-
|
117
|
-
|
168
|
+
@trocla.delete_password('delete_test1')
|
169
|
+
expect(@trocla.get_password('delete_test1','plain')).to be_nil
|
170
|
+
expect(@trocla.get_password('delete_test1','mysql')).to be_nil
|
171
|
+
end
|
118
172
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
expect(@trocla.get_password('delete_test1','plain')).not_to be_nil
|
173
|
+
it "deletes only a given format" do
|
174
|
+
expect(@trocla.password('delete_test2','mysql')).not_to be_nil
|
175
|
+
expect(@trocla.get_password('delete_test2','plain')).not_to be_nil
|
123
176
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
177
|
+
@trocla.delete_password('delete_test2','plain')
|
178
|
+
expect(@trocla.get_password('delete_test2','plain')).to be_nil
|
179
|
+
expect(@trocla.get_password('delete_test2','mysql')).not_to be_nil
|
180
|
+
end
|
128
181
|
|
129
|
-
|
130
|
-
|
131
|
-
|
182
|
+
it "deletes only a given non-plain format" do
|
183
|
+
expect(@trocla.password('delete_test3','mysql')).not_to be_nil
|
184
|
+
expect(@trocla.get_password('delete_test3','plain')).not_to be_nil
|
132
185
|
|
133
|
-
|
134
|
-
|
135
|
-
|
186
|
+
@trocla.delete_password('delete_test3','mysql')
|
187
|
+
expect(@trocla.get_password('delete_test3','mysql')).to be_nil
|
188
|
+
expect(@trocla.get_password('delete_test3','plain')).not_to be_nil
|
189
|
+
end
|
136
190
|
end
|
137
191
|
|
138
|
-
|
139
|
-
|
140
|
-
|
192
|
+
context 'concurrent access' do
|
193
|
+
context 'on expensive flagged formats' do
|
194
|
+
before(:each) do
|
195
|
+
expect(Trocla::Formats).to receive(:[]).with('sleep').at_least(:once).and_return(Trocla::Formats::Sleep)
|
196
|
+
expect(Trocla::Formats::Sleep).to receive(:expensive?).at_least(:once).and_return(true)
|
197
|
+
expect(Trocla::Formats).to receive(:available?).with('sleep').at_least(:once).and_return(true)
|
198
|
+
end
|
199
|
+
it 'should not overwrite a value if it takes longer' do
|
200
|
+
t1 = Thread.new{ @trocla.password('threadpwd','sleep','sleep' => 4) }
|
201
|
+
t2 = Thread.new{ @trocla.password('threadpwd','sleep','sleep' => 1) }
|
202
|
+
pwd1 = t1.value
|
203
|
+
pwd2 = t2.value
|
204
|
+
real_value = @trocla.password('threadpwd','sleep')
|
205
|
+
# as t2 finished first this should win
|
206
|
+
expect(pwd1).to eql(pwd2)
|
207
|
+
expect(real_value).to eql(pwd1)
|
208
|
+
expect(real_value).to eql(pwd2)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
context 'on inexpensive flagged formats' do
|
212
|
+
before(:each) do
|
213
|
+
expect(Trocla::Formats).to receive(:[]).with('sleep').at_least(:once).and_return(Trocla::Formats::Sleep)
|
214
|
+
expect(Trocla::Formats::Sleep).to receive(:expensive?).at_least(:once).and_return(false)
|
215
|
+
expect(Trocla::Formats).to receive(:available?).with('sleep').at_least(:once).and_return(true)
|
216
|
+
end
|
217
|
+
it 'should not overwrite a value if it takes longer' do
|
218
|
+
t1 = Thread.new{ @trocla.password('threadpwd_inexp','sleep','sleep' => 4) }
|
219
|
+
t2 = Thread.new{ @trocla.password('threadpwd_inexp','sleep','sleep' => 1) }
|
220
|
+
pwd1 = t1.value
|
221
|
+
pwd2 = t2.value
|
222
|
+
real_value = @trocla.password('threadpwd_inexp','sleep')
|
223
|
+
# as t2 finished first but the format is inexpensive it gets overwritten
|
224
|
+
expect(pwd1).not_to eql(pwd2)
|
225
|
+
expect(real_value).to eql(pwd1)
|
226
|
+
expect(real_value).not_to eql(pwd2)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
context 'real world example' do
|
230
|
+
it 'should store the quicker one' do
|
231
|
+
t1 = Thread.new{ @trocla.password('threadpwd_real','bcrypt','cost' => 17) }
|
232
|
+
t2 = Thread.new{ @trocla.password('threadpwd_real','bcrypt') }
|
233
|
+
pwd1 = t1.value
|
234
|
+
pwd2 = t2.value
|
235
|
+
real_value = @trocla.password('threadpwd_real','bcrypt')
|
236
|
+
# t2 should still win but both should be the same
|
237
|
+
expect(pwd1).to eql(pwd2)
|
238
|
+
expect(real_value).to eql(pwd1)
|
239
|
+
expect(real_value).to eql(pwd2)
|
240
|
+
end
|
241
|
+
it 'should store the quicker one test 2' do
|
242
|
+
t1 = Thread.new{ @trocla.password('my_shiny_selfsigned_ca', 'x509', {
|
243
|
+
'CN' => 'This is my self-signed certificate',
|
244
|
+
'become_ca' => false,
|
245
|
+
}) }
|
246
|
+
t2 = Thread.new{ @trocla.password('my_shiny_selfsigned_ca', 'x509', {
|
247
|
+
'CN' => 'This is my self-signed certificate',
|
248
|
+
'become_ca' => false,
|
249
|
+
}) }
|
250
|
+
cert1 = t1.value
|
251
|
+
cert2 = t2.value
|
252
|
+
real_value = @trocla.password('my_shiny_selfsigned_ca','x509')
|
253
|
+
# t2 should still win but both should be the same
|
254
|
+
expect(cert1).to eql(cert2)
|
255
|
+
expect(real_value).to eql(cert1)
|
256
|
+
expect(real_value).to eql(cert2)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
141
260
|
|
142
|
-
|
143
|
-
|
144
|
-
|
261
|
+
end
|
262
|
+
context 'with .open' do
|
263
|
+
it 'closes the connection with a block' do
|
264
|
+
expect_any_instance_of(Trocla::Stores::Memory).to receive(:close)
|
265
|
+
Trocla.open{|t|
|
266
|
+
t.password('plain_open','plain')
|
267
|
+
}
|
268
|
+
end
|
269
|
+
it 'keeps the connection without a block' do
|
270
|
+
expect_any_instance_of(Trocla::Stores::Memory).not_to receive(:close)
|
271
|
+
Trocla.open.password('plain_open','plain')
|
145
272
|
end
|
146
273
|
end
|
147
274
|
|