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.
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(cache.fetch(key, {})[format])
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
- if format.nil?
45
- decrypt(cache.delete(key))
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
- old_val = (h = cache.fetch(key,{})).delete(format)
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
- if (format == 'plain')
55
- h = (cache[key] = { 'plain' => encrypt(password) })
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
- enc = config['encryption']
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 cache
79
- @cache ||= build_cache
79
+ def store
80
+ @store ||= build_store
80
81
  end
81
82
 
82
- def build_cache
83
- require 'moneta'
84
- lconfig = config
85
- Moneta.new(lconfig['adapter'], lconfig['adapter_options']||{})
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: 12
11
+ length: 16
5
12
  charset: default
6
- adapter: :YAML
7
- adapter_options:
8
- :file: '/tmp/trocla.yaml'
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
+
@@ -1,9 +1,10 @@
1
1
  class Trocla::Encryptions
2
2
 
3
3
  class Base
4
- attr_reader :trocla
5
- def initialize(trocla)
4
+ attr_reader :trocla, :config
5
+ def initialize(config, trocla)
6
6
  @trocla = trocla
7
+ @config = config
7
8
  end
8
9
 
9
10
  def encrypt(value)
@@ -25,19 +25,17 @@ class Trocla::Encryptions::Ssl < Trocla::Encryptions::Base
25
25
  end
26
26
 
27
27
  def private_key
28
- pass = nil
29
- file = require_option(:private_key)
30
- @private_key ||= OpenSSL::PKey::RSA.new(File.read(file), nil)
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
- file = require_option(:public_key)
35
- @public_key ||= OpenSSL::PKey::RSA.new(File.read(file), nil)
36
- end
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)
@@ -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'] || nil
22
- keysize = options['keysize'] || 2048
23
- serial = options['serial'] || 1
24
- days = options['days'].to_i || 365
25
- altnames = options['altnames'] || nil
26
- altnames.collect { |v| "DNS:#{v}" }.join(', ') if altnames
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
- ca = OpenSSL::X509::Certificate.new(getca(sign_with))
37
- cakey = OpenSSL::PKey::RSA.new(getca(sign_with))
38
- caserial = getserial(sign_with, serial)
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
- setserial(sign_with, caserial)
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.send("to_pem") + csr_cert.send("to_pem")
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(serial, subj, nil, key.public_key, days, altnames)
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.send("to_pem") + cert.send("to_pem")
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
- cert.add_extension ef.create_extension("basicConstraints","CA:TRUE", true) if subject == issuer
121
- cert.add_extension ef.create_extension("basicConstraints","CA:FALSE", true) if subject != issuer
122
- cert.add_extension ef.create_extension("keyUsage", "nonRepudiation, digitalSignature, keyEncipherment", true)
123
- cert.add_extension ef.create_extension("subjectAltName", altnames, true) if altnames
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 getca(ca)
130
- trocla.get_password(ca,'x509')
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 getserial(ca,serial)
134
- newser = trocla.get_password("#{ca}_serial",'plain')
135
- if newser
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
- serial
164
+ []
139
165
  end
140
166
  end
141
167
 
142
- def setserial(ca,serial)
143
- trocla.set_password("#{ca}_serial",'plain',serial)
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
@@ -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