trocla-ruby2 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 +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
|