#! /usr/bin/env ruby -w # frozen_string_literal: true require 'fileutils' require 'io/console' require 'openssl' require 'optparse' require 'yaml' require 'zip' require_relative '../lib/version' require_relative '../lib/functions' APP_CONF = 'ovpn-key.yml' CRL_FILE = 'crl.pem' SERIAL_FILE = 'serial' options = {} # rubocop:disable Metrics/BlockLength OptionParser.new do |opts| # rubocop:enable Metrics/BlockLength opts.banner = "Usage: #{File.basename $PROGRAM_NAME} [--nopass]" opts.on('--init [directory]', 'Init a CA directory (defaults to current)') do |v| options[:init] = v || '.' end opts.on('--ca', 'Generate a CA (ca.crt)') do |v| check_crt('ca') options[:generate_ca] = v end opts.on('--dh', 'Generate a DH keyfile (dh.pem)') do |v| # it's safe to overwrite this file options[:generate_dh] = v end opts.on('--static', 'Generate OpenVPN static key (ta.key)') do |v| options[:generate_static] = v check_crt('ta') end opts.on('--server [name]', "Generate a server key (defaults to 'server')") do |v| options[:generate_server] = v || 'server' check_crt(options[:generate_server]) end opts.on('--client [name]', 'Generate a client key and sign it') do |v| check_client(v) options[:generate_client] = v end opts.on('--zip [name]', 'Ditto plus pack it to ZIP with OpenVPN config') do |v| check_client(v) options[:generate_zip] = v end opts.on('--revoke [name]', "Revoke a certificate (using #{CRL_FILE}) and delete it") do |v| abort 'Please specify what certificate to revoke' unless v options[:revoke] = v end opts.on('--nopass', "Don't protect .key files with a password") do |v| options[:no_password] = v end end.parse! if ARGV.length.positive? abort "Error: invalid args: #{ARGV.join ' '}\nSee `#{File.basename $PROGRAM_NAME} -h` for help" end unless options[:init] || options[:generate_ca] || options[:generate_dh] || options[:generate_static] \ || options[:generate_server] || options[:generate_client] || options[:generate_zip] || options[:revoke] abort "See `#{File.basename $PROGRAM_NAME} -h` for usage" end if options[:generate_client] && options[:generate_zip] # I assume that user likely wants one of them and is confused with usage abort 'There can be only one: --client or --zip' end if options[:init] unless options[:init] == '.' create_dir options[:init] Dir.chdir options[:init] end unless File.exist? APP_CONF FileUtils.copy_file(File.expand_path("defaults/#{APP_CONF}", "#{__dir__}/.."), "./#{APP_CONF}") puts "Created file: #{APP_CONF}" end elsif !File.exist? APP_CONF begin rc = YAML.load_file(File.expand_path("~/.#{APP_CONF}")) rescue Errno::ENOENT # no configuration file in home directory is not an error end Dir.chdir File.expand_path(rc['cd']) if rc && rc['cd'] end begin settings = YAML.load_file(APP_CONF) rescue Errno::ENOENT abort "Run `#{File.basename $PROGRAM_NAME} --init` before generating certificates" end ZIP_DIR = settings['zip_dir'] || '~' OPENVPN = settings['openvpn'] || 'openvpn' ENCRYPT = settings['encrypt'] || 'aes128' DIGEST = settings['digest'] || 'sha256' KEY_SIZE = settings['key_size'] || 2048 CN_CA = settings['ca_name'] || 'Certification Authority' unless settings['ca_days'].nil? if settings['expire'].nil? puts 'Migrating pre-0.8 configuration to new format: ca_days' puts "WARNING: if you tweaked `default_days` or `default_days_crl` in #{SSL_CONF}, edit #{APP_CONF}" File.open(APP_CONF, 'a') do |f| f.write "# ca_days is not used anymore, you can remove it\nexpire:\n" f.write " ca: #{settings['ca_days']}\n crl: 3650\n server: 3650\n client: 3650\n" end else puts "WARNING: `ca_days` setting is deprecated, remove it from #{APP_CONF}" end end settings['expire'] ||= {} settings['expire']['ca'] ||= settings['ca_days'] || 3650 settings['expire']['crl'] ||= 3650 settings['expire']['server'] ||= 3650 settings['expire']['client'] ||= 3650 EXPIRE = settings['expire'] if settings['x509'].nil? && !settings['details'].nil? puts 'Migrating pre-0.8 configuration to new format: details' REQ = OpenSSL::X509::Name.parse(settings['details']).to_a File.open(APP_CONF, 'a') do |f| f.write "# details is not used anymore, you can remove it\nx509:\n" REQ.map {|i, j| f.write " #{i}: #{j}\n" } end else REQ = settings['x509'] puts "WARNING: `details` section is deprecated, remove it from #{APP_CONF}" unless settings['details'].nil? end if settings['openssl'] puts "WARNING: `openssl` setting is deprecated, remove it from #{APP_CONF}" end if !File.exist?(SERIAL_FILE) && File.exist?('meta/serial') FileUtils.copy_file('meta/serial', SERIAL_FILE) puts 'Copied meta/serial to serial' end %w[certs meta].each {|dir| puts "WARNING: #{dir} directory is not used anymore. you can remove it" if File.exist? dir } if File.exist? 'openssl.ini' puts 'WARNING: openssl.ini file is not used anymore. you can remove it' end if options[:generate_ca] ca_pass = options[:no_password] ? nil : ask_password('ca') gen_key('ca', ca_pass) sign_key('ca', CN_CA, ca_pass) gen_crl(ca_pass) end if options[:generate_dh] File.open('dh.pem', 'w') do |f| print 'Generating dh.pem. This will take a while' f.write OpenSSL::PKey::DH.new(KEY_SIZE) puts '. Done' end end if options[:generate_static] exe "#{OPENVPN} --genkey --secret ta.key" end if options[:generate_server] gen_and_sign('server', options[:generate_server], options[:no_password] ? nil : ask_password(options[:generate_server])) end if options[:generate_client] gen_and_sign('client', options[:generate_client], options[:no_password] ? nil : ask_password(options[:generate_client])) end if options[:generate_zip] ovpn_files = Dir['*.ovpn'] case ovpn_files.length when 1 ovpn_file = ovpn_files.first when 0 abort 'No .ovpn file in current directory, please add one' else abort 'More than one .ovpn files in current directory, aborting' end gen_and_sign('client', options[:generate_zip], options[:no_password] ? nil : ask_password(options[:generate_zip])) zip_file = File.join(File.expand_path(ZIP_DIR), "#{File.basename ovpn_file, '.ovpn'}.tblk.zip") File.delete(zip_file) if File.exist?(zip_file) Zip::File.open(zip_file, Zip::File::CREATE) do |zip| zip.get_output_stream(ovpn_file) {|f| f.write File.read(ovpn_file) f.write "cert #{options[:generate_zip]}.crt\nkey #{options[:generate_zip]}.key\n" } ['ca.crt', "#{options[:generate_zip]}.crt", "#{options[:generate_zip]}.key"].each {|i| zip.add(i, i) } zip.add('ta.key', 'ta.key') if File.exist? 'ta.key' end end if options[:revoke] revoke(options[:revoke]) end