trocla-ruby2 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +4 -0
- data/.rspec +1 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +71 -0
- data/Gemfile +51 -0
- data/LICENSE.txt +15 -0
- data/README.md +351 -0
- data/Rakefile +53 -0
- data/bin/trocla +148 -0
- data/ext/redhat/rubygem-trocla.spec +120 -0
- data/lib/VERSION +4 -0
- data/lib/trocla.rb +162 -0
- data/lib/trocla/default_config.yaml +47 -0
- data/lib/trocla/encryptions.rb +54 -0
- data/lib/trocla/encryptions/none.rb +10 -0
- data/lib/trocla/encryptions/ssl.rb +51 -0
- data/lib/trocla/formats.rb +54 -0
- data/lib/trocla/formats/bcrypt.rb +7 -0
- data/lib/trocla/formats/md5crypt.rb +6 -0
- data/lib/trocla/formats/mysql.rb +6 -0
- data/lib/trocla/formats/pgsql.rb +7 -0
- data/lib/trocla/formats/plain.rb +7 -0
- data/lib/trocla/formats/sha1.rb +7 -0
- data/lib/trocla/formats/sha256crypt.rb +6 -0
- data/lib/trocla/formats/sha512crypt.rb +6 -0
- data/lib/trocla/formats/ssha.rb +9 -0
- data/lib/trocla/formats/sshkey.rb +46 -0
- data/lib/trocla/formats/x509.rb +197 -0
- data/lib/trocla/store.rb +80 -0
- data/lib/trocla/stores.rb +39 -0
- data/lib/trocla/stores/memory.rb +56 -0
- data/lib/trocla/stores/moneta.rb +58 -0
- data/lib/trocla/util.rb +71 -0
- data/lib/trocla/version.rb +22 -0
- data/spec/data/.keep +0 -0
- data/spec/spec_helper.rb +290 -0
- data/spec/trocla/encryptions/none_spec.rb +22 -0
- data/spec/trocla/encryptions/ssl_spec.rb +26 -0
- data/spec/trocla/formats/x509_spec.rb +375 -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 +54 -0
- data/spec/trocla_spec.rb +248 -0
- data/trocla-ruby2.gemspec +104 -0
- 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,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::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,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
|