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 +  #}  } | 
