vpnmaker 0.0.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/.document +5 -0
- data/Gemfile +25 -0
- data/README.rdoc +91 -0
- data/Rakefile +99 -0
- data/VERSION +1 -0
- data/bin/vpnmaker +11 -0
- data/foocorp.config.yaml +24 -0
- data/lib/client.haml +13 -0
- data/lib/openssl.haml +144 -0
- data/lib/server.haml +38 -0
- data/lib/vpnmaker.rb +45 -0
- data/lib/vpnmaker/config_generator.rb +36 -0
- data/lib/vpnmaker/key_builder.rb +141 -0
- data/lib/vpnmaker/key_config.rb +9 -0
- data/lib/vpnmaker/key_db.rb +62 -0
- data/lib/vpnmaker/key_tracker.rb +158 -0
- data/lib/vpnmaker/manager.rb +55 -0
- data/vpnmaker.gemspec +99 -0
- metadata +308 -0
data/lib/vpnmaker.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'gibberish'
|
4
|
+
require 'rubyzip'
|
5
|
+
|
6
|
+
require 'fileutils'
|
7
|
+
require 'yaml'
|
8
|
+
require 'socket'
|
9
|
+
|
10
|
+
require 'ipaddr'
|
11
|
+
require 'ipaddr_extensions'
|
12
|
+
require 'haml'
|
13
|
+
|
14
|
+
require 'pry'
|
15
|
+
|
16
|
+
class String
|
17
|
+
def path(p)
|
18
|
+
File.join(File.dirname(__FILE__), p)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class HashBinding < Object
|
23
|
+
def self.from_hash(h)
|
24
|
+
hb = self.new
|
25
|
+
h.each do |k, v|
|
26
|
+
hb.instance_variable_set("@#{k}", v)
|
27
|
+
end
|
28
|
+
hb
|
29
|
+
end
|
30
|
+
|
31
|
+
def binding; super; end # normally private
|
32
|
+
end
|
33
|
+
|
34
|
+
module VPNMaker
|
35
|
+
autoload :ConfigGenerator, './vpnmaker/config_generator'
|
36
|
+
autoload :KeyDB, './vpnmaker/key_db'
|
37
|
+
autoload :KeyConfig, './vpnmaker/key_config'
|
38
|
+
autoload :KeyTracker, './vpnmaker/key_tracker'
|
39
|
+
autoload :Manager, './vpnmaker/manager'
|
40
|
+
autoload :KeyBuilder, './vpnmaker/key_builder'
|
41
|
+
|
42
|
+
def self.generate(*args)
|
43
|
+
KeyTracker.generate(args.first, args.last)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module VPNMaker
|
2
|
+
class ConfigGenerator
|
3
|
+
def initialize(mgr)
|
4
|
+
@mgr = mgr
|
5
|
+
end
|
6
|
+
|
7
|
+
def client_conf(client)
|
8
|
+
{
|
9
|
+
:gen_host => Socket.gethostname,
|
10
|
+
:server => @mgr.config[:server],
|
11
|
+
:client => @mgr.config[:client]
|
12
|
+
}.merge(client)
|
13
|
+
end
|
14
|
+
|
15
|
+
def server_conf
|
16
|
+
{
|
17
|
+
:gen_host => Socket.gethostname
|
18
|
+
}.merge(@mgr.config[:server])
|
19
|
+
end
|
20
|
+
|
21
|
+
def server
|
22
|
+
haml_vars = server_conf.dup
|
23
|
+
haml_vars[:base_ip] = ((a = IPAddr.new haml_vars[:base_ip]); {:net => a.to_s, :mask => a.subnet_mask.to_s})
|
24
|
+
haml_vars[:bridgednets] = haml_vars[:bridgednets].map {|net| a = (IPAddr.new net); {:net => a.to_s, :mask => a.subnet_mask.to_s}}
|
25
|
+
haml_vars[:subnets] = haml_vars[:subnets].map {|net| a = (IPAddr.new net); {:net => a.to_s, :mask => a.subnet_mask.to_s}}
|
26
|
+
template = File.read(__FILE__.path('server.haml'))
|
27
|
+
Haml::Engine.new(template).render(Object.new, haml_vars)
|
28
|
+
end
|
29
|
+
|
30
|
+
def client(client)
|
31
|
+
haml_vars = client_conf(client).dup
|
32
|
+
template = File.read(__FILE__.path('client.haml'))
|
33
|
+
Haml::Engine.new(template).render(Object.new, haml_vars)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module VPNMaker
|
2
|
+
class KeyBuilder
|
3
|
+
def initialize(tracker, config)
|
4
|
+
@tmpdir = '/tmp/keybuilder'
|
5
|
+
clean_tmpdir
|
6
|
+
@tracker = tracker
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def clean_tmpdir
|
11
|
+
FileUtils.rm_rf(@tmpdir)
|
12
|
+
FileUtils.mkdir_p(@tmpdir)
|
13
|
+
end
|
14
|
+
|
15
|
+
def cnfpath; "/tmp/openssl-#{$$}.cnf"; end
|
16
|
+
|
17
|
+
def opensslvars
|
18
|
+
{
|
19
|
+
:key_size => 1024,
|
20
|
+
:key_dir => @tmpdir,
|
21
|
+
:key_country => @config[:key_properties][:country],
|
22
|
+
:key_province => @config[:key_properties][:province],
|
23
|
+
:key_city => @config[:key_properties][:city],
|
24
|
+
:key_org => @config[:key_properties][:organization],
|
25
|
+
:key_email => @config[:key_properties][:email],
|
26
|
+
:key_org => @config[:key_properties][:organization],
|
27
|
+
:key_ou => 'Organization Unit',
|
28
|
+
:key_cn => 'Common Name',
|
29
|
+
:key_name => 'Name'
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def init
|
34
|
+
`touch #{@dir}/index.txt`
|
35
|
+
`echo 01 > #{@dir}/serial`
|
36
|
+
end
|
37
|
+
|
38
|
+
def opensslcnf(hash={})
|
39
|
+
c = cnfpath
|
40
|
+
|
41
|
+
File.open(cnfpath, 'w') do |f|
|
42
|
+
f.write(Haml::Engine.new(File.read __FILE__.path('openssl.haml')).render(Object.new, opensslvars.merge(hash)))
|
43
|
+
end
|
44
|
+
|
45
|
+
c
|
46
|
+
end
|
47
|
+
|
48
|
+
# Build Diffie-Hellman parameters for the server side of an SSL/TLS connection.
|
49
|
+
def build_dh_key(keysize=1024)
|
50
|
+
`openssl dhparam -out #{tmppath('dh.pem')} #{keysize}`
|
51
|
+
@tracker.set_dh(tmpfile('dh.pem'))
|
52
|
+
end
|
53
|
+
|
54
|
+
def ca
|
55
|
+
@tracker[:ca]
|
56
|
+
end
|
57
|
+
|
58
|
+
def gen_crl
|
59
|
+
`openssl ca -gencrl -crldays 3650 -keyfile #{tmppath('ca.key')} -cert #{tmppath('ca.crt')} -out #{tmppath('crl.pem')} -config #{opensslcnf}`
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_ca
|
63
|
+
index = tmppath('index.txt')
|
64
|
+
|
65
|
+
FileUtils.touch(index)
|
66
|
+
|
67
|
+
`openssl req -batch -days 3650 -nodes -new -x509 -keyout #{@tmpdir}/ca.key -out #{@tmpdir}/ca.crt -config #{opensslcnf}`
|
68
|
+
gen_crl
|
69
|
+
@tracker.set_ca(tmpfile('ca.key'), tmpfile('ca.crt'), tmpfile('crl.pem'), tmpfile('index.txt'), "01\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_server_key
|
73
|
+
place_file('ca.crt')
|
74
|
+
place_file('ca.key')
|
75
|
+
place_file('index.txt')
|
76
|
+
place_file('serial')
|
77
|
+
|
78
|
+
`openssl req -batch -days 3650 -nodes -new -keyout #{tmppath('server.key')} -out #{tmppath('server.csr')} -extensions server -config #{opensslcnf}`
|
79
|
+
`openssl ca -batch -days 3650 -out #{tmppath('server.crt')} -in #{tmppath('server.csr')} -extensions server -config #{opensslcnf}`
|
80
|
+
|
81
|
+
@tracker.set_server_key(tmpfile('server.key'), tmpfile('server.crt'), tmpfile('index.txt'), tmpfile('serial'))
|
82
|
+
end
|
83
|
+
|
84
|
+
def build_ta_key
|
85
|
+
`openvpn --genkey --secret #{tmppath('ta.key')}`
|
86
|
+
@tracker.set_ta_key(tmpfile('ta.key'))
|
87
|
+
end
|
88
|
+
|
89
|
+
def place_file(name)
|
90
|
+
if data = @tracker.db.data(name)
|
91
|
+
File.open(File.join(@tmpdir, name), 'w') {|f| f.write(data)}
|
92
|
+
else
|
93
|
+
raise "No data for #{name}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def tmppath(f, extn=nil); File.join(@tmpdir, extn ? "#{f}.#{extn}" : f); end
|
98
|
+
def tmpfile(*args); File.read(tmppath(*args)); end
|
99
|
+
|
100
|
+
def build_key(user, name, email, pass, delegate)
|
101
|
+
h = {:key_cn => user, :key_name => name, :key_email => email}
|
102
|
+
place_file('ca.crt')
|
103
|
+
place_file('ca.key')
|
104
|
+
place_file('index.txt')
|
105
|
+
place_file('serial')
|
106
|
+
if pass
|
107
|
+
pass_spec = "-passin 'pass:#{pass}' -passout 'pass:#{pass}'"
|
108
|
+
else
|
109
|
+
pass_spec = '-nodes'
|
110
|
+
end
|
111
|
+
|
112
|
+
`openssl req -batch -days 3650 -new -keyout #{tmppath(user, 'key')} -out #{tmppath(user, 'csr')} -config #{opensslcnf(h)} #{pass_spec}`
|
113
|
+
`openssl ca -batch -days 3650 -out #{tmppath(user, 'crt')} -in #{tmppath(user, 'csr')} -config #{opensslcnf(h)}`
|
114
|
+
# TODO: this still asks for the export password
|
115
|
+
`openssl pkcs12 -export -clcerts -in #{tmppath(user, 'crt')} -inkey #{tmppath(user, 'key')} -out #{tmppath(user, 'p12')} #{pass_spec}`
|
116
|
+
@tracker.send(delegate, user, name, email, tmpfile(user, 'key'), tmpfile(user, 'crt'), tmpfile(user, 'p12'), tmpfile('index.txt'), tmpfile('serial'))
|
117
|
+
end
|
118
|
+
|
119
|
+
def revoke_key(user, version)
|
120
|
+
h = {:key_cn => ""}
|
121
|
+
place_file('ca.crt')
|
122
|
+
place_file('ca.key')
|
123
|
+
place_file('crl.pem')
|
124
|
+
place_file('index.txt')
|
125
|
+
place_file('serial')
|
126
|
+
|
127
|
+
user_crt = tmppath(user, 'crt')
|
128
|
+
rev_crt = tmppath('rev-test.crt')
|
129
|
+
File.open(user_crt, 'w') {|f| f.write(@tracker.key(user, version, 'crt'))}
|
130
|
+
`openssl ca -revoke #{user_crt} -keyfile #{tmppath('ca.key')} -cert #{tmppath('ca.crt')} -config #{opensslcnf(h)}`
|
131
|
+
gen_crl
|
132
|
+
|
133
|
+
File.open(rev_crt, 'w') {|f| f.write(File.read(tmppath('ca.crt'))); f.write(File.read(tmppath('crl.pem')))}
|
134
|
+
if `openssl verify -CAfile #{rev_crt} -crl_check #{user_crt}` =~ /certificate revoked/
|
135
|
+
@tracker.user_key_revoked(user, version, tmpfile('crl.pem'), tmpfile('index.txt'))
|
136
|
+
else
|
137
|
+
raise "Revocation verification failed: openssl isn't recognizing it"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module VPNMaker
|
2
|
+
class KeyDB
|
3
|
+
def initialize(path)
|
4
|
+
@path = path
|
5
|
+
@db = File.exists?(path) ? YAML.load_file(path) : {}
|
6
|
+
@touched = false
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](k); @db[k]; end
|
10
|
+
|
11
|
+
def []=(k, v)
|
12
|
+
@db[k] = v
|
13
|
+
@db[:modified] = Time.now
|
14
|
+
@touched = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def touched!
|
18
|
+
@touched = true
|
19
|
+
@db[:modified] = Time.now
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def datadir; self[:datadir]; end
|
24
|
+
|
25
|
+
def data_path(k)
|
26
|
+
File.join(File.dirname(@path), self.datadir, k)
|
27
|
+
end
|
28
|
+
|
29
|
+
def dump(k, v, overwrite=false)
|
30
|
+
p = data_path(k)
|
31
|
+
raise "#{k} already exists" if File.exists?(p) && !overwrite
|
32
|
+
File.open(p, 'w') {|f| f.write(v)}
|
33
|
+
@touched = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def data(k)
|
37
|
+
File.exists?(data_path(k)) ? File.read(data_path(k)) : nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def disk_version
|
41
|
+
File.exists?(@path) ? YAML.load_file(@path)[:version] : 0
|
42
|
+
end
|
43
|
+
|
44
|
+
def sync
|
45
|
+
if disk_version == @db[:version]
|
46
|
+
if @touched
|
47
|
+
FileUtils.mkdir_p(self.datadir)
|
48
|
+
@db[:version] += 1
|
49
|
+
File.open(@path, 'w') {|f| f.write(@db.to_yaml)}
|
50
|
+
true
|
51
|
+
else
|
52
|
+
false
|
53
|
+
end
|
54
|
+
else
|
55
|
+
raise "Disk version of #{@path} (#{disk_version}) != loaded version (#{@db[:version]}). " + \
|
56
|
+
"Try reloading and making your changes again."
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def version; @db[:version]; end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module VPNMaker
|
2
|
+
class KeyTracker
|
3
|
+
attr_reader :builder
|
4
|
+
attr_reader :db
|
5
|
+
attr_reader :config
|
6
|
+
|
7
|
+
def self.generate(name, path=nil)
|
8
|
+
path ||= '/tmp'
|
9
|
+
dir = File.join(File.expand_path(path), name + '.vpn')
|
10
|
+
|
11
|
+
FileUtils.mkdir_p(dir)
|
12
|
+
datadir = "#{name}_data"
|
13
|
+
dbpath = File.join(dir, "#{name}.db.yaml")
|
14
|
+
|
15
|
+
d = KeyDB.new(dbpath)
|
16
|
+
d[:version] = 0
|
17
|
+
d[:modified] = Time.now
|
18
|
+
d[:users] = {}
|
19
|
+
d[:datadir] = datadir
|
20
|
+
d.sync
|
21
|
+
end
|
22
|
+
|
23
|
+
def assert_user(user)
|
24
|
+
raise "User doesn't exist: #{user}" unless @db[:users][user]
|
25
|
+
end
|
26
|
+
|
27
|
+
def ca; @db[:ca]; end
|
28
|
+
|
29
|
+
def set_ca(key, crt, crl, index, serial)
|
30
|
+
raise "CA already set" if @db[:ca]
|
31
|
+
|
32
|
+
@db[:ca] = {:modified => Time.now}
|
33
|
+
@db.dump('ca.key', key)
|
34
|
+
@db.dump('ca.crt', crt)
|
35
|
+
@db.dump('crl.pem', crl)
|
36
|
+
@db.dump('index.txt', index)
|
37
|
+
@db.dump('serial', serial)
|
38
|
+
@db.touched!
|
39
|
+
@db.sync
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_server_key(key, crt, index, serial)
|
43
|
+
raise "Server key already set" if @db[:server]
|
44
|
+
|
45
|
+
@db[:server] = {:modified => Time.now}
|
46
|
+
@db.dump('server.key', key)
|
47
|
+
@db.dump('server.crt', crt)
|
48
|
+
@db.dump('index.txt', index, true)
|
49
|
+
@db.dump('serial', serial, true)
|
50
|
+
@db.touched!
|
51
|
+
@db.sync
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_ta_key(ta)
|
55
|
+
raise "TA key already set" if @db[:ta]
|
56
|
+
|
57
|
+
@db[:ta] = {:modified => Time.now}
|
58
|
+
@db.dump('ta.key', ta)
|
59
|
+
@db.touched!
|
60
|
+
@db.sync
|
61
|
+
end
|
62
|
+
|
63
|
+
def set_dh(dh)
|
64
|
+
raise "DH key already set" if @db[:dh]
|
65
|
+
|
66
|
+
@db[:dh] = {:modified => Time.now}
|
67
|
+
@db.dump('dh.pem', dh)
|
68
|
+
@db.touched!
|
69
|
+
@db.sync
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_key(user, key, crt, p12, ver)
|
73
|
+
@db.dump("#{user}-#{ver}.key", key)
|
74
|
+
@db.dump("#{user}-#{ver}.crt", crt)
|
75
|
+
@db.dump("#{user}-#{ver}.p12", p12)
|
76
|
+
end
|
77
|
+
|
78
|
+
def key(user, ver, type)
|
79
|
+
@db.data("#{user}-#{ver}.#{type}")
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_user(user, name, email, key, crt, p12, index, serial)
|
83
|
+
raise "User must be a non-empty string" unless user.is_a?(String) && user.size > 0
|
84
|
+
raise "User already exists: #{user}" if @db[:users][user]
|
85
|
+
|
86
|
+
@db[:users][user] = {
|
87
|
+
:user => user,
|
88
|
+
:name => name,
|
89
|
+
:email => email,
|
90
|
+
:active_key => 0,
|
91
|
+
:revoked => [],
|
92
|
+
:modified => Time.now
|
93
|
+
}
|
94
|
+
@db.dump('serial', serial, true)
|
95
|
+
@db.dump('index.txt', index, true)
|
96
|
+
add_key(user, key, crt, p12, 0)
|
97
|
+
@db.touched!
|
98
|
+
@db.sync
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_user_key(user, name, email, key, crt, p12, index, serial)
|
102
|
+
assert_user(user)
|
103
|
+
|
104
|
+
u = @db[:users][user]
|
105
|
+
u[:modified] = Time.now
|
106
|
+
u[:active_key] += 1
|
107
|
+
add_key(user, key, crt, p12, u[:active_key])
|
108
|
+
|
109
|
+
@db.dump('serial', serial, true)
|
110
|
+
@db.dump('index.txt', index, true)
|
111
|
+
|
112
|
+
@db.touched!
|
113
|
+
@db.sync
|
114
|
+
end
|
115
|
+
|
116
|
+
def user_key_revoked(user, version, crl, index)
|
117
|
+
assert_user(user)
|
118
|
+
|
119
|
+
raise "Verison must be an int" unless version.kind_of?(Integer)
|
120
|
+
u = @db[:users][user]
|
121
|
+
u[:revoked] << version
|
122
|
+
u[:modified] = Time.now
|
123
|
+
@db.dump('index.txt', index, true)
|
124
|
+
@db.dump('crl.pem', crl, true)
|
125
|
+
@db.touched!
|
126
|
+
@db.sync
|
127
|
+
end
|
128
|
+
|
129
|
+
def revoked?(user, version)
|
130
|
+
assert_user(user)
|
131
|
+
|
132
|
+
@db[:users][user][:revoked].include?(version)
|
133
|
+
end
|
134
|
+
|
135
|
+
def active_key_version(user)
|
136
|
+
assert_user(user)
|
137
|
+
|
138
|
+
@db[:users][user][:active_key]
|
139
|
+
end
|
140
|
+
|
141
|
+
def user(user)
|
142
|
+
assert_user(user)
|
143
|
+
@db[:users][user]
|
144
|
+
end
|
145
|
+
|
146
|
+
def users; @db[:users]; end
|
147
|
+
|
148
|
+
def initialize(name, dir)
|
149
|
+
@db = KeyDB.new(File.join(dir, name + '.db.yaml'))
|
150
|
+
@config = KeyConfig.new(File.join(dir, name + '.config.yaml'))
|
151
|
+
@builder = KeyBuilder.new(self, @config)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.generate(name, path)
|
156
|
+
KeyTracker.generate(name, path)
|
157
|
+
end
|
158
|
+
end
|