ovpn-key 0.7.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13681f4cf8c6abc0badce3feceacf4ce489daab4ff9ea7b177bdca7c3fe983ed
4
- data.tar.gz: 621ad232db5032b90b1b631f7765dc15aca7b8131465bb2658408c13a7c1d8b0
3
+ metadata.gz: ecf480dc290e372a3b081ebc12dceb82e2f0ca22ea508cbbe622e8e1ce80b58f
4
+ data.tar.gz: 585a28ba43b652afd0add8b912a8ff0590ad99ffcb96abcb9929c8ca488c03b1
5
5
  SHA512:
6
- metadata.gz: cc2d031bd9f8a595fa1efd862c2e5e371643928d34c7d7398db179466b655e48d412dd5494cfc6d705e614114846acbcce717b1117026df685bac5a4eb6e65b7
7
- data.tar.gz: d31fd3d8936ab9bbd94daed1db9f2334925e073dfb9319d6d96aabfa1db566bd2f340144dd0190ee27ee2652d64ef70d59e6c0a6a1c63af6392dfdba0191073e
6
+ metadata.gz: 47783d3b02d6948cb19fc4434dff11a980c0fc9f22675a66d2cc6fc34423be7e7b2dc6b4e46f420eaa85755ef14ca8e787dddaa30eacd707837033138239753f
7
+ data.tar.gz: 405a4389cf9b26a8a8cf6e4e615a04dc2bcf80b6fe71ed7b22c3380c3e5d215e70bab5d3ef8ba78eab20e94e19e74d1dd3c58fe47e118760cae842fbfc1e7c40
data/NOTICE CHANGED
@@ -1,6 +1,6 @@
1
1
  ovpn-key: https://github.com/chillum/ovpn-key
2
2
 
3
- Copyright 2018 Vasily Korytov
3
+ Copyright 2018-2021 Vasily Korytov
4
4
 
5
5
  Licensed under the Apache License, Version 2.0 (the "License");
6
6
  you may not use this software except in compliance with the License.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This utility is designed as [easy-rsa](https://github.com/OpenVPN/easy-rsa) replacement suitable for one exact use case.
4
4
 
5
- It's basically a wrapper around `openssl` to:
5
+ It's basically a wrapper around OpenSSL API to:
6
6
  * create a self-signed CA
7
7
  * create client and server certificates and pack them to ZIP files along with the OpenVPN config
8
8
  * revoke the certificates
@@ -28,7 +28,7 @@ If you're brave, [let me know](https://github.com/chillum/ovpn-key/issues), wher
28
28
  ### Usage
29
29
 
30
30
  1. `ovpn-key --init`
31
- 2. edit `ovpn-key.yml` and `openssl.ini`
31
+ 2. edit `ovpn-key.yml`
32
32
  3. `ovpn-key --ca --dh`
33
33
  4. `ovpn-key --server --nopass`
34
34
  5. `ovpn-key --client somebody [--nopass]`
data/bin/ovpn-key CHANGED
@@ -1,15 +1,18 @@
1
- #! /usr/bin/env ruby
1
+ #! /usr/bin/env ruby -w
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'optparse'
5
4
  require 'fileutils'
5
+ require 'io/console'
6
+ require 'openssl'
7
+ require 'optparse'
6
8
  require 'yaml'
7
9
  require 'zip'
8
10
  require_relative '../lib/version'
9
11
  require_relative '../lib/functions'
10
12
 
11
- SSL_CONF = 'openssl.ini'
12
13
  APP_CONF = 'ovpn-key.yml'
14
+ CRL_FILE = 'crl.pem'
15
+ SERIAL_FILE = 'serial'
13
16
 
14
17
  options = {}
15
18
  OptionParser.new do |opts|
@@ -41,7 +44,7 @@ OptionParser.new do |opts|
41
44
  check_client(v)
42
45
  options[:generate_zip] = v
43
46
  end
44
- opts.on('--revoke [name]', 'Revoke a certificate (using crl.pem) and delete it') do |v|
47
+ opts.on("--revoke [name]', 'Revoke a certificate (using #{CRL_FILE}) and delete it") do |v|
45
48
  abort 'Please specify what certificate to revoke' unless v
46
49
  options[:revoke] = v
47
50
  end
@@ -67,13 +70,10 @@ if options[:init]
67
70
  create_dir options[:init]
68
71
  Dir.chdir options[:init]
69
72
  end
70
- %w[certs meta].each {|dir| create_dir dir}
71
- ['meta/index.txt', 'meta/index.txt.attr', 'meta/serial', SSL_CONF, APP_CONF].each {|file|
72
- unless File.exist? file
73
- FileUtils.copy_file(File.expand_path("defaults/#{file}", "#{__dir__}/.."), "./#{file}")
74
- puts "Created file: #{file}"
75
- end
76
- }
73
+ unless File.exist? APP_CONF
74
+ FileUtils.copy_file(File.expand_path("defaults/#{APP_CONF}", "#{__dir__}/.."), "./#{APP_CONF}")
75
+ puts "Created file: #{APP_CONF}"
76
+ end
77
77
  elsif !File.exist? APP_CONF
78
78
  begin
79
79
  rc = YAML.load_file(File.expand_path("~/.#{APP_CONF}"))
@@ -90,29 +90,78 @@ rescue Errno::ENOENT
90
90
  end
91
91
  ZIP_DIR = settings['zip_dir'] || '~'
92
92
  OPENVPN = settings['openvpn'] || 'openvpn'
93
- OPENSSL = settings['openssl'] || 'openssl'
94
93
  ENCRYPT = settings['encrypt'] || 'aes128'
94
+ DIGEST = settings['digest'] || 'sha256'
95
95
  KEY_SIZE = settings['key_size'] || 2048
96
- CA_DAYS = settings['ca_days'] || 3650
97
96
  CN_CA = settings['ca_name'] || 'Certification Authority'
98
- REQ = settings['details']
97
+
98
+ unless settings['ca_days'].nil?
99
+ if settings['expire'].nil?
100
+ puts 'Migrating pre-0.8 configuration to new format: ca_days'
101
+ puts "WARNING: if you tweaked `default_days` or `default_days_crl` in #{SSL_CONF}, edit #{APP_CONF}"
102
+ File.open(APP_CONF, 'a') do |f|
103
+ f.write "# ca_days is not used anymore, you can remove it\nexpire:\n"
104
+ f.write " ca: #{settings['ca_days']}\n crl: 3650\n server: 3650\n client: 3650\n"
105
+ end
106
+ else
107
+ puts "WARNING: `ca_days` setting is deprecated, remove it from #{APP_CONF}"
108
+ end
109
+ end
110
+
111
+ settings['expire'] ||= {}
112
+ settings['expire']['ca'] ||= settings['ca_days'] || 3650
113
+ settings['expire']['crl'] ||= 3650
114
+ settings['expire']['server'] ||= 3650
115
+ settings['expire']['client'] ||= 3650
116
+ EXPIRE = settings['expire']
117
+
118
+ if settings['x509'].nil? && !settings['details'].nil?
119
+ puts 'Migrating pre-0.8 configuration to new format: details'
120
+ REQ = OpenSSL::X509::Name.parse(settings['details']).to_a
121
+ File.open(APP_CONF, 'a') do |f|
122
+ f.write "# details is not used anymore, you can remove it\nx509:\n"
123
+ REQ.map {|i, j| f.write " #{i}: #{j}\n" }
124
+ end
125
+ else
126
+ REQ = settings['x509']
127
+ puts "WARNING: `details` section is deprecated, remove it from #{APP_CONF}" unless settings['details'].nil?
128
+ end
129
+ if settings['openssl']
130
+ puts "WARNING: `openssl` setting is deprecated, remove it from #{APP_CONF}"
131
+ end
132
+
133
+ if !File.exist?(SERIAL_FILE) && File.exist?('meta/serial')
134
+ FileUtils.copy_file('meta/serial', SERIAL_FILE)
135
+ puts 'Copied meta/serial to serial'
136
+ end
137
+ %w[certs meta].each {|dir|
138
+ puts "WARNING: #{dir} directory is not used anymore. you can remove it" if File.exist? dir
139
+ }
140
+ if File.exist? 'openssl.ini'
141
+ puts 'WARNING: openssl.ini file is not used anymore. you can remove it'
142
+ end
99
143
 
100
144
  if options[:generate_ca]
101
- gen_key('ca', options[:no_password])
102
- sign_key('ca', 'ca', CN_CA)
103
- gen_crl
145
+ ca_pass = options[:no_password] ? nil : ask_password('ca')
146
+ gen_key('ca', ca_pass)
147
+ sign_key('ca', CN_CA, ca_pass)
148
+ gen_crl(ca_pass)
104
149
  end
105
150
  if options[:generate_dh]
106
- exe "#{OPENSSL} dhparam -out dh.pem #{KEY_SIZE}"
151
+ File.open('dh.pem', 'w') do |f|
152
+ print 'Generating dh.pem. This will take a while'
153
+ f.write OpenSSL::PKey::DH.new(KEY_SIZE)
154
+ puts '. Done'
155
+ end
107
156
  end
108
157
  if options[:generate_static]
109
158
  exe "#{OPENVPN} --genkey --secret ta.key"
110
159
  end
111
160
  if options[:generate_server]
112
- gen_and_sign('server', options[:generate_server], options[:no_password])
161
+ gen_and_sign('server', options[:generate_server], options[:no_password] ? nil : ask_password(options[:generate_server]))
113
162
  end
114
163
  if options[:generate_client]
115
- gen_and_sign('client', options[:generate_client], options[:no_password])
164
+ gen_and_sign('client', options[:generate_client], options[:no_password] ? nil : ask_password(options[:generate_client]))
116
165
  end
117
166
  if options[:generate_zip]
118
167
  ovpn_files = Dir['*.ovpn']
@@ -125,14 +174,14 @@ if options[:generate_zip]
125
174
  abort 'More than one .ovpn files in current directory, aborting'
126
175
  end
127
176
 
128
- gen_and_sign('client', options[:generate_zip], options[:no_password])
177
+ gen_and_sign('client', options[:generate_zip], options[:no_password] ? nil : ask_password(options[:generate_zip]))
129
178
 
130
179
  zip_file = File.join(File.expand_path(ZIP_DIR), "#{File.basename ovpn_file, '.ovpn'}.tblk.zip")
131
180
  File.delete(zip_file) if File.exist?(zip_file)
132
181
  File.umask umask
133
182
  Zip::File.open(zip_file, Zip::File::CREATE) do |zip|
134
183
  zip.get_output_stream(ovpn_file) {|f|
135
- File.open(ovpn_file).each {|line| f.write line}
184
+ f.write File.read(ovpn_file)
136
185
  f.write "cert #{options[:generate_zip]}.crt\nkey #{options[:generate_zip]}.key\n"
137
186
  }
138
187
  ['ca.crt', "#{options[:generate_zip]}.crt", "#{options[:generate_zip]}.key"].each {|i|
@@ -142,7 +191,5 @@ if options[:generate_zip]
142
191
  end
143
192
  end
144
193
  if options[:revoke]
145
- exe "#{OPENSSL} ca -revoke '#{options[:revoke]}.crt' -config #{SSL_CONF}"
146
- gen_crl
147
- %w[crt key].each {|ext| File.delete "#{options[:revoke]}.#{ext}"}
194
+ revoke(options[:revoke])
148
195
  end
@@ -1,8 +1,17 @@
1
1
  zip_dir: '~'
2
2
  openvpn: openvpn
3
- openssl: openssl
4
- encrypt: aes128
3
+ encrypt: AES128
4
+ digest: SHA256
5
5
  key_size: 2048
6
- ca_days: 3650
6
+ expire: # days
7
+ ca: 3650
8
+ crl: 3650
9
+ server: 3650
10
+ client: 3650
7
11
  ca_name: Certification Authority
8
- details: /C=US/ST=CA/L=San Francisco/O=Dva Debila/OU=OpenVPN
12
+ x509:
13
+ C: US
14
+ ST: CA
15
+ L: San Francisco
16
+ O: Dva Debila
17
+ OU: OpenVPN
data/lib/functions.rb CHANGED
@@ -12,34 +12,128 @@ def check_client(name)
12
12
  end
13
13
 
14
14
  def exe(cmd)
15
- system(cmd) or abort "error executing: #{cmd}"
15
+ system(cmd) || abort("error executing: #{cmd}")
16
16
  end
17
17
 
18
- def gen_and_sign(type, certname, no_password)
19
- gen_key(certname, no_password)
20
- sign_key(type, certname, certname)
18
+ def ask_password(name)
19
+ password = ''
20
+ loop do
21
+ print "Enter password for #{name}.key: "
22
+ password = $stdin.noecho(&:gets).chomp
23
+ puts # trailing newline
24
+ break unless password.empty?
25
+ end
26
+ password
27
+ end
28
+
29
+ def gen_and_sign(type, certname, password)
30
+ gen_key(certname, password)
31
+ sign_key(type, certname, password)
21
32
  end
22
33
 
23
- def gen_key(certname, no_password)
24
- if no_password
25
- exe "#{OPENSSL} genrsa -out '#{certname}.key' #{KEY_SIZE}"
26
- else
27
- exe "#{OPENSSL} genrsa -#{ENCRYPT} -out '#{certname}.key' #{KEY_SIZE}"
34
+ def gen_key(certname, password)
35
+ key = OpenSSL::PKey::RSA.new(KEY_SIZE)
36
+ File.open("#{certname}.key", 'w') do |f|
37
+ f.write password ? key.to_pem(OpenSSL::Cipher.new(ENCRYPT), password) : key
28
38
  end
29
39
  end
30
40
 
31
- def sign_key(type, certname, cn)
32
- if certname == 'ca'
33
- exe "#{OPENSSL} req -new -x509 -key '#{certname}.key' -out '#{certname}.crt' -config #{SSL_CONF} -subj '/CN=#{cn}#{REQ}' -extensions ext.#{type} -days #{CA_DAYS}"
34
- else
35
- exe "#{OPENSSL} req -new -key '#{certname}.key' -out '#{certname}.csr' -config #{SSL_CONF} -subj '/CN=#{cn}#{REQ}' -extensions ext.#{type}"
36
- exe "#{OPENSSL} ca -in '#{certname}.csr' -out '#{certname}.crt' -config #{SSL_CONF} -extensions ext.#{type} -batch"
37
- File.delete "#{certname}.csr"
41
+ # type is one of: 'ca', 'server', 'client'
42
+ # rubocop:disable Naming/MethodParameterName
43
+ def sign_key(type, cn, password)
44
+ # rubocop:enable Naming/MethodParameterName
45
+ certname = type == 'ca' ? 'ca' : cn
46
+ key = OpenSSL::PKey::RSA.new File.read("#{certname}.key"), password
47
+ subj = OpenSSL::X509::Name.new([['CN', cn]] + REQ.to_a)
48
+ serial = begin
49
+ File.read(SERIAL_FILE).to_i
50
+ rescue Errno::ENOENT
51
+ 0
52
+ end + 1
53
+
54
+ cert = OpenSSL::X509::Certificate.new
55
+ cert.version = 2
56
+ cert.serial = serial
57
+ cert.not_before = Time.now
58
+ cert.not_after =
59
+ Time.now +
60
+ case type
61
+ when 'ca'
62
+ EXPIRE['ca']
63
+ when 'server'
64
+ EXPIRE['server']
65
+ when 'client'
66
+ EXPIRE['client']
67
+ # days to seconds
68
+ end * 86_400
69
+ cert.public_key = key.public_key
70
+ cert.subject = subj
71
+ cert.issuer = OpenSSL::X509::Name.new([['CN', CN_CA]] + REQ.to_a)
72
+
73
+ ef = OpenSSL::X509::ExtensionFactory.new nil, cert
74
+ ef.issuer_certificate = cert
75
+
76
+ cert.add_extension ef.create_extension('subjectKeyIdentifier', 'hash')
77
+ cert.add_extension ef.create_extension('authorityKeyIdentifier', 'keyid,issuer:always')
78
+ cert.add_extension ef.create_extension('basicConstraints', type == 'ca' ? 'CA:true' : 'CA:false')
79
+
80
+ case type
81
+ when 'ca'
82
+ cert.add_extension ef.create_extension('keyUsage', 'cRLSign,keyCertSign')
83
+ cert.sign key, OpenSSL::Digest.new(DIGEST)
84
+ when 'server'
85
+ cert.add_extension ef.create_extension('keyUsage', 'keyEncipherment,digitalSignature')
86
+ cert.add_extension ef.create_extension('extendedKeyUsage', 'serverAuth')
87
+ when 'client'
88
+ cert.add_extension ef.create_extension('keyUsage', 'digitalSignature')
89
+ cert.add_extension ef.create_extension('extendedKeyUsage', 'clientAuth')
38
90
  end
91
+ unless type == 'ca'
92
+ ca_key = begin
93
+ OpenSSL::PKey::RSA.new File.read('ca.key'), ask_password('ca')
94
+ rescue OpenSSL::PKey::RSAError
95
+ retry
96
+ end
97
+ cert.sign ca_key, OpenSSL::Digest.new(DIGEST)
98
+ end
99
+
100
+ File.open(SERIAL_FILE, 'w') {|f| f.write serial }
101
+ File.open("#{certname}.crt", 'w') {|f| f.write cert.to_pem }
102
+ end
103
+
104
+ def revoke(certname)
105
+ crl = OpenSSL::X509::CRL.new(File.read(CRL_FILE))
106
+ cert = OpenSSL::X509::Certificate.new(File.read("#{certname}.crt"))
107
+ revoke = OpenSSL::X509::Revoked.new.tap {|rev|
108
+ rev.serial = cert.serial
109
+ rev.time = Time.now
110
+ }
111
+ crl.next_update = Time.now + EXPIRE['crl'] * 86_400 # days to seconds
112
+ crl.add_revoked(revoke)
113
+ begin
114
+ update_crl(crl, ask_password('ca'))
115
+ rescue OpenSSL::PKey::RSAError
116
+ retry
117
+ end
118
+
119
+ %w[crt key].each {|ext| File.delete "#{certname}.#{ext}" }
120
+ end
121
+
122
+ def gen_crl(ca_pass)
123
+ return if File.exist? CRL_FILE
124
+
125
+ crl = OpenSSL::X509::CRL.new
126
+ crl.issuer = OpenSSL::X509::Name.new([['CN', CN_CA]] + REQ.to_a)
127
+ update_crl(crl, ca_pass)
39
128
  end
40
129
 
41
- def gen_crl
42
- exe "#{OPENSSL} ca -gencrl -out crl.pem -config #{SSL_CONF}"
130
+ def update_crl(crl, ca_pass)
131
+ ca_key = OpenSSL::PKey::RSA.new File.read('ca.key'), ca_pass
132
+ crl.last_update = Time.now
133
+ crl.next_update = Time.now + EXPIRE['crl'] * 86_400 # days to seconds
134
+ crl.version = crl.version + 1
135
+ crl.sign(ca_key, OpenSSL::Digest.new(DIGEST))
136
+ File.open(CRL_FILE, 'w') {|f| f.write crl.to_pem }
43
137
  end
44
138
 
45
139
  def create_dir(name)
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ::VERSION = '0.7.7'
3
+ ::VERSION = '0.8'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ovpn-key
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.7
4
+ version: '0.8'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vasily Korytov
@@ -35,10 +35,6 @@ files:
35
35
  - NOTICE
36
36
  - README.md
37
37
  - bin/ovpn-key
38
- - defaults/meta/index.txt
39
- - defaults/meta/index.txt.attr
40
- - defaults/meta/serial
41
- - defaults/openssl.ini
42
38
  - defaults/ovpn-key.yml
43
39
  - lib/functions.rb
44
40
  - lib/version.rb
File without changes
@@ -1 +0,0 @@
1
- unique_subject = yes
data/defaults/meta/serial DELETED
@@ -1 +0,0 @@
1
- 00
data/defaults/openssl.ini DELETED
@@ -1,48 +0,0 @@
1
- [req]
2
- default_md = sha256
3
- distinguished_name = dn.ovpn
4
-
5
- [dn.ovpn]
6
- CN = Certificate name (required)
7
-
8
- [ca]
9
- default_ca = ca.ovpn
10
-
11
- [ca.ovpn]
12
- default_md = sha256
13
- private_key = ca.key
14
- certificate = ca.crt
15
- database = meta/index.txt
16
- serial = meta/serial
17
- crl = crl.pem
18
- policy = policy.ovpn
19
- # create this directory if changing this value
20
- new_certs_dir = certs
21
- default_days = 3650
22
- default_crl_days = 3650
23
- subjectKeyIdentifier = hash
24
- authorityKeyIdentifier = keyid,issuer:always
25
-
26
- [ext.ca]
27
- basicConstraints = CA:true
28
-
29
- [ext.server]
30
- basicConstraints = CA:false
31
- nsCertType = server
32
- extendedKeyUsage = serverAuth
33
- keyUsage = digitalSignature, keyEncipherment
34
-
35
- [ext.client]
36
- basicConstraints = CA:false
37
- extendedKeyUsage = clientAuth
38
- keyUsage = digitalSignature
39
-
40
- [policy.ovpn]
41
- commonName = supplied
42
- countryName = optional
43
- stateOrProvinceName = optional
44
- localityName = optional
45
- organizationName = optional
46
- organizationalUnitName = optional
47
- name = optional
48
- emailAddress = optional