ovpn-key 0.7.7 → 0.8.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13681f4cf8c6abc0badce3feceacf4ce489daab4ff9ea7b177bdca7c3fe983ed
4
- data.tar.gz: 621ad232db5032b90b1b631f7765dc15aca7b8131465bb2658408c13a7c1d8b0
3
+ metadata.gz: 0ae3146e987d293da8fbf2880e9696e94d7d51c06db2f63fd2c021d63158dea5
4
+ data.tar.gz: fbac8d69275b82527304e063db26bdea596186a7dee71acd98d9a3da282a6f59
5
5
  SHA512:
6
- metadata.gz: cc2d031bd9f8a595fa1efd862c2e5e371643928d34c7d7398db179466b655e48d412dd5494cfc6d705e614114846acbcce717b1117026df685bac5a4eb6e65b7
7
- data.tar.gz: d31fd3d8936ab9bbd94daed1db9f2334925e073dfb9319d6d96aabfa1db566bd2f340144dd0190ee27ee2652d64ef70d59e6c0a6a1c63af6392dfdba0191073e
6
+ metadata.gz: 2dfbae985471d80c40a7ea2ada9b8dfdd237b3973027c737640e782f0dc8503cc466ee5560d0f3f0456a20c2199fba2407a59202a4bf4c08291ec2a820b02333
7
+ data.tar.gz: 5451ceb967404146948d348bd63deb7d2bc2e73df9ed93c840c4cf160a1cb6048b923e48ad96b1c121efd64cd5386da6bdca20606dbfa9c69f5bd85b484710a8
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,18 +1,23 @@
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 = {}
18
+ # rubocop:disable Metrics/BlockLength
15
19
  OptionParser.new do |opts|
20
+ # rubocop:enable Metrics/BlockLength
16
21
  opts.banner = "Usage: #{File.basename $PROGRAM_NAME} <options> [--nopass]"
17
22
  opts.on('--init [directory]', 'Init a CA directory (defaults to current)') do |v|
18
23
  options[:init] = v || '.'
@@ -41,7 +46,7 @@ OptionParser.new do |opts|
41
46
  check_client(v)
42
47
  options[:generate_zip] = v
43
48
  end
44
- opts.on('--revoke [name]', 'Revoke a certificate (using crl.pem) and delete it') do |v|
49
+ opts.on('--revoke [name]', "Revoke a certificate (using #{CRL_FILE}) and delete it") do |v|
45
50
  abort 'Please specify what certificate to revoke' unless v
46
51
  options[:revoke] = v
47
52
  end
@@ -67,13 +72,10 @@ if options[:init]
67
72
  create_dir options[:init]
68
73
  Dir.chdir options[:init]
69
74
  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
- }
75
+ unless File.exist? APP_CONF
76
+ FileUtils.copy_file(File.expand_path("defaults/#{APP_CONF}", "#{__dir__}/.."), "./#{APP_CONF}")
77
+ puts "Created file: #{APP_CONF}"
78
+ end
77
79
  elsif !File.exist? APP_CONF
78
80
  begin
79
81
  rc = YAML.load_file(File.expand_path("~/.#{APP_CONF}"))
@@ -90,29 +92,78 @@ rescue Errno::ENOENT
90
92
  end
91
93
  ZIP_DIR = settings['zip_dir'] || '~'
92
94
  OPENVPN = settings['openvpn'] || 'openvpn'
93
- OPENSSL = settings['openssl'] || 'openssl'
94
95
  ENCRYPT = settings['encrypt'] || 'aes128'
96
+ DIGEST = settings['digest'] || 'sha256'
95
97
  KEY_SIZE = settings['key_size'] || 2048
96
- CA_DAYS = settings['ca_days'] || 3650
97
98
  CN_CA = settings['ca_name'] || 'Certification Authority'
98
- REQ = settings['details']
99
+
100
+ unless settings['ca_days'].nil?
101
+ if settings['expire'].nil?
102
+ puts 'Migrating pre-0.8 configuration to new format: ca_days'
103
+ puts "WARNING: if you tweaked `default_days` or `default_days_crl` in #{SSL_CONF}, edit #{APP_CONF}"
104
+ File.open(APP_CONF, 'a') do |f|
105
+ f.write "# ca_days is not used anymore, you can remove it\nexpire:\n"
106
+ f.write " ca: #{settings['ca_days']}\n crl: 3650\n server: 3650\n client: 3650\n"
107
+ end
108
+ else
109
+ puts "WARNING: `ca_days` setting is deprecated, remove it from #{APP_CONF}"
110
+ end
111
+ end
112
+
113
+ settings['expire'] ||= {}
114
+ settings['expire']['ca'] ||= settings['ca_days'] || 3650
115
+ settings['expire']['crl'] ||= 3650
116
+ settings['expire']['server'] ||= 3650
117
+ settings['expire']['client'] ||= 3650
118
+ EXPIRE = settings['expire']
119
+
120
+ if settings['x509'].nil? && !settings['details'].nil?
121
+ puts 'Migrating pre-0.8 configuration to new format: details'
122
+ REQ = OpenSSL::X509::Name.parse(settings['details']).to_a
123
+ File.open(APP_CONF, 'a') do |f|
124
+ f.write "# details is not used anymore, you can remove it\nx509:\n"
125
+ REQ.map {|i, j| f.write " #{i}: #{j}\n" }
126
+ end
127
+ else
128
+ REQ = settings['x509']
129
+ puts "WARNING: `details` section is deprecated, remove it from #{APP_CONF}" unless settings['details'].nil?
130
+ end
131
+ if settings['openssl']
132
+ puts "WARNING: `openssl` setting is deprecated, remove it from #{APP_CONF}"
133
+ end
134
+
135
+ if !File.exist?(SERIAL_FILE) && File.exist?('meta/serial')
136
+ FileUtils.copy_file('meta/serial', SERIAL_FILE)
137
+ puts 'Copied meta/serial to serial'
138
+ end
139
+ %w[certs meta].each {|dir|
140
+ puts "WARNING: #{dir} directory is not used anymore. you can remove it" if File.exist? dir
141
+ }
142
+ if File.exist? 'openssl.ini'
143
+ puts 'WARNING: openssl.ini file is not used anymore. you can remove it'
144
+ end
99
145
 
100
146
  if options[:generate_ca]
101
- gen_key('ca', options[:no_password])
102
- sign_key('ca', 'ca', CN_CA)
103
- gen_crl
147
+ ca_pass = options[:no_password] ? nil : ask_password('ca')
148
+ gen_key('ca', ca_pass)
149
+ sign_key('ca', CN_CA, ca_pass)
150
+ gen_crl(ca_pass)
104
151
  end
105
152
  if options[:generate_dh]
106
- exe "#{OPENSSL} dhparam -out dh.pem #{KEY_SIZE}"
153
+ File.open('dh.pem', 'w') do |f|
154
+ print 'Generating dh.pem. This will take a while'
155
+ f.write OpenSSL::PKey::DH.new(KEY_SIZE)
156
+ puts '. Done'
157
+ end
107
158
  end
108
159
  if options[:generate_static]
109
160
  exe "#{OPENVPN} --genkey --secret ta.key"
110
161
  end
111
162
  if options[:generate_server]
112
- gen_and_sign('server', options[:generate_server], options[:no_password])
163
+ gen_and_sign('server', options[:generate_server], options[:no_password] ? nil : ask_password(options[:generate_server]))
113
164
  end
114
165
  if options[:generate_client]
115
- gen_and_sign('client', options[:generate_client], options[:no_password])
166
+ gen_and_sign('client', options[:generate_client], options[:no_password] ? nil : ask_password(options[:generate_client]))
116
167
  end
117
168
  if options[:generate_zip]
118
169
  ovpn_files = Dir['*.ovpn']
@@ -125,14 +176,14 @@ if options[:generate_zip]
125
176
  abort 'More than one .ovpn files in current directory, aborting'
126
177
  end
127
178
 
128
- gen_and_sign('client', options[:generate_zip], options[:no_password])
179
+ gen_and_sign('client', options[:generate_zip], options[:no_password] ? nil : ask_password(options[:generate_zip]))
129
180
 
130
181
  zip_file = File.join(File.expand_path(ZIP_DIR), "#{File.basename ovpn_file, '.ovpn'}.tblk.zip")
131
182
  File.delete(zip_file) if File.exist?(zip_file)
132
183
  File.umask umask
133
184
  Zip::File.open(zip_file, Zip::File::CREATE) do |zip|
134
185
  zip.get_output_stream(ovpn_file) {|f|
135
- File.open(ovpn_file).each {|line| f.write line}
186
+ f.write File.read(ovpn_file)
136
187
  f.write "cert #{options[:generate_zip]}.crt\nkey #{options[:generate_zip]}.key\n"
137
188
  }
138
189
  ['ca.crt', "#{options[:generate_zip]}.crt", "#{options[:generate_zip]}.key"].each {|i|
@@ -142,7 +193,5 @@ if options[:generate_zip]
142
193
  end
143
194
  end
144
195
  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}"}
196
+ revoke(options[:revoke])
148
197
  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,143 @@ 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
21
27
  end
22
28
 
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}"
29
+ def unencrypt_ca_key(pass = '')
30
+ begin
31
+ OpenSSL::PKey::RSA.new File.read('ca.key'), pass
32
+ rescue OpenSSL::PKey::RSAError
33
+ # this means pass is wrong, so ask for it
34
+ OpenSSL::PKey::RSA.new File.read('ca.key'), ask_password('ca')
28
35
  end
36
+ rescue OpenSSL::PKey::RSAError
37
+ retry
38
+ end
39
+
40
+ def gen_and_sign(type, certname, password)
41
+ gen_key(certname, password)
42
+ sign_key(type, certname, password)
29
43
  end
30
44
 
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"
45
+ def gen_key(certname, password)
46
+ key = OpenSSL::PKey::RSA.new(KEY_SIZE)
47
+ File.open("#{certname}.key", 'w') do |f|
48
+ f.write password ? key.to_pem(OpenSSL::Cipher.new(ENCRYPT), password) : key
38
49
  end
39
50
  end
40
51
 
41
- def gen_crl
42
- exe "#{OPENSSL} ca -gencrl -out crl.pem -config #{SSL_CONF}"
52
+ # type is one of: 'ca', 'server', 'client'
53
+ def sign_key(type, cn, password)
54
+ certname = type == 'ca' ? 'ca' : cn
55
+ key = OpenSSL::PKey::RSA.new File.read("#{certname}.key"), password
56
+ serial = new_serial
57
+ cert = gen_cert(type, cn, key.public_key, serial)
58
+
59
+ ca_key = type == 'ca' ? key : unencrypt_ca_key
60
+ cert.sign ca_key, OpenSSL::Digest.new(DIGEST)
61
+
62
+ File.open(SERIAL_FILE, 'w') {|f| f.write serial }
63
+ File.open("#{certname}.crt", 'w') {|f| f.write cert.to_pem }
64
+ end
65
+
66
+ def gen_cert(type, cn, pubkey, serial)
67
+ cert = basic_cert(type, cn)
68
+ cert.public_key = pubkey
69
+ cert.serial = serial
70
+
71
+ customize_cert(type, cert)
72
+ end
73
+
74
+ def basic_cert(type, cn)
75
+ cert = OpenSSL::X509::Certificate.new
76
+
77
+ cert.version = 2
78
+ cert.subject = OpenSSL::X509::Name.new([['CN', cn]] + REQ.to_a)
79
+ cert.issuer = OpenSSL::X509::Name.new([['CN', CN_CA]] + REQ.to_a)
80
+ cert.not_before = Time.now
81
+ cert.not_after = time_after_days(EXPIRE[type])
82
+
83
+ cert
84
+ end
85
+
86
+ def time_after_days(days)
87
+ Time.now + days * 86_400 # days to seconds
88
+ end
89
+
90
+ # rubocop:disable Metrics/MethodLength
91
+ # rubocop:disable Metrics/AbcSize
92
+ def customize_cert(type, cert)
93
+ # rubocop:enable Metrics/AbcSize
94
+ # rubocop:enable Metrics/MethodLength
95
+
96
+ ef = OpenSSL::X509::ExtensionFactory.new nil, cert
97
+ ef.issuer_certificate = cert
98
+
99
+ cert.add_extension ef.create_extension('subjectKeyIdentifier', 'hash')
100
+ cert.add_extension ef.create_extension('authorityKeyIdentifier', 'keyid,issuer:always')
101
+ cert.add_extension ef.create_extension('basicConstraints', type == 'ca' ? 'CA:true' : 'CA:false')
102
+
103
+ case type
104
+ when 'ca'
105
+ cert.add_extension ef.create_extension('keyUsage', 'cRLSign,keyCertSign')
106
+ when 'server'
107
+ cert.add_extension ef.create_extension('keyUsage', 'keyEncipherment,digitalSignature')
108
+ cert.add_extension ef.create_extension('extendedKeyUsage', 'serverAuth')
109
+ when 'client'
110
+ cert.add_extension ef.create_extension('keyUsage', 'digitalSignature')
111
+ cert.add_extension ef.create_extension('extendedKeyUsage', 'clientAuth')
112
+ end
113
+
114
+ cert
115
+ end
116
+
117
+ # rubocop:disable Metrics/AbcSize
118
+ def revoke(certname)
119
+ # rubocop:enable Metrics/AbcSize
120
+ crl = OpenSSL::X509::CRL.new(File.read(CRL_FILE))
121
+ cert = OpenSSL::X509::Certificate.new(File.read("#{certname}.crt"))
122
+ revoke = OpenSSL::X509::Revoked.new.tap {|rev|
123
+ rev.serial = cert.serial
124
+ rev.time = Time.now
125
+ }
126
+ crl.next_update = time_after_days(EXPIRE['crl'])
127
+ crl.add_revoked(revoke)
128
+ update_crl(crl, '')
129
+ %w[crt key].each {|ext| File.delete "#{certname}.#{ext}" }
130
+ end
131
+
132
+ def gen_crl(ca_pass)
133
+ return if File.exist? CRL_FILE
134
+
135
+ crl = OpenSSL::X509::CRL.new
136
+ crl.issuer = OpenSSL::X509::Name.new([['CN', CN_CA]] + REQ.to_a)
137
+ update_crl(crl, ca_pass)
138
+ end
139
+
140
+ def update_crl(crl, ca_pass)
141
+ ca_key = unencrypt_ca_key(ca_pass)
142
+ crl.last_update = Time.now
143
+ crl.next_update = time_after_days(EXPIRE['crl'])
144
+ crl.sign(ca_key, OpenSSL::Digest.new(DIGEST))
145
+ File.open(CRL_FILE, 'w') {|f| f.write crl.to_pem }
146
+ end
147
+
148
+ def new_serial
149
+ File.read(SERIAL_FILE).to_i + 1
150
+ rescue Errno::ENOENT
151
+ 0
43
152
  end
44
153
 
45
154
  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.4'
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.4
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