trocla 0.1.2 → 0.2.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 +7 -7
- data/.travis.yml +4 -0
- data/Gemfile +4 -1
- data/README.md +164 -17
- data/bin/trocla +31 -16
- data/lib/VERSION +2 -2
- data/lib/trocla.rb +48 -28
- data/lib/trocla/default_config.yaml +33 -4
- data/lib/trocla/encryptions.rb +3 -2
- data/lib/trocla/encryptions/ssl.rb +8 -10
- data/lib/trocla/formats/x509.rb +57 -30
- data/lib/trocla/store.rb +74 -0
- data/lib/trocla/stores.rb +39 -0
- data/lib/trocla/stores/memory.rb +56 -0
- data/lib/trocla/stores/moneta.rb +54 -0
- data/lib/trocla/util.rb +22 -8
- data/spec/spec_helper.rb +235 -20
- data/spec/trocla/encryptions/none_spec.rb +22 -0
- data/spec/trocla/encryptions/ssl_spec.rb +2 -32
- data/spec/trocla/formats/x509_spec.rb +295 -0
- data/spec/trocla/store/memory_spec.rb +6 -0
- data/spec/trocla/store/moneta_spec.rb +6 -0
- data/spec/trocla/util_spec.rb +24 -12
- data/spec/trocla_spec.rb +109 -76
- data/trocla.gemspec +31 -22
- metadata +120 -84
data/lib/trocla.rb
CHANGED
@@ -2,9 +2,9 @@ require 'trocla/version'
|
|
2
2
|
require 'trocla/util'
|
3
3
|
require 'trocla/formats'
|
4
4
|
require 'trocla/encryptions'
|
5
|
+
require 'trocla/stores'
|
5
6
|
|
6
7
|
class Trocla
|
7
|
-
|
8
8
|
def initialize(config_file=nil)
|
9
9
|
if config_file
|
10
10
|
@config_file = File.expand_path(config_file)
|
@@ -14,7 +14,14 @@ class Trocla
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def password(key,format,options={})
|
17
|
+
# respect a default profile, but let the
|
18
|
+
# profiles win over the default options
|
19
|
+
options['profiles'] ||= config['options']['profiles']
|
20
|
+
if options['profiles']
|
21
|
+
options = merge_profiles(options['profiles']).merge(options)
|
22
|
+
end
|
17
23
|
options = config['options'].merge(options)
|
24
|
+
|
18
25
|
raise "Format #{format} is not supported! Supported formats: #{Trocla::Formats.all.join(', ')}" unless Trocla::Formats::available?(format)
|
19
26
|
|
20
27
|
unless (password=get_password(key,format)).nil?
|
@@ -24,39 +31,36 @@ class Trocla
|
|
24
31
|
plain_pwd = get_password(key,'plain')
|
25
32
|
if options['random'] && plain_pwd.nil?
|
26
33
|
plain_pwd = Trocla::Util.random_str(options['length'].to_i,options['charset'])
|
27
|
-
set_password(key,'plain',plain_pwd) unless format == 'plain'
|
34
|
+
set_password(key,'plain',plain_pwd,options) unless format == 'plain'
|
28
35
|
elsif !options['random'] && plain_pwd.nil?
|
29
36
|
raise "Password must be present as plaintext if you don't want a random password"
|
30
37
|
end
|
31
|
-
set_password(key,format,self.formats(format).format(plain_pwd,options))
|
38
|
+
set_password(key,format,self.formats(format).format(plain_pwd,options),options)
|
32
39
|
end
|
33
40
|
|
34
41
|
def get_password(key, format)
|
35
|
-
decrypt(
|
42
|
+
decrypt(store.get(key,format))
|
36
43
|
end
|
37
44
|
|
38
45
|
def reset_password(key,format,options={})
|
39
|
-
set_password(key,format,nil)
|
46
|
+
set_password(key,format,nil,options)
|
40
47
|
password(key,format,options)
|
41
48
|
end
|
42
49
|
|
43
50
|
def delete_password(key,format=nil)
|
44
|
-
|
45
|
-
|
51
|
+
v = store.delete(key,format)
|
52
|
+
if v.is_a?(Hash)
|
53
|
+
Hash[*v.map do |f,encrypted_value|
|
54
|
+
[f,decrypt(encrypted_value)]
|
55
|
+
end.flatten]
|
46
56
|
else
|
47
|
-
|
48
|
-
h.empty? ? cache.delete(key) : cache[key] = h
|
49
|
-
decrypt(old_val)
|
57
|
+
decrypt(v)
|
50
58
|
end
|
51
59
|
end
|
52
60
|
|
53
|
-
def set_password(key,format,password)
|
54
|
-
|
55
|
-
|
56
|
-
else
|
57
|
-
h = (cache[key] = cache.fetch(key,{}).merge({ format => encrypt(password) }))
|
58
|
-
end
|
59
|
-
decrypt h[format]
|
61
|
+
def set_password(key,format,password,options={})
|
62
|
+
store.set(key,format,encrypt(password),options)
|
63
|
+
password
|
60
64
|
end
|
61
65
|
|
62
66
|
def formats(format)
|
@@ -64,10 +68,7 @@ class Trocla
|
|
64
68
|
end
|
65
69
|
|
66
70
|
def encryption
|
67
|
-
|
68
|
-
enc ||= :none
|
69
|
-
@encryption ||= Trocla::Encryptions[enc].new(self)
|
70
|
-
@encryption
|
71
|
+
@encryption ||= Trocla::Encryptions[config['encryption']].new(config['encryption_options'],self)
|
71
72
|
end
|
72
73
|
|
73
74
|
def config
|
@@ -75,14 +76,19 @@ class Trocla
|
|
75
76
|
end
|
76
77
|
|
77
78
|
private
|
78
|
-
def
|
79
|
-
@
|
79
|
+
def store
|
80
|
+
@store ||= build_store
|
80
81
|
end
|
81
82
|
|
82
|
-
def
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
def build_store
|
84
|
+
s = config['store']
|
85
|
+
clazz = if s.is_a?(Symbol)
|
86
|
+
Trocla::Stores[s]
|
87
|
+
else
|
88
|
+
require config['store_require'] if config['store_require']
|
89
|
+
eval(s)
|
90
|
+
end
|
91
|
+
clazz.new(config['store_options'],self)
|
86
92
|
end
|
87
93
|
|
88
94
|
def read_config
|
@@ -90,7 +96,14 @@ class Trocla
|
|
90
96
|
default_config
|
91
97
|
else
|
92
98
|
raise "Configfile #{@config_file} does not exist!" unless File.exists?(@config_file)
|
93
|
-
default_config.merge(YAML.load(File.read(@config_file)))
|
99
|
+
c = default_config.merge(YAML.load(File.read(@config_file)))
|
100
|
+
c['profiles'] = default_config['profiles'].merge(c['profiles'])
|
101
|
+
# migrate all options to new store options
|
102
|
+
# TODO: remove workaround in 0.3.0
|
103
|
+
c['store_options']['adapter'] = c['adapter'] if c['adapter']
|
104
|
+
c['store_options']['adapter_options'] = c['adapter_options'] if c['adapter_options']
|
105
|
+
c['encryption_options'] = c['ssl_options'] if c['ssl_options']
|
106
|
+
c
|
94
107
|
end
|
95
108
|
end
|
96
109
|
|
@@ -108,4 +121,11 @@ class Trocla
|
|
108
121
|
YAML.load(File.read(File.expand_path(File.join(File.dirname(__FILE__),'trocla','default_config.yaml'))))
|
109
122
|
end
|
110
123
|
|
124
|
+
def merge_profiles(profiles)
|
125
|
+
Array(profiles).inject({}) do |res,profile|
|
126
|
+
raise "No such profile #{profile} defined" unless profile_hash = config['profiles'][profile]
|
127
|
+
profile_hash.merge(res)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
111
131
|
end
|
@@ -1,8 +1,37 @@
|
|
1
1
|
---
|
2
|
+
store: :moneta
|
3
|
+
store_options:
|
4
|
+
adapter: :YAML
|
5
|
+
adapter_options:
|
6
|
+
:file: '/tmp/trocla.yaml'
|
7
|
+
|
8
|
+
encryption: :none
|
2
9
|
options:
|
3
10
|
random: true
|
4
|
-
length:
|
11
|
+
length: 16
|
5
12
|
charset: default
|
6
|
-
|
7
|
-
|
8
|
-
|
13
|
+
|
14
|
+
profiles:
|
15
|
+
rootpw:
|
16
|
+
charset: consolesafe
|
17
|
+
length: 32
|
18
|
+
mysql:
|
19
|
+
charset: shellsafe
|
20
|
+
length: 32
|
21
|
+
login:
|
22
|
+
charset: consolesafe
|
23
|
+
length: 16
|
24
|
+
x509long:
|
25
|
+
# 5 years
|
26
|
+
days: 1825
|
27
|
+
# 1800 days
|
28
|
+
expires: 155520000
|
29
|
+
x509auto:
|
30
|
+
days: 40
|
31
|
+
# 30 days
|
32
|
+
expires: 2592000
|
33
|
+
x509short:
|
34
|
+
days: 2
|
35
|
+
# 1 day
|
36
|
+
expires: 86400
|
37
|
+
|
data/lib/trocla/encryptions.rb
CHANGED
@@ -25,19 +25,17 @@ class Trocla::Encryptions::Ssl < Trocla::Encryptions::Base
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def private_key
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
@private_key ||= begin
|
29
|
+
file = require_option(:private_key)
|
30
|
+
OpenSSL::PKey::RSA.new(File.read(file), nil)
|
31
|
+
end
|
31
32
|
end
|
32
33
|
|
33
34
|
def public_key
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
def config
|
39
|
-
@config = @trocla.config['ssl_options']
|
40
|
-
@config ||= Hash.new
|
35
|
+
@public_key ||= begin
|
36
|
+
file = require_option(:public_key)
|
37
|
+
OpenSSL::PKey::RSA.new(File.read(file), nil)
|
38
|
+
end
|
41
39
|
end
|
42
40
|
|
43
41
|
def option(key)
|
data/lib/trocla/formats/x509.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
+
require 'openssl'
|
1
2
|
class Trocla::Formats::X509 < Trocla::Formats::Base
|
2
|
-
require 'openssl'
|
3
3
|
def format(plain_password,options={})
|
4
4
|
|
5
5
|
if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/m)
|
@@ -7,10 +7,15 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
7
7
|
return plain_password
|
8
8
|
end
|
9
9
|
|
10
|
+
cn = nil
|
10
11
|
if options['subject']
|
11
12
|
subject = options['subject']
|
13
|
+
if cna = OpenSSL::X509::Name.parse(subject).to_a.find{|e| e[0] == 'CN' }
|
14
|
+
cn = cna[1]
|
15
|
+
end
|
12
16
|
elsif options['CN']
|
13
17
|
subject = ''
|
18
|
+
cn = options['CN']
|
14
19
|
['C','ST','L','O','OU','CN','emailAddress'].each do |field|
|
15
20
|
subject << "/#{field}=#{options[field]}" if options[field]
|
16
21
|
end
|
@@ -18,24 +23,38 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
18
23
|
raise "You need to pass \"subject\" or \"CN\" as an option to use this format"
|
19
24
|
end
|
20
25
|
hash = options['hash'] || 'sha2'
|
21
|
-
sign_with = options['ca']
|
22
|
-
|
23
|
-
|
24
|
-
days = options['days'].
|
25
|
-
|
26
|
-
|
26
|
+
sign_with = options['ca']
|
27
|
+
become_ca = options['become_ca'] || false
|
28
|
+
keysize = options['keysize'] || 4096
|
29
|
+
days = options['days'].nil? ? 365 : options['days'].to_i
|
30
|
+
name_constraints = Array(options['name_constraints'])
|
31
|
+
|
32
|
+
altnames = if become_ca || (an = options['altnames']) && Array(an).empty?
|
33
|
+
[]
|
34
|
+
else
|
35
|
+
# ensure that we have the CN with us, but only if it
|
36
|
+
# it's like a hostname.
|
37
|
+
# This might have to be improved.
|
38
|
+
if cn.include?(' ')
|
39
|
+
Array(an).collect { |v| "DNS:#{v}" }.join(', ')
|
40
|
+
else
|
41
|
+
(["DNS:#{cn}"] + Array(an).collect { |v| "DNS:#{v}" }).uniq.join(', ')
|
42
|
+
end
|
43
|
+
end
|
27
44
|
|
28
45
|
begin
|
29
46
|
key = mkkey(keysize)
|
30
47
|
rescue Exception => e
|
48
|
+
puts e.backtrace
|
31
49
|
raise "Private key for #{subject} creation failed: #{e.message}"
|
32
50
|
end
|
33
51
|
|
34
52
|
if sign_with # certificate signed with CA
|
35
53
|
begin
|
36
|
-
|
37
|
-
|
38
|
-
|
54
|
+
ca_str = trocla.get_password(sign_with,'x509')
|
55
|
+
ca = OpenSSL::X509::Certificate.new(ca_str)
|
56
|
+
cakey = OpenSSL::PKey::RSA.new(ca_str)
|
57
|
+
caserial = getserial(sign_with)
|
39
58
|
rescue Exception => e
|
40
59
|
raise "Value of #{sign_with} can't be loaded as CA: #{e.message}"
|
41
60
|
end
|
@@ -49,24 +68,24 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
49
68
|
end
|
50
69
|
|
51
70
|
begin
|
52
|
-
csr_cert = mkcert(caserial, request.subject, ca, request.public_key, days, altnames)
|
71
|
+
csr_cert = mkcert(caserial, request.subject, ca, request.public_key, days, altnames, name_constraints, become_ca)
|
53
72
|
csr_cert.sign(cakey, signature(hash))
|
54
|
-
|
73
|
+
addserial(sign_with, caserial)
|
55
74
|
rescue Exception => e
|
56
75
|
raise "Certificate #{subject} signing failed: #{e.message}"
|
57
76
|
end
|
58
77
|
|
59
|
-
key.
|
78
|
+
key.to_pem + csr_cert.to_pem
|
60
79
|
else # self-signed certificate
|
61
80
|
begin
|
62
81
|
subj = OpenSSL::X509::Name.parse(subject)
|
63
|
-
cert = mkcert(
|
82
|
+
cert = mkcert(getserial(subj), subj, nil, key.public_key, days, altnames, name_constraints, become_ca)
|
64
83
|
cert.sign(key, signature(hash))
|
65
84
|
rescue Exception => e
|
66
85
|
raise "Self-signed certificate #{subject} creation failed: #{e.message}"
|
67
86
|
end
|
68
87
|
|
69
|
-
key.
|
88
|
+
key.to_pem + cert.to_pem
|
70
89
|
end
|
71
90
|
end
|
72
91
|
private
|
@@ -95,14 +114,13 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
95
114
|
|
96
115
|
def mkreq(subject,public_key)
|
97
116
|
request = OpenSSL::X509::Request.new
|
98
|
-
request.version = 0
|
99
117
|
request.subject = subject
|
100
118
|
request.public_key = public_key
|
101
119
|
|
102
120
|
request
|
103
121
|
end
|
104
122
|
|
105
|
-
def mkcert(serial,subject,issuer,public_key,days,altnames)
|
123
|
+
def mkcert(serial,subject,issuer,public_key,days,altnames, name_constraints = [], become_ca = false)
|
106
124
|
cert = OpenSSL::X509::Certificate.new
|
107
125
|
issuer = cert if issuer == nil
|
108
126
|
cert.subject = subject
|
@@ -117,29 +135,38 @@ class Trocla::Formats::X509 < Trocla::Formats::Base
|
|
117
135
|
ef.subject_certificate = cert
|
118
136
|
ef.issuer_certificate = issuer
|
119
137
|
cert.extensions = [ ef.create_extension("subjectKeyIdentifier", "hash") ]
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
138
|
+
|
139
|
+
if become_ca
|
140
|
+
cert.add_extension ef.create_extension("basicConstraints","CA:TRUE", true)
|
141
|
+
cert.add_extension ef.create_extension("keyUsage", "keyCertSign, cRLSign, nonRepudiation, digitalSignature, keyEncipherment", true)
|
142
|
+
if name_constraints && !name_constraints.empty?
|
143
|
+
cert.add_extension ef.create_extension("nameConstraints","permitted;DNS:#{name_constraints.join(',permitted;DNS:')}",true)
|
144
|
+
end
|
145
|
+
else
|
146
|
+
cert.add_extension ef.create_extension("subjectAltName", altnames, true) unless altnames.empty?
|
147
|
+
cert.add_extension ef.create_extension("basicConstraints","CA:FALSE", true)
|
148
|
+
cert.add_extension ef.create_extension("keyUsage", "nonRepudiation, digitalSignature, keyEncipherment", true)
|
149
|
+
end
|
124
150
|
cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
|
125
151
|
|
126
152
|
cert
|
127
153
|
end
|
128
154
|
|
129
|
-
def
|
130
|
-
|
155
|
+
def getserial(ca)
|
156
|
+
newser = Trocla::Util.random_str(20,'hexadecimal').to_i(16)
|
157
|
+
all_serials(ca).include?(newser) ? getserial(ca) : newser
|
131
158
|
end
|
132
159
|
|
133
|
-
def
|
134
|
-
|
135
|
-
|
136
|
-
newser + 1
|
160
|
+
def all_serials(ca)
|
161
|
+
if allser = trocla.get_password("#{ca}_all_serials",'plain')
|
162
|
+
YAML.load(allser)
|
137
163
|
else
|
138
|
-
|
164
|
+
[]
|
139
165
|
end
|
140
166
|
end
|
141
167
|
|
142
|
-
def
|
143
|
-
|
168
|
+
def addserial(ca,serial)
|
169
|
+
serials = all_serials(ca) << serial
|
170
|
+
trocla.set_password("#{ca}_all_serials",'plain',YAML.dump(serials))
|
144
171
|
end
|
145
172
|
end
|
data/lib/trocla/store.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# implements the default store behavior
|
2
|
+
class Trocla::Store
|
3
|
+
attr_reader :store_config, :trocla
|
4
|
+
def initialize(config,trocla)
|
5
|
+
@store_config = config
|
6
|
+
@trocla = trocla
|
7
|
+
end
|
8
|
+
|
9
|
+
# should return value for key & format
|
10
|
+
# returns nil if nothing or a nil value
|
11
|
+
# was found.
|
12
|
+
# If a key is expired it must return nil.
|
13
|
+
def get(key,format)
|
14
|
+
raise 'not implemented'
|
15
|
+
end
|
16
|
+
|
17
|
+
# sets value for key & format
|
18
|
+
# setting the plain format must invalidate
|
19
|
+
# all other formats as they should either
|
20
|
+
# be derived from plain or set directly.
|
21
|
+
# options is a hash containing further
|
22
|
+
# information for the store. e.g. expiration
|
23
|
+
# of a key. Keys can have an expiration /
|
24
|
+
# timeout by setting `expires` within
|
25
|
+
# the options hashs. Value of `expires`
|
26
|
+
# must be an integer indicating the
|
27
|
+
# amount of seconds a key can live with.
|
28
|
+
# This mechanism is expected to be
|
29
|
+
# be implemented by the backend.
|
30
|
+
def set(key,format,value,options={})
|
31
|
+
if format == 'plain'
|
32
|
+
set_plain(key,value,options)
|
33
|
+
else
|
34
|
+
set_format(key,format,value,options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# deletes the value for format
|
39
|
+
# if format is nil everything is deleted
|
40
|
+
# returns value of format or hash of
|
41
|
+
# format => value # if everything is
|
42
|
+
# deleted.
|
43
|
+
def delete(key,format=nil)
|
44
|
+
format.nil? ? (delete_all(key)||{}) : delete_format(key,format)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
# sets a new plain value
|
49
|
+
# *must* invalidate all
|
50
|
+
# other formats
|
51
|
+
def set_plain(key,value,options)
|
52
|
+
raise 'not implemented'
|
53
|
+
end
|
54
|
+
|
55
|
+
# sets a value of a format
|
56
|
+
def set_format(key,format,value,options)
|
57
|
+
raise 'not implemented'
|
58
|
+
end
|
59
|
+
|
60
|
+
# deletes all entries of this key
|
61
|
+
# and returns a hash with all
|
62
|
+
# formats and values
|
63
|
+
# or nil if nothing is found
|
64
|
+
def delete_all(key)
|
65
|
+
raise 'not implemented'
|
66
|
+
end
|
67
|
+
|
68
|
+
# deletes the value of the passed
|
69
|
+
# key & format and returns the
|
70
|
+
# value.
|
71
|
+
def delete_format(key,format)
|
72
|
+
raise 'not implemented'
|
73
|
+
end
|
74
|
+
end
|