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/VERSION
CHANGED
data/lib/trocla.rb
CHANGED
@@ -13,6 +13,17 @@ class Trocla
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
+
def self.open(config_file=nil)
|
17
|
+
trocla = Trocla.new(config_file)
|
18
|
+
|
19
|
+
if block_given?
|
20
|
+
yield trocla
|
21
|
+
trocla.close
|
22
|
+
else
|
23
|
+
trocla
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
16
27
|
def password(key,format,options={})
|
17
28
|
# respect a default profile, but let the
|
18
29
|
# profiles win over the default options
|
@@ -24,43 +35,59 @@ class Trocla
|
|
24
35
|
|
25
36
|
raise "Format #{format} is not supported! Supported formats: #{Trocla::Formats.all.join(', ')}" unless Trocla::Formats::available?(format)
|
26
37
|
|
27
|
-
unless (password=get_password(key,format)).nil?
|
38
|
+
unless (password=get_password(key,format,options)).nil?
|
28
39
|
return password
|
29
40
|
end
|
30
41
|
|
31
|
-
plain_pwd = get_password(key,'plain')
|
42
|
+
plain_pwd = get_password(key,'plain',options)
|
32
43
|
if options['random'] && plain_pwd.nil?
|
33
44
|
plain_pwd = Trocla::Util.random_str(options['length'].to_i,options['charset'])
|
34
45
|
set_password(key,'plain',plain_pwd,options) unless format == 'plain'
|
35
46
|
elsif !options['random'] && plain_pwd.nil?
|
36
47
|
raise "Password must be present as plaintext if you don't want a random password"
|
37
48
|
end
|
38
|
-
|
49
|
+
pwd = self.formats(format).format(plain_pwd,options)
|
50
|
+
# it's possible that meanwhile another thread/process was faster in
|
51
|
+
# formating the password. But we want todo that second lookup
|
52
|
+
# only for expensive formats
|
53
|
+
if self.formats(format).expensive?
|
54
|
+
get_password(key,format,options) || set_password(key, format, pwd, options)
|
55
|
+
else
|
56
|
+
set_password(key, format, pwd, options)
|
57
|
+
end
|
39
58
|
end
|
40
59
|
|
41
|
-
def get_password(key, format)
|
42
|
-
decrypt(store.get(key,format))
|
60
|
+
def get_password(key, format, options={})
|
61
|
+
render(format,decrypt(store.get(key,format)),options)
|
43
62
|
end
|
44
63
|
|
45
64
|
def reset_password(key,format,options={})
|
46
|
-
|
65
|
+
delete_password(key,format)
|
47
66
|
password(key,format,options)
|
48
67
|
end
|
49
68
|
|
50
|
-
def delete_password(key,format=nil)
|
69
|
+
def delete_password(key,format=nil,options={})
|
51
70
|
v = store.delete(key,format)
|
52
71
|
if v.is_a?(Hash)
|
53
72
|
Hash[*v.map do |f,encrypted_value|
|
54
|
-
[f,decrypt(encrypted_value)]
|
73
|
+
[f,render(format,decrypt(encrypted_value),options)]
|
55
74
|
end.flatten]
|
56
75
|
else
|
57
|
-
decrypt(v)
|
76
|
+
render(format,decrypt(v),options)
|
58
77
|
end
|
59
78
|
end
|
60
79
|
|
61
80
|
def set_password(key,format,password,options={})
|
62
81
|
store.set(key,format,encrypt(password),options)
|
63
|
-
password
|
82
|
+
render(format,password,options)
|
83
|
+
end
|
84
|
+
|
85
|
+
def available_format(key, options={})
|
86
|
+
render(false,store.formats(key),options)
|
87
|
+
end
|
88
|
+
|
89
|
+
def search_key(key, options={})
|
90
|
+
render(false,store.search(key),options)
|
64
91
|
end
|
65
92
|
|
66
93
|
def formats(format)
|
@@ -75,6 +102,10 @@ class Trocla
|
|
75
102
|
@config ||= read_config
|
76
103
|
end
|
77
104
|
|
105
|
+
def close
|
106
|
+
store.close
|
107
|
+
end
|
108
|
+
|
78
109
|
private
|
79
110
|
def store
|
80
111
|
@store ||= build_store
|
@@ -116,6 +147,14 @@ class Trocla
|
|
116
147
|
encryption.decrypt(value)
|
117
148
|
end
|
118
149
|
|
150
|
+
def render(format,output,options={})
|
151
|
+
if format && output && f=self.formats(format)
|
152
|
+
f.render(output,options['render']||{})
|
153
|
+
else
|
154
|
+
output
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
119
158
|
def default_config
|
120
159
|
require 'yaml'
|
121
160
|
YAML.load(File.read(File.expand_path(File.join(File.dirname(__FILE__),'trocla','default_config.yaml'))))
|
@@ -21,6 +21,16 @@ profiles:
|
|
21
21
|
login:
|
22
22
|
charset: consolesafe
|
23
23
|
length: 16
|
24
|
+
x509veryverylong:
|
25
|
+
# 15 years
|
26
|
+
days: 5475
|
27
|
+
# 5475 days
|
28
|
+
expires: 466560000
|
29
|
+
x509verylong:
|
30
|
+
# 10 years
|
31
|
+
days: 3650
|
32
|
+
# 3600 days
|
33
|
+
expires: 311040000
|
24
34
|
x509long:
|
25
35
|
# 5 years
|
26
36
|
days: 1825
|
data/lib/trocla/formats.rb
CHANGED
@@ -5,6 +5,20 @@ class Trocla::Formats
|
|
5
5
|
def initialize(trocla)
|
6
6
|
@trocla = trocla
|
7
7
|
end
|
8
|
+
def render(output,render_options={})
|
9
|
+
output
|
10
|
+
end
|
11
|
+
def expensive?
|
12
|
+
self.class.expensive?
|
13
|
+
end
|
14
|
+
class << self
|
15
|
+
def expensive(is_expensive)
|
16
|
+
@expensive = is_expensive
|
17
|
+
end
|
18
|
+
def expensive?
|
19
|
+
@expensive == true
|
20
|
+
end
|
21
|
+
end
|
8
22
|
end
|
9
23
|
|
10
24
|
class << self
|
@@ -1,6 +1,7 @@
|
|
1
1
|
class Trocla::Formats::Bcrypt < Trocla::Formats::Base
|
2
|
+
expensive true
|
2
3
|
require 'bcrypt'
|
3
4
|
def format(plain_password,options={})
|
4
|
-
BCrypt::Password.create(plain_password).to_s
|
5
|
+
BCrypt::Password.create(plain_password, :cost => options['cost']||BCrypt::Engine.cost).to_s
|
5
6
|
end
|
6
7
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'sshkey'
|
2
|
+
|
3
|
+
class Trocla::Formats::Sshkey < Trocla::Formats::Base
|
4
|
+
|
5
|
+
expensive true
|
6
|
+
|
7
|
+
def format(plain_password,options={})
|
8
|
+
|
9
|
+
if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY/m)
|
10
|
+
# Import, validate ssh key
|
11
|
+
begin
|
12
|
+
sshkey = ::SSHKey.new(plain_password)
|
13
|
+
rescue Exception => e
|
14
|
+
raise "SSH key import failed: #{e.message}"
|
15
|
+
end
|
16
|
+
return sshkey.private_key + sshkey.ssh_public_key
|
17
|
+
end
|
18
|
+
|
19
|
+
type = options['type'] || 'rsa'
|
20
|
+
bits = options['bits'] || 2048
|
21
|
+
|
22
|
+
begin
|
23
|
+
sshkey = ::SSHKey.generate(
|
24
|
+
type: type,
|
25
|
+
bits: bits,
|
26
|
+
comment: options['comment'],
|
27
|
+
passphrase: options['passphrase']
|
28
|
+
)
|
29
|
+
rescue Exception => e
|
30
|
+
raise "SSH key creation failed: #{e.message}"
|
31
|
+
end
|
32
|
+
|
33
|
+
sshkey.private_key + sshkey.ssh_public_key
|
34
|
+
end
|
35
|
+
|
36
|
+
def render(output,render_options={})
|
37
|
+
if render_options['privonly']
|
38
|
+
::SSHKey.new(output).private_key
|
39
|
+
elsif render_options['pubonly']
|
40
|
+
::SSHKey.new(output).ssh_public_key
|
41
|
+
else
|
42
|
+
super(output,render_options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/lib/trocla/formats/x509.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'openssl'
|
2
2
|
class Trocla::Formats::X509 < Trocla::Formats::Base
|
3
|
+
expensive true
|
3
4
|
def format(plain_password,options={})
|
4
5
|
|
5
6
|
if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/m)
|
@@ -28,6 +29,8 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
28
29
|
keysize = options['keysize'] || 4096
|
29
30
|
days = options['days'].nil? ? 365 : options['days'].to_i
|
30
31
|
name_constraints = Array(options['name_constraints'])
|
32
|
+
key_usages = options['key_usages']
|
33
|
+
key_usages = Array(key_usages) if key_usages
|
31
34
|
|
32
35
|
altnames = if become_ca || (an = options['altnames']) && Array(an).empty?
|
33
36
|
[]
|
@@ -49,6 +52,7 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
49
52
|
raise "Private key for #{subject} creation failed: #{e.message}"
|
50
53
|
end
|
51
54
|
|
55
|
+
cert = nil
|
52
56
|
if sign_with # certificate signed with CA
|
53
57
|
begin
|
54
58
|
ca_str = trocla.get_password(sign_with,'x509')
|
@@ -68,28 +72,39 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
68
72
|
end
|
69
73
|
|
70
74
|
begin
|
71
|
-
|
72
|
-
|
75
|
+
cert = mkcert(caserial, request.subject, ca, request.public_key, days,
|
76
|
+
altnames, key_usages, name_constraints, become_ca)
|
77
|
+
cert.sign(cakey, signature(hash))
|
73
78
|
addserial(sign_with, caserial)
|
74
79
|
rescue Exception => e
|
75
80
|
raise "Certificate #{subject} signing failed: #{e.message}"
|
76
81
|
end
|
77
|
-
|
78
|
-
key.to_pem + csr_cert.to_pem
|
79
82
|
else # self-signed certificate
|
80
83
|
begin
|
81
84
|
subj = OpenSSL::X509::Name.parse(subject)
|
82
|
-
cert = mkcert(getserial(subj), subj, nil, key.public_key, days,
|
85
|
+
cert = mkcert(getserial(subj), subj, nil, key.public_key, days,
|
86
|
+
altnames, key_usages, name_constraints, become_ca)
|
83
87
|
cert.sign(key, signature(hash))
|
84
88
|
rescue Exception => e
|
85
89
|
raise "Self-signed certificate #{subject} creation failed: #{e.message}"
|
86
90
|
end
|
91
|
+
end
|
92
|
+
key.to_pem + cert.to_pem
|
93
|
+
end
|
87
94
|
|
88
|
-
|
95
|
+
def render(output,render_options={})
|
96
|
+
if render_options['keyonly']
|
97
|
+
OpenSSL::PKey::RSA.new(output).to_pem
|
98
|
+
elsif render_options['certonly']
|
99
|
+
OpenSSL::X509::Certificate.new(output).to_pem
|
100
|
+
elsif render_options['publickeyonly']
|
101
|
+
OpenSSL::PKey::RSA.new(output).public_key.to_pem
|
102
|
+
else
|
103
|
+
super(output,render_options)
|
89
104
|
end
|
90
105
|
end
|
91
|
-
private
|
92
106
|
|
107
|
+
private
|
93
108
|
# nice help: https://gist.github.com/mitfik/1922961
|
94
109
|
|
95
110
|
def signature(hash = 'sha2')
|
@@ -120,7 +135,7 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
120
135
|
request
|
121
136
|
end
|
122
137
|
|
123
|
-
def mkcert(serial,subject,issuer,public_key,days,altnames, name_constraints = [], become_ca = false)
|
138
|
+
def mkcert(serial,subject,issuer,public_key,days,altnames, key_usages = nil, name_constraints = [], become_ca = false)
|
124
139
|
cert = OpenSSL::X509::Certificate.new
|
125
140
|
issuer = cert if issuer == nil
|
126
141
|
cert.subject = subject
|
@@ -138,14 +153,18 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
138
153
|
|
139
154
|
if become_ca
|
140
155
|
cert.add_extension ef.create_extension("basicConstraints","CA:TRUE", true)
|
141
|
-
|
156
|
+
unless (ku = key_usages || ca_key_usages).empty?
|
157
|
+
cert.add_extension ef.create_extension("keyUsage", ku.join(', '), true)
|
158
|
+
end
|
142
159
|
if name_constraints && !name_constraints.empty?
|
143
160
|
cert.add_extension ef.create_extension("nameConstraints","permitted;DNS:#{name_constraints.join(',permitted;DNS:')}",true)
|
144
161
|
end
|
145
162
|
else
|
146
163
|
cert.add_extension ef.create_extension("subjectAltName", altnames, true) unless altnames.empty?
|
147
164
|
cert.add_extension ef.create_extension("basicConstraints","CA:FALSE", true)
|
148
|
-
|
165
|
+
unless (ku = key_usages || cert_key_usages).empty?
|
166
|
+
cert.add_extension ef.create_extension("keyUsage", ku.join(', '), true)
|
167
|
+
end
|
149
168
|
end
|
150
169
|
cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
|
151
170
|
|
@@ -169,4 +188,12 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
169
188
|
serials = all_serials(ca) << serial
|
170
189
|
trocla.set_password("#{ca}_all_serials",'plain',YAML.dump(serials))
|
171
190
|
end
|
191
|
+
|
192
|
+
def cert_key_usages
|
193
|
+
['nonRepudiation', 'digitalSignature', 'keyEncipherment']
|
194
|
+
end
|
195
|
+
def ca_key_usages
|
196
|
+
['keyCertSign', 'cRLSign', 'nonRepudiation',
|
197
|
+
'digitalSignature', 'keyEncipherment' ]
|
198
|
+
end
|
172
199
|
end
|
data/lib/trocla/store.rb
CHANGED
@@ -6,6 +6,12 @@ class Trocla::Store
|
|
6
6
|
@trocla = trocla
|
7
7
|
end
|
8
8
|
|
9
|
+
# closes the store
|
10
|
+
# when called do whatever "closes" your
|
11
|
+
# store, e.g. close database connections.
|
12
|
+
def close
|
13
|
+
end
|
14
|
+
|
9
15
|
# should return value for key & format
|
10
16
|
# returns nil if nothing or a nil value
|
11
17
|
# was found.
|
@@ -44,6 +50,16 @@ class Trocla::Store
|
|
44
50
|
format.nil? ? (delete_all(key)||{}) : delete_format(key,format)
|
45
51
|
end
|
46
52
|
|
53
|
+
# returns all formats for a key
|
54
|
+
def formats(key)
|
55
|
+
raise 'not implemented'
|
56
|
+
end
|
57
|
+
|
58
|
+
# def searches for a key
|
59
|
+
def search(key)
|
60
|
+
raise 'not implemented'
|
61
|
+
end
|
62
|
+
|
47
63
|
private
|
48
64
|
# sets a new plain value
|
49
65
|
# *must* invalidate all
|
data/lib/trocla/stores/memory.rb
CHANGED
@@ -19,6 +19,15 @@ class Trocla::Stores::Memory < Trocla::Store
|
|
19
19
|
set_expires(key,options['expires'])
|
20
20
|
end
|
21
21
|
|
22
|
+
def formats(key)
|
23
|
+
memory[key].empty? ? nil : memory[key].keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def search(key)
|
27
|
+
r = memory.keys.grep(/#{key}/)
|
28
|
+
r.empty? ? nil : r
|
29
|
+
end
|
30
|
+
|
22
31
|
private
|
23
32
|
def set_plain(key,value,options)
|
24
33
|
memory[key] = { 'plain' => value }
|
data/lib/trocla/stores/moneta.rb
CHANGED
@@ -10,10 +10,25 @@ class Trocla::Stores::Moneta < Trocla::Store
|
|
10
10
|
@moneta = Moneta.new(store_config['adapter'],adapter_options)
|
11
11
|
end
|
12
12
|
|
13
|
+
def close
|
14
|
+
moneta.close
|
15
|
+
end
|
16
|
+
|
13
17
|
def get(key,format)
|
14
18
|
moneta.fetch(key, {})[format]
|
15
19
|
end
|
16
20
|
|
21
|
+
def formats(key)
|
22
|
+
r = moneta.fetch(key)
|
23
|
+
r.nil? ? nil : r.keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def search(key)
|
27
|
+
raise 'The search option is not available for any adapter other than Sequel or YAML' unless store_config['adapter'] == :Sequel || store_config['adapter'] == :YAML
|
28
|
+
r = search_keys(key)
|
29
|
+
r.empty? ? nil : r
|
30
|
+
end
|
31
|
+
|
17
32
|
private
|
18
33
|
def set_plain(key,value,options)
|
19
34
|
h = { 'plain' => value }
|
@@ -51,4 +66,19 @@ class Trocla::Stores::Moneta < Trocla::Store
|
|
51
66
|
end
|
52
67
|
res
|
53
68
|
end
|
69
|
+
def search_keys(key)
|
70
|
+
_moneta = Moneta.new(store_config['adapter'], (store_config['adapter_options']||{}).merge({ :expires => false }))
|
71
|
+
a = []
|
72
|
+
if store_config['adapter'] == :Sequel
|
73
|
+
keys = _moneta.adapter.backend[:trocla].select_order_map {from_base64(:k)}
|
74
|
+
elsif store_config['adapter'] == :YAML
|
75
|
+
keys = _moneta.adapter.backend.transaction(true) { _moneta.adapter.backend.roots }
|
76
|
+
end
|
77
|
+
_moneta.close
|
78
|
+
regexp = Regexp.new("#{key}")
|
79
|
+
keys.each do |k|
|
80
|
+
a << k if regexp.match(k)
|
81
|
+
end
|
82
|
+
a
|
83
|
+
end
|
54
84
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# the default vault based store
|
2
|
+
class Trocla::Stores::Vault < Trocla::Store
|
3
|
+
attr_reader :vault, :mount
|
4
|
+
def initialize(config,trocla)
|
5
|
+
super(config,trocla)
|
6
|
+
require 'vault'
|
7
|
+
@mount = (config.delete(:mount) || 'kv')
|
8
|
+
# load expire support by default
|
9
|
+
@vault = Vault::Client.new(config)
|
10
|
+
end
|
11
|
+
|
12
|
+
def close
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(key,format)
|
16
|
+
read(key)[format.to_sym]
|
17
|
+
end
|
18
|
+
|
19
|
+
def formats(key)
|
20
|
+
read(key).keys
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def read(key)
|
25
|
+
k = vault.kv(mount).read(key)
|
26
|
+
k.nil? ? {} : k.data
|
27
|
+
end
|
28
|
+
|
29
|
+
def write(key, value)
|
30
|
+
vault.kv(mount).write(key, value)
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_plain(key,value,options)
|
34
|
+
set_format(key,'plain',value,options)
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_format(key,format,value,options)
|
38
|
+
write(key, read(key).merge({format.to_sym => value}))
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete_all(key)
|
42
|
+
vault.kv(mount).delete(key)
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete_format(key,format)
|
46
|
+
old = read(key)
|
47
|
+
write(key, old.reject { |k,v| k == format.to_sym })
|
48
|
+
old[format.to_sym]
|
49
|
+
end
|
50
|
+
end
|