diff options
-rwxr-xr-x | bin/leap | 5 | ||||
-rw-r--r-- | leap_cli.gemspec | 8 | ||||
-rw-r--r-- | lib/leap_cli/commands/ca.rb | 162 | ||||
-rw-r--r-- | lib/leap_cli/commands/node.rb | 2 | ||||
-rw-r--r-- | lib/leap_cli/commands/project.rb | 2 | ||||
-rw-r--r-- | lib/leap_cli/config/manager.rb | 23 | ||||
-rw-r--r-- | lib/leap_cli/config/object.rb | 6 | ||||
-rw-r--r-- | lib/leap_cli/path.rb | 14 | ||||
-rw-r--r-- | lib/leap_cli/util.rb | 74 | ||||
-rw-r--r-- | lib/leap_cli/version.rb | 2 | ||||
-rw-r--r-- | test/provider/common.json | 5 | ||||
-rw-r--r-- | test/provider/provider.json | 9 | ||||
-rw-r--r-- | test/provider/services/openvpn.json | 5 |
13 files changed, 280 insertions, 37 deletions
@@ -46,6 +46,7 @@ module LeapCli::Commands def_delegator :@terminal, :agree, 'self.agree' def_delegator :@terminal, :choose, 'self.choose' def_delegator :@terminal, :say, 'self.say' + def_delegator :@terminal, :color, 'self.color' # # make config manager available as 'manager' @@ -61,8 +62,8 @@ module LeapCli::Commands # # info about leap command line suite # - program_desc 'LEAP platform command line interface' - program_long_desc 'This is the long description. It is very interesting.' + program_desc LeapCli::SUMMARY + program_long_desc LeapCli::DESCRIPTION version LeapCli::VERSION # diff --git a/leap_cli.gemspec b/leap_cli.gemspec index ecabe45..20e50a8 100644 --- a/leap_cli.gemspec +++ b/leap_cli.gemspec @@ -16,6 +16,7 @@ spec = Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.summary = LeapCli::SUMMARY s.description = LeapCli::DESCRIPTION + s.license = "GPLv3" ## ## GEM FILES @@ -48,13 +49,16 @@ spec = Gem::Specification.new do |s| s.add_runtime_dependency('highline') # network gems - s.add_runtime_dependency('net-ssh') s.add_runtime_dependency('capistrano') #s.add_runtime_dependency('supply_drop') + # crypto gems + s.add_runtime_dependency('certificate_authority') # this gem pulls in ActiveModel, but it just uses it for validation logic. + s.add_runtime_dependency('net-ssh') + s.add_runtime_dependency('gpgme') # not essential, but used for some minor stuff in adding sysadmins + # misc gems s.add_runtime_dependency('ya2yaml') # pure ruby yaml, so we can better control output. see https://github.com/afunai/ya2yaml s.add_runtime_dependency('json_pure') # pure ruby json, so we can better control output. - s.add_runtime_dependency('gpgme') # not essential, but used for some minor stuff in adding sysadmins end diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb new file mode 100644 index 0000000..9f1d42e --- /dev/null +++ b/lib/leap_cli/commands/ca.rb @@ -0,0 +1,162 @@ +require 'openssl' +require 'certificate_authority' +require 'date' +require 'digest/md5' + +module LeapCli; module Commands + + desc 'Creates the public and private key for your Certificate Authority.' + command :'init-ca' do |c| + c.action do |global_options,options,args| + assert_files_missing! :ca_cert, :ca_key + assert_config! 'provider.ca.name' + assert_config! 'provider.ca.bit_size' + + provider = manager.provider + root = CertificateAuthority::Certificate.new + + # set subject + root.subject.common_name = provider.ca.name + possible = ['country', 'state', 'locality', 'organization', 'organizational_unit', 'email_address'] + provider.ca.keys.each do |key| + if possible.include?(key) + root.subject.send(key + '=', provider.ca[key]) + end + end + + # set expiration + years = 2 + today = Date.today + root.not_before = Time.gm today.year, today.month, today.day + root.not_after = root.not_before + years * 60 * 60 * 24 * 365 + + # generate private key + root.serial_number.number = 1 + root.key_material.generate_key(provider.ca.bit_size) + + # sign self + root.signing_entity = true + root.parent = root + root.sign!(ca_root_signing_profile) + + # save + write_file!(:ca_key, root.key_material.private_key.to_pem) + write_file!(:ca_cert, root.to_pem) + end + end + + desc 'Creates or renews a X.509 certificate/key pair for a single node or all nodes' + arg_name '<node-name | "all">', :optional => false, :multiple => false + command :'update-cert' do |c| + c.action do |global_options,options,args| + assert_files_exist! :ca_cert, :ca_key, :msg => 'Run init-ca to create them' + assert_config! 'provider.ca.server_certificates.bit_size' + assert_config! 'provider.ca.server_certificates.life_span' + + if args.first == 'all' + bail! 'not supported yet' + else + provider = manager.provider + ca_root = cert_from_files(:ca_cert, :ca_key) + node = get_node_from_args(args) + + # set subject + cert = CertificateAuthority::Certificate.new + cert.subject.common_name = node.domain.full + + # set expiration + years = provider.ca.server_certificates.life_span.to_i + today = Date.today + cert.not_before = Time.gm today.year, today.month, today.day + cert.not_after = cert.not_before + years * 60 * 60 * 24 * 365 + + # generate key + cert.serial_number.number = cert_serial_number(node.domain.full) + cert.key_material.generate_key(provider.ca.server_certificates.bit_size) + + # sign + cert.parent = ca_root + cert.sign!(server_signing_profile(node)) + + # save + write_file!([:node_x509_key, node.name], cert.key_material.private_key.to_pem) + write_file!([:node_x509_cert, node.name], cert.to_pem) + end + end + end + + desc 'Generates Diffie-Hellman parameter file (needed for server-side of TLS connections)' + command :'init-dh' do |c| + c.action do |global_options,options,args| + long_running do + if cmd_exists?('certtool') + progress('Generating DH parameters (takes a long time)...') + output = assert_run!('certtool --generate-dh-params --sec-param high') + write_file!(:dh_params, output) + else + progress('Generating DH parameters (takes a REALLY long time)...') + output = OpenSSL::PKey::DH.generate(3248).to_pem + write_file!(:dh_params, output) + end + end + end + end + + private + + def cert_from_files(crt, key) + crt = read_file!(crt) + key = read_file!(key) + openssl_cert = OpenSSL::X509::Certificate.new(crt) + cert = CertificateAuthority::Certificate.from_openssl(openssl_cert) + cert.key_material.private_key = OpenSSL::PKey::RSA.new(key) # second argument is password, if set + return cert + end + + def ca_root_signing_profile + { + "extensions" => { + "basicConstraints" => {"ca" => true}, + "keyUsage" => { + "usage" => ["critical", "keyCertSign"] + }, + "extendedKeyUsage" => { + "usage" => [] + } + } + } + end + + # + # for keyusage, openvpn server certs can have keyEncipherment or keyAgreement. I am not sure which is preferable. + # going with keyAgreement for now. + # + def server_signing_profile(node) + { + "extensions" => { + "keyUsage" => { + "usage" => ["digitalSignature", "keyAgreement"] + }, + "extendedKeyUsage" => { + "usage" => ["serverAuth"] + }, + "subjectAltName" => { + "uris" => [ + "IP:#{node.ip_address}", + "DNS:#{node.domain.internal}" + ] + } + } + } + end + + # + # For cert serial numbers, we need a non-colliding number less than 160 bits. + # md5 will do nicely, since there is no need for a secure hash, just a short one. + # (md5 is 128 bits) + # + def cert_serial_number(domain_name) + Digest::MD5.hexdigest("#{domain_name} -- #{Time.now}").to_i(16) + end + +end; end diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb index e96293c..e200a19 100644 --- a/lib/leap_cli/commands/node.rb +++ b/lib/leap_cli/commands/node.rb @@ -8,7 +8,7 @@ module LeapCli; module Commands ## desc 'not yet implemented... Create a new configuration for a node' - command :'new-node' do |c| + command :'add-node' do |c| c.action do |global_options,options,args| end end diff --git a/lib/leap_cli/commands/project.rb b/lib/leap_cli/commands/project.rb index 8ec9625..c748128 100644 --- a/lib/leap_cli/commands/project.rb +++ b/lib/leap_cli/commands/project.rb @@ -4,7 +4,7 @@ module LeapCli desc 'Creates a new provider directory.' arg_name '<directory>' skips_pre - command :'new-provider' do |c| + command :'init-provider' do |c| c.action do |global_options,options,args| directory = args.first unless directory && directory.any? diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb index 246b79f..72958dd 100644 --- a/lib/leap_cli/config/manager.rb +++ b/lib/leap_cli/config/manager.rb @@ -8,7 +8,7 @@ module LeapCli # class Manager - attr_reader :services, :tags, :nodes, :provider + attr_reader :services, :tags, :nodes, :provider, :common ## ## IMPORT EXPORT @@ -18,11 +18,11 @@ module LeapCli # load .json configuration files # def load(provider_dir=Path.provider) - @services = load_all_json(Path.named_path( [:service_config, '*'], provider_dir )) - @tags = load_all_json(Path.named_path( [:tag_config, '*'], provider_dir )) - @common = load_all_json(Path.named_path( :common_config, provider_dir ))['common'] - @provider = load_all_json(Path.named_path( :provider_config, provider_dir ))['provider'] - @nodes = load_all_json(Path.named_path( [:node_config, '*'], provider_dir )) + @services = load_all_json(Path.named_path([:service_config, '*'], provider_dir)) + @tags = load_all_json(Path.named_path([:tag_config, '*'], provider_dir)) + @nodes = load_all_json(Path.named_path([:node_config, '*'], provider_dir)) + @common = load_json(Path.named_path(:common_config, provider_dir)) + @provider = load_json(Path.named_path(:provider_config, provider_dir)) Util::assert!(@provider, "Failed to load provider.json") Util::assert!(@common, "Failed to load common.json") @@ -105,10 +105,10 @@ module LeapCli private - def load_all_json(pattern, config_type = :class) + def load_all_json(pattern) results = Config::ObjectList.new Dir.glob(pattern).each do |filename| - obj = load_json(filename, config_type) + obj = load_json(filename) if obj name = File.basename(filename).sub(/\.json$/,'') obj['name'] ||= name @@ -118,9 +118,7 @@ module LeapCli results end - def load_json(filename, config_type) - #log2 { filename.sub(/^#{Regexp.escape(Path.root)}/,'') } - + def load_json(filename) # # read file, strip out comments # (File.read(filename) would be faster, but we like ability to have comments) @@ -133,9 +131,8 @@ module LeapCli end end - # parse json, and flatten hash + # parse json begin - #hash = Oj.load(buffer.string) || {} hash = JSON.parse(buffer.string, :object_class => Hash, :array_class => Array) || {} rescue SyntaxError => exc log0 'Error in file "%s":' % filename diff --git a/lib/leap_cli/config/object.rb b/lib/leap_cli/config/object.rb index e044353..06a4fef 100644 --- a/lib/leap_cli/config/object.rb +++ b/lib/leap_cli/config/object.rb @@ -79,8 +79,6 @@ module LeapCli end elsif self.has_key?(key) evaluate_value(key) - elsif @node != self - @node.get!(key) else raise NoMethodError.new(key, "No method '#{key}' for #{self.class}") end @@ -110,7 +108,7 @@ module LeapCli # def deep_merge!(object) object.each do |key,new_value| - old_value = self[key] + old_value = self.fetch key, nil if old_value.is_a?(Hash) || new_value.is_a?(Hash) # merge hashes value = Config::Object.new(@manager, @node) @@ -152,7 +150,7 @@ module LeapCli else if value =~ /^= (.*)$/ begin - value = eval($1, self.send(:binding)) + value = eval($1, @node.send(:binding)) self[key] = value rescue SystemStackError => exc puts "STACK OVERFLOW, BAILING OUT" diff --git a/lib/leap_cli/path.rb b/lib/leap_cli/path.rb index 9b4e3c9..aa20e17 100644 --- a/lib/leap_cli/path.rb +++ b/lib/leap_cli/path.rb @@ -23,7 +23,12 @@ module LeapCli; module Path :hiera => 'hiera/#{arg}.yaml', :node_ssh_pub_key => 'files/nodes/#{arg}/#{arg}_ssh_key.pub', :known_hosts => 'files/ssh/known_hosts', - :authorized_keys => 'files/ssh/authorized_keys' + :authorized_keys => 'files/ssh/authorized_keys', + :ca_key => 'files/ca/ca.key', + :ca_cert => 'files/ca/ca.crt', + :dh_params => 'files/ca/dh.pem', + :node_x509_key => 'files/nodes/#{arg}/#{arg}.key', + :node_x509_cert => 'files/nodes/#{arg}/#{arg}.crt' } # @@ -132,7 +137,12 @@ module LeapCli; module Path # def self.named_path(name, provider_dir=Path.provider) if name.is_a? Array - name, arg = name + if name.length > 2 + arg = name[1..-1] + name = name[0] + else + name, arg = name + end else arg = nil end diff --git a/lib/leap_cli/util.rb b/lib/leap_cli/util.rb index 3b0c334..3bfb66b 100644 --- a/lib/leap_cli/util.rb +++ b/lib/leap_cli/util.rb @@ -67,6 +67,41 @@ module LeapCli return output end + def assert_files_missing!(*files) + options = files.last.is_a?(Hash) ? files.pop : {} + file_list = files.collect { |file_path| + file_path = Path.named_path(file_path) + File.exists?(file_path) ? relative_path(file_path) : nil + }.compact + if file_list.length > 1 + bail! "Sorry, we can't continue because these files already exist: #{file_list.join(', ')}. You are not supposed to remove these files. Do so only with caution." + elsif file_list.length == 1 + bail! "Sorry, we can't continue because this file already exists: #{file_list}. You are not supposed to remove this file. Do so only with caution." + end + end + + def assert_config!(conf_path) + value = nil + begin + value = eval(conf_path, manager.send(:binding)) + rescue NoMethodError + end + assert! value, "* Error: Nothing set for #{conf_path}" + end + + def assert_files_exist!(*files) + options = files.last.is_a?(Hash) ? files.pop : {} + file_list = files.collect { |file_path| + file_path = Path.named_path(file_path) + !File.exists?(file_path) ? relative_path(file_path) : nil + }.compact + if file_list.length > 1 + bail! "Sorry, you are missing these files: #{file_list.join(', ')}. #{options[:msg]}" + elsif file_list.length == 1 + bail! "Sorry, you are missing this file: #{file_list.join(', ')}. #{options[:msg]}" + end + end + ## ## FILES AND DIRECTORIES ## @@ -176,14 +211,9 @@ module LeapCli end end - #def rename_file(filepath) - #end - - #private - - ## - ## PRIVATE HELPER METHODS - ## + def cmd_exists?(cmd) + `which #{cmd}`.strip.chars.any? + end # # compares md5 fingerprints to see if the contents of a file match the string we have in memory @@ -198,6 +228,34 @@ module LeapCli end end + ## + ## PROCESSES + ## + + # + # run a long running block of code in a separate process and display marching ants as time goes by. + # if the user hits ctrl-c, the program exits. + # + def long_running(&block) + pid = fork + if pid == nil + yield + exit! + end + Signal.trap("SIGINT") do + Process.kill("KILL", pid) + Process.wait(pid) + bail! + end + while true + sleep 0.2 + STDOUT.print '.' + STDOUT.flush + break if Process.wait(pid, Process::WNOHANG) + end + STDOUT.puts + end + end end diff --git a/lib/leap_cli/version.rb b/lib/leap_cli/version.rb index 366e5a2..437d861 100644 --- a/lib/leap_cli/version.rb +++ b/lib/leap_cli/version.rb @@ -2,6 +2,6 @@ module LeapCli unless defined?(LeapCli::VERSION) VERSION = '0.1.0' SUMMARY = 'Command line interface to the LEAP platform' - DESCRIPTION = 'Provides the command "leap", used to manage a bevy of servers running the LEAP platform from the comfort of your own home.' + DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.' end end diff --git a/test/provider/common.json b/test/provider/common.json index 8f83558..9e19836 100644 --- a/test/provider/common.json +++ b/test/provider/common.json @@ -17,4 +17,9 @@ "known_hosts": "= file :known_hosts", "port": 22 } + #"x509": { + # "use": false, + # "cert": "= x509.use ? file(:node_x509_cert) : nil", + # "key": "= x509.use ? file(:node_x509_key) : nil" + #} } diff --git a/test/provider/provider.json b/test/provider/provider.json index 4e8bb34..d4153a6 100644 --- a/test/provider/provider.json +++ b/test/provider/provider.json @@ -13,7 +13,12 @@ "enrollment_policy": "open", "ca": { "name": "Rewire Root CA", - "organization": "#{name}", - "bit_size": 4096 + "organization": "= global.provider.name[global.provider.default_language]", + "organizational_unit": "= 'https://' + global.common.domain.full_suffix", + "bit_size": 4096, + "server_certificates": { + "bit_size": 3248, + "life_span": "1y" + } } }
\ No newline at end of file diff --git a/test/provider/services/openvpn.json b/test/provider/services/openvpn.json index 86d6c14..629c5b7 100644 --- a/test/provider/services/openvpn.json +++ b/test/provider/services/openvpn.json @@ -5,9 +5,12 @@ "nat": true, "ca_crt": "= file 'ca/ca.crt'", "ca_key": "= file 'ca/ca.key'", - "dh_key": "= file 'ca/dh.key'", + "dh": "= file 'ca/dh.pem'", "server_crt": "= file domain.name + '.crt'", "server_key": "= file domain.name + '.key'" }, "service_type": "user_service" + #"x509": { + # "use": true + #} } |