trocla-ruby2 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.document +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +10 -0
  5. data/CHANGELOG.md +71 -0
  6. data/Gemfile +51 -0
  7. data/LICENSE.txt +15 -0
  8. data/README.md +351 -0
  9. data/Rakefile +53 -0
  10. data/bin/trocla +148 -0
  11. data/ext/redhat/rubygem-trocla.spec +120 -0
  12. data/lib/VERSION +4 -0
  13. data/lib/trocla.rb +162 -0
  14. data/lib/trocla/default_config.yaml +47 -0
  15. data/lib/trocla/encryptions.rb +54 -0
  16. data/lib/trocla/encryptions/none.rb +10 -0
  17. data/lib/trocla/encryptions/ssl.rb +51 -0
  18. data/lib/trocla/formats.rb +54 -0
  19. data/lib/trocla/formats/bcrypt.rb +7 -0
  20. data/lib/trocla/formats/md5crypt.rb +6 -0
  21. data/lib/trocla/formats/mysql.rb +6 -0
  22. data/lib/trocla/formats/pgsql.rb +7 -0
  23. data/lib/trocla/formats/plain.rb +7 -0
  24. data/lib/trocla/formats/sha1.rb +7 -0
  25. data/lib/trocla/formats/sha256crypt.rb +6 -0
  26. data/lib/trocla/formats/sha512crypt.rb +6 -0
  27. data/lib/trocla/formats/ssha.rb +9 -0
  28. data/lib/trocla/formats/sshkey.rb +46 -0
  29. data/lib/trocla/formats/x509.rb +197 -0
  30. data/lib/trocla/store.rb +80 -0
  31. data/lib/trocla/stores.rb +39 -0
  32. data/lib/trocla/stores/memory.rb +56 -0
  33. data/lib/trocla/stores/moneta.rb +58 -0
  34. data/lib/trocla/util.rb +71 -0
  35. data/lib/trocla/version.rb +22 -0
  36. data/spec/data/.keep +0 -0
  37. data/spec/spec_helper.rb +290 -0
  38. data/spec/trocla/encryptions/none_spec.rb +22 -0
  39. data/spec/trocla/encryptions/ssl_spec.rb +26 -0
  40. data/spec/trocla/formats/x509_spec.rb +375 -0
  41. data/spec/trocla/store/memory_spec.rb +6 -0
  42. data/spec/trocla/store/moneta_spec.rb +6 -0
  43. data/spec/trocla/util_spec.rb +54 -0
  44. data/spec/trocla_spec.rb +248 -0
  45. data/trocla-ruby2.gemspec +104 -0
  46. metadata +202 -0
@@ -0,0 +1,47 @@
1
+ ---
2
+ store: :moneta
3
+ store_options:
4
+ adapter: :YAML
5
+ adapter_options:
6
+ :file: '/tmp/trocla.yaml'
7
+
8
+ encryption: :none
9
+ options:
10
+ random: true
11
+ length: 16
12
+ charset: default
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
+ 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
34
+ x509long:
35
+ # 5 years
36
+ days: 1825
37
+ # 1800 days
38
+ expires: 155520000
39
+ x509auto:
40
+ days: 40
41
+ # 30 days
42
+ expires: 2592000
43
+ x509short:
44
+ days: 2
45
+ # 1 day
46
+ expires: 86400
47
+
@@ -0,0 +1,54 @@
1
+ class Trocla::Encryptions
2
+
3
+ class Base
4
+ attr_reader :trocla, :config
5
+ def initialize(config, trocla)
6
+ @trocla = trocla
7
+ @config = config
8
+ end
9
+
10
+ def encrypt(value)
11
+ raise NoMethodError.new("#{self.class.name} needs to implement 'encrypt()'")
12
+ end
13
+
14
+ def decrypt(value)
15
+ raise NoMethodError.new("#{self.class.name} needs to implement 'decrypt()'")
16
+ end
17
+ end
18
+
19
+ class << self
20
+ def [](enc)
21
+ encryptions[enc.to_s.downcase]
22
+ end
23
+
24
+ def all
25
+ Dir[ path '*' ].collect do |enc|
26
+ File.basename(enc, '.rb').downcase
27
+ end
28
+ end
29
+
30
+ def available?(encryption)
31
+ all.include?(encryption.to_s.downcase)
32
+ end
33
+
34
+ private
35
+ def encryptions
36
+ @@encryptions ||= Hash.new do |hash, encryption|
37
+ encryption = encryption.to_s.downcase
38
+ if File.exists?( path encryption )
39
+ require "trocla/encryptions/#{encryption}"
40
+ class_name = "Trocla::Encryptions::#{encryption.capitalize}"
41
+ hash[encryption] = (eval class_name)
42
+ else
43
+ raise "Encryption #{encryption} is not supported!"
44
+ end
45
+ end
46
+ end
47
+
48
+ def path(encryption)
49
+ File.expand_path(
50
+ File.join(File.dirname(__FILE__), 'encryptions', "#{encryption}.rb")
51
+ )
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,10 @@
1
+ class Trocla::Encryptions::None < Trocla::Encryptions::Base
2
+ def encrypt(value)
3
+ value
4
+ end
5
+
6
+ def decrypt(value)
7
+ value
8
+ end
9
+ end
10
+
@@ -0,0 +1,51 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ class Trocla::Encryptions::Ssl < Trocla::Encryptions::Base
5
+ def encrypt(value)
6
+ ciphertext = ''
7
+ value.scan(/.{0,#{chunksize}}/m).each do |chunk|
8
+ ciphertext += Base64.encode64(public_key.public_encrypt(chunk)).gsub("\n",'')+"\n" if chunk
9
+ end
10
+ ciphertext
11
+ end
12
+
13
+ def decrypt(value)
14
+ plaintext = ''
15
+ value.split(/\n/).each do |line|
16
+ plaintext += private_key.private_decrypt(Base64.decode64(line)) if line
17
+ end
18
+ plaintext
19
+ end
20
+
21
+ private
22
+
23
+ def chunksize
24
+ public_key.n.num_bytes - 11
25
+ end
26
+
27
+ def private_key
28
+ @private_key ||= begin
29
+ file = require_option(:private_key)
30
+ OpenSSL::PKey::RSA.new(File.read(file), nil)
31
+ end
32
+ end
33
+
34
+ def public_key
35
+ @public_key ||= begin
36
+ file = require_option(:public_key)
37
+ OpenSSL::PKey::RSA.new(File.read(file), nil)
38
+ end
39
+ end
40
+
41
+ def option(key)
42
+ config[key]
43
+ end
44
+
45
+ def require_option(key)
46
+ val = option(key)
47
+ raise "Config error: 'ssl_options' => :#{key} is not defined" if val.nil?
48
+ val
49
+ end
50
+ end
51
+
@@ -0,0 +1,54 @@
1
+ class Trocla::Formats
2
+
3
+ class Base
4
+ attr_reader :trocla
5
+ def initialize(trocla)
6
+ @trocla = trocla
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
22
+ end
23
+
24
+ class << self
25
+ def [](format)
26
+ formats[format.downcase]
27
+ end
28
+
29
+ def all
30
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'formats','*.rb'))].collect{|f| File.basename(f,'.rb').downcase }
31
+ end
32
+
33
+ def available?(format)
34
+ all.include?(format.downcase)
35
+ end
36
+
37
+ private
38
+ def formats
39
+ @@formats ||= Hash.new do |hash, format|
40
+ format = format.downcase
41
+ if File.exists?(path(format))
42
+ require "trocla/formats/#{format}"
43
+ hash[format] = (eval "Trocla::Formats::#{format.capitalize}")
44
+ else
45
+ raise "Format #{format} is not supported!"
46
+ end
47
+ end
48
+ end
49
+
50
+ def path(format)
51
+ File.expand_path(File.join(File.dirname(__FILE__),'formats',"#{format}.rb"))
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,7 @@
1
+ class Trocla::Formats::Bcrypt < Trocla::Formats::Base
2
+ expensive true
3
+ require 'bcrypt'
4
+ def format(plain_password,options={})
5
+ BCrypt::Password.create(plain_password, :cost => options['cost']||BCrypt::Engine.cost).to_s
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # salted crypt
2
+ class Trocla::Formats::Md5crypt < Trocla::Formats::Base
3
+ def format(plain_password,options={})
4
+ plain_password.crypt('$1$' << Trocla::Util.salt << '$')
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class Trocla::Formats::Mysql < Trocla::Formats::Base
2
+ require 'digest/sha1'
3
+ def format(plain_password,options={})
4
+ "*" + Digest::SHA1.hexdigest(Digest::SHA1.digest(plain_password)).upcase
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ class Trocla::Formats::Pgsql < Trocla::Formats::Base
2
+ require 'digest/md5'
3
+ def format(plain_password,options={})
4
+ raise "You need pass the username as an option to use this format" unless options['username']
5
+ "md5" + Digest::MD5.hexdigest(plain_password + options['username'])
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ class Trocla::Formats::Plain < Trocla::Formats::Base
2
+
3
+ def format(plain_password,options={})
4
+ plain_password
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ class Trocla::Formats::Sha1 < Trocla::Formats::Base
2
+ require 'digest/sha1'
3
+ require 'base64'
4
+ def format(plain_password,options={})
5
+ '{SHA}' + Base64.encode64(Digest::SHA1.digest(plain_password))
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # salted crypt
2
+ class Trocla::Formats::Sha256crypt < Trocla::Formats::Base
3
+ def format(plain_password,options={})
4
+ plain_password.crypt('$5$' << Trocla::Util.salt << '$')
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # salted crypt
2
+ class Trocla::Formats::Sha512crypt < Trocla::Formats::Base
3
+ def format(plain_password,options={})
4
+ plain_password.crypt('$6$' << Trocla::Util.salt << '$')
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ # salted crypt
2
+ require 'base64'
3
+ require 'digest'
4
+ class Trocla::Formats::Ssha < Trocla::Formats::Base
5
+ def format(plain_password,options={})
6
+ salt = options['salt'] || Trocla::Util.salt(16)
7
+ "{SSHA}"+Base64.encode64("#{Digest::SHA1.digest("#{plain_password}#{salt}")}#{salt}").chomp
8
+ end
9
+ 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
@@ -0,0 +1,197 @@
1
+ require 'openssl'
2
+ class Trocla::Formats::X509 < Trocla::Formats::Base
3
+ expensive true
4
+ def format(plain_password,options={})
5
+
6
+ if plain_password.match(/-----BEGIN RSA PRIVATE KEY-----.*-----END RSA PRIVATE KEY-----.*-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----/m)
7
+ # just an import, don't generate any new keys
8
+ return plain_password
9
+ end
10
+
11
+ cn = nil
12
+ if options['subject']
13
+ subject = options['subject']
14
+ if cna = OpenSSL::X509::Name.parse(subject).to_a.find{|e| e[0] == 'CN' }
15
+ cn = cna[1]
16
+ end
17
+ elsif options['CN']
18
+ subject = ''
19
+ cn = options['CN']
20
+ ['C','ST','L','O','OU','CN','emailAddress'].each do |field|
21
+ subject << "/#{field}=#{options[field]}" if options[field]
22
+ end
23
+ else
24
+ raise "You need to pass \"subject\" or \"CN\" as an option to use this format"
25
+ end
26
+ hash = options['hash'] || 'sha2'
27
+ sign_with = options['ca']
28
+ become_ca = options['become_ca'] || false
29
+ keysize = options['keysize'] || 4096
30
+ days = options['days'].nil? ? 365 : options['days'].to_i
31
+ name_constraints = Array(options['name_constraints'])
32
+ key_usages = options['key_usages']
33
+ key_usages = Array(key_usages) if key_usages
34
+
35
+ altnames = if become_ca || (an = options['altnames']) && Array(an).empty?
36
+ []
37
+ else
38
+ # ensure that we have the CN with us, but only if it
39
+ # it's like a hostname.
40
+ # This might have to be improved.
41
+ if cn.include?(' ')
42
+ Array(an).collect { |v| "DNS:#{v}" }.join(', ')
43
+ else
44
+ (["DNS:#{cn}"] + Array(an).collect { |v| "DNS:#{v}" }).uniq.join(', ')
45
+ end
46
+ end
47
+
48
+ begin
49
+ key = mkkey(keysize)
50
+ rescue Exception => e
51
+ puts e.backtrace
52
+ raise "Private key for #{subject} creation failed: #{e.message}"
53
+ end
54
+
55
+ cert = nil
56
+ if sign_with # certificate signed with CA
57
+ begin
58
+ ca_str = trocla.get_password(sign_with,'x509')
59
+ ca = OpenSSL::X509::Certificate.new(ca_str)
60
+ cakey = OpenSSL::PKey::RSA.new(ca_str)
61
+ caserial = getserial(sign_with)
62
+ rescue Exception => e
63
+ raise "Value of #{sign_with} can't be loaded as CA: #{e.message}"
64
+ end
65
+
66
+ begin
67
+ subj = OpenSSL::X509::Name.parse(subject)
68
+ request = mkreq(subj, key.public_key)
69
+ request.sign(key, signature(hash))
70
+ rescue Exception => e
71
+ raise "Certificate request #{subject} creation failed: #{e.message}"
72
+ end
73
+
74
+ begin
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))
78
+ addserial(sign_with, caserial)
79
+ rescue Exception => e
80
+ raise "Certificate #{subject} signing failed: #{e.message}"
81
+ end
82
+ else # self-signed certificate
83
+ begin
84
+ subj = OpenSSL::X509::Name.parse(subject)
85
+ cert = mkcert(getserial(subj), subj, nil, key.public_key, days,
86
+ altnames, key_usages, name_constraints, become_ca)
87
+ cert.sign(key, signature(hash))
88
+ rescue Exception => e
89
+ raise "Self-signed certificate #{subject} creation failed: #{e.message}"
90
+ end
91
+ end
92
+ key.to_pem + cert.to_pem
93
+ end
94
+
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
+ else
101
+ super(output,render_options)
102
+ end
103
+ end
104
+
105
+ private
106
+ # nice help: https://gist.github.com/mitfik/1922961
107
+
108
+ def signature(hash = 'sha2')
109
+ if hash == 'sha1'
110
+ OpenSSL::Digest::SHA1.new
111
+ elsif hash == 'sha224'
112
+ OpenSSL::Digest::SHA224.new
113
+ elsif hash == 'sha2' || hash == 'sha256'
114
+ OpenSSL::Digest::SHA256.new
115
+ elsif hash == 'sha384'
116
+ OpenSSL::Digest::SHA384.new
117
+ elsif hash == 'sha512'
118
+ OpenSSL::Digest::SHA512.new
119
+ else
120
+ raise "Unrecognized hash: #{hash}"
121
+ end
122
+ end
123
+
124
+ def mkkey(len)
125
+ OpenSSL::PKey::RSA.generate(len)
126
+ end
127
+
128
+ def mkreq(subject,public_key)
129
+ request = OpenSSL::X509::Request.new
130
+ request.subject = subject
131
+ request.public_key = public_key
132
+
133
+ request
134
+ end
135
+
136
+ def mkcert(serial,subject,issuer,public_key,days,altnames, key_usages = nil, name_constraints = [], become_ca = false)
137
+ cert = OpenSSL::X509::Certificate.new
138
+ issuer = cert if issuer == nil
139
+ cert.subject = subject
140
+ cert.issuer = issuer.subject
141
+ cert.not_before = Time.now
142
+ cert.not_after = Time.now + days * 24 * 60 * 60
143
+ cert.public_key = public_key
144
+ cert.serial = serial
145
+ cert.version = 2
146
+
147
+ ef = OpenSSL::X509::ExtensionFactory.new
148
+ ef.subject_certificate = cert
149
+ ef.issuer_certificate = issuer
150
+ cert.extensions = [ ef.create_extension("subjectKeyIdentifier", "hash") ]
151
+
152
+ if become_ca
153
+ cert.add_extension ef.create_extension("basicConstraints","CA:TRUE", true)
154
+ unless (ku = key_usages || ca_key_usages).empty?
155
+ cert.add_extension ef.create_extension("keyUsage", ku.join(', '), true)
156
+ end
157
+ if name_constraints && !name_constraints.empty?
158
+ cert.add_extension ef.create_extension("nameConstraints","permitted;DNS:#{name_constraints.join(',permitted;DNS:')}",true)
159
+ end
160
+ else
161
+ cert.add_extension ef.create_extension("subjectAltName", altnames, true) unless altnames.empty?
162
+ cert.add_extension ef.create_extension("basicConstraints","CA:FALSE", true)
163
+ unless (ku = key_usages || cert_key_usages).empty?
164
+ cert.add_extension ef.create_extension("keyUsage", ku.join(', '), true)
165
+ end
166
+ end
167
+ cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
168
+
169
+ cert
170
+ end
171
+
172
+ def getserial(ca)
173
+ newser = Trocla::Util.random_str(20,'hexadecimal').to_i(16)
174
+ all_serials(ca).include?(newser) ? getserial(ca) : newser
175
+ end
176
+
177
+ def all_serials(ca)
178
+ if allser = trocla.get_password("#{ca}_all_serials",'plain')
179
+ YAML.load(allser)
180
+ else
181
+ []
182
+ end
183
+ end
184
+
185
+ def addserial(ca,serial)
186
+ serials = all_serials(ca) << serial
187
+ trocla.set_password("#{ca}_all_serials",'plain',YAML.dump(serials))
188
+ end
189
+
190
+ def cert_key_usages
191
+ ['nonRepudiation', 'digitalSignature', 'keyEncipherment']
192
+ end
193
+ def ca_key_usages
194
+ ['keyCertSign', 'cRLSign', 'nonRepudiation',
195
+ 'digitalSignature', 'keyEncipherment' ]
196
+ end
197
+ end