puppetserver-ca 0.0.1.pre.dev1 → 0.1.0
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 +4 -4
- data/.travis.yml +7 -1
- data/Gemfile +3 -0
- data/exe/puppetserver-ca +10 -0
- data/lib/puppetserver/ca/cli.rb +105 -0
- data/lib/puppetserver/ca/logger.rb +41 -0
- data/lib/puppetserver/ca/puppet_config.rb +134 -0
- data/lib/puppetserver/ca/setup_action.rb +211 -0
- data/lib/puppetserver/ca/version.rb +1 -1
- data/lib/puppetserver/ca/x509_loader.rb +131 -0
- metadata +13 -6
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 3ce9f1836cdf33c3a90c4ead5503c9e3bd77dd4c
         | 
| 4 | 
            +
              data.tar.gz: 4c935b843c029cce7626c9b5ae5ec4d2e3d7a483
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 136312e8f3183ba47e97d7e256c6887e22b044bc4b455cb0d7f7e635835b511a83e2a78d53c87b6c45f12c1b11ff149a82dd02346324fde2a75dcb656de4c688
         | 
| 7 | 
            +
              data.tar.gz: 1bff6c715e519f6cc4fcb238059c318659b244a7fad493d8a4b446a7d204a9947464838adb76f213a80fbf7416088b49231dab4531013318c8703abf01b6b579
         | 
    
        data/.travis.yml
    CHANGED
    
    
    
        data/Gemfile
    CHANGED
    
    
    
        data/exe/puppetserver-ca
    ADDED
    
    | @@ -0,0 +1,10 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # This requires everything in our Gemfile, so when functionally testing
         | 
| 4 | 
            +
            # our debugging tools are available without having to require them
         | 
| 5 | 
            +
            require 'bundler/setup'
         | 
| 6 | 
            +
            Bundler.require(:default)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            require 'puppetserver/ca/cli'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            exit Puppetserver::Ca::Cli.run(ARGV)
         | 
| @@ -0,0 +1,105 @@ | |
| 1 | 
            +
            require 'optparse'
         | 
| 2 | 
            +
            require 'puppetserver/ca/version'
         | 
| 3 | 
            +
            require 'puppetserver/ca/setup_action'
         | 
| 4 | 
            +
            require 'puppetserver/ca/logger'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Puppetserver
         | 
| 7 | 
            +
              module Ca
         | 
| 8 | 
            +
                class Cli
         | 
| 9 | 
            +
                  BANNER= <<-BANNER
         | 
| 10 | 
            +
            Usage: puppetserver ca <action> [options]
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            Manage the Private Key Infrastructure for
         | 
| 13 | 
            +
            Puppet Server's built-in Certificate Authority
         | 
| 14 | 
            +
            BANNER
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  VALID_ACTIONS = {'setup' => SetupAction}
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  ACTION_LIST = "\nAvailable Actions:\n" +
         | 
| 19 | 
            +
                    VALID_ACTIONS.map do |action, cls|
         | 
| 20 | 
            +
                      "    #{action}\t#{cls::SUMMARY}"
         | 
| 21 | 
            +
                    end.join("\n")
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  ACTION_OPTIONS = "\nAction Options:\n" +
         | 
| 24 | 
            +
                    VALID_ACTIONS.map do |action, cls|
         | 
| 25 | 
            +
                      "  #{action}:\n" +
         | 
| 26 | 
            +
                      cls.parser.summarize.
         | 
| 27 | 
            +
                        select{|line| line =~ /^\s*--/ }.
         | 
| 28 | 
            +
                        reject{|line| line =~ /--help|--version/ }.join('')
         | 
| 29 | 
            +
                    end.join("\n")
         | 
| 30 | 
            +
             | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def self.run(cli_args = ARGV, out = STDOUT, err = STDERR)
         | 
| 33 | 
            +
                    logger = Puppetserver::Ca::Logger.new(:info, out, err)
         | 
| 34 | 
            +
                    parser, general_options, unparsed = parse_general_inputs(cli_args)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                    if general_options['version']
         | 
| 37 | 
            +
                      logger.inform Puppetserver::Ca::VERSION
         | 
| 38 | 
            +
                      return 0
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    action_argument = unparsed.shift
         | 
| 42 | 
            +
                    action_class = VALID_ACTIONS[action_argument]
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    if general_options['help']
         | 
| 45 | 
            +
                      if action_class
         | 
| 46 | 
            +
                        logger.inform action_class.parser.help
         | 
| 47 | 
            +
                      else
         | 
| 48 | 
            +
                        logger.inform parser.help
         | 
| 49 | 
            +
                      end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                      return 0
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    if action_class
         | 
| 55 | 
            +
                      action = action_class.new(logger)
         | 
| 56 | 
            +
                      input, exit_code = action.parse(unparsed)
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                      if exit_code
         | 
| 59 | 
            +
                        return exit_code
         | 
| 60 | 
            +
                      else
         | 
| 61 | 
            +
                        return action.run(input)
         | 
| 62 | 
            +
                      end
         | 
| 63 | 
            +
                    else
         | 
| 64 | 
            +
                      logger.warn "Unknown action: #{action_argument}"
         | 
| 65 | 
            +
                      logger.warn parser.help
         | 
| 66 | 
            +
                      return 1
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  def self.parse_general_inputs(inputs)
         | 
| 71 | 
            +
                    parsed = {}
         | 
| 72 | 
            +
                    general_parser = OptionParser.new do |opts|
         | 
| 73 | 
            +
                      opts.banner = BANNER
         | 
| 74 | 
            +
                      opts.separator ACTION_LIST
         | 
| 75 | 
            +
                      opts.separator "\nGeneral Options:"
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                      opts.on('--help', 'Display this general help output') do |help|
         | 
| 78 | 
            +
                        parsed['help'] = true
         | 
| 79 | 
            +
                      end
         | 
| 80 | 
            +
                      opts.on('--version', 'Display the version') do |v|
         | 
| 81 | 
            +
                        parsed['version'] = true
         | 
| 82 | 
            +
                      end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                      opts.separator ACTION_OPTIONS
         | 
| 85 | 
            +
                      opts.separator "\nSee `puppetserver ca <action> --help` for detailed info"
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    unparsed, nonopts = [], []
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    begin
         | 
| 92 | 
            +
                      general_parser.order!(inputs) do |nonopt|
         | 
| 93 | 
            +
                        nonopts << nonopt
         | 
| 94 | 
            +
                      end
         | 
| 95 | 
            +
                    rescue OptionParser::InvalidOption => e
         | 
| 96 | 
            +
                      unparsed += e.args
         | 
| 97 | 
            +
                      unparsed << inputs.shift unless inputs.first =~ /^-{1,2}/
         | 
| 98 | 
            +
                      retry
         | 
| 99 | 
            +
                    end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    return general_parser, parsed, nonopts + unparsed
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
              end
         | 
| 105 | 
            +
            end
         | 
| @@ -0,0 +1,41 @@ | |
| 1 | 
            +
            module Puppetserver
         | 
| 2 | 
            +
              module Ca
         | 
| 3 | 
            +
                class Logger
         | 
| 4 | 
            +
                  LEVELS = {error: 1, warning: 2, info: 3, debug: 4}
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(level = :info, out = STDOUT, err = STDERR)
         | 
| 7 | 
            +
                    @level = LEVELS[level]
         | 
| 8 | 
            +
                    if @level.nil?
         | 
| 9 | 
            +
                      raise ArgumentError, "Unknown log level #{level}"
         | 
| 10 | 
            +
                    end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    @out = out
         | 
| 13 | 
            +
                    @err = err
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def debug(text)
         | 
| 17 | 
            +
                    if @level >= LEVELS[:debug]
         | 
| 18 | 
            +
                      @out.puts(text)
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def inform(text)
         | 
| 23 | 
            +
                    if @level >= LEVELS[:info]
         | 
| 24 | 
            +
                      @out.puts(text)
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def warn(text)
         | 
| 29 | 
            +
                    if @level >= LEVELS[:warning]
         | 
| 30 | 
            +
                      @err.puts(text)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def err(text)
         | 
| 35 | 
            +
                    if @level >= LEVELS[:error]
         | 
| 36 | 
            +
                      @err.puts(text)
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| @@ -0,0 +1,134 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module Puppetserver
         | 
| 3 | 
            +
              module Ca
         | 
| 4 | 
            +
                # Provides an interface for asking for Puppet[ Server] settings w/o loading
         | 
| 5 | 
            +
                # either Puppet or Puppet Server. Includes a simple ini parser that will
         | 
| 6 | 
            +
                # ignore Puppet's more complicated conventions.
         | 
| 7 | 
            +
                class PuppetConfig
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  def self.parse(config_path = nil)
         | 
| 10 | 
            +
                    instance = new(config_path)
         | 
| 11 | 
            +
                    instance.load
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    return instance
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  attr_reader :errors, :settings
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def initialize(supplied_config_path = nil)
         | 
| 19 | 
            +
                    @using_default_location = !supplied_config_path
         | 
| 20 | 
            +
                    @config_path = supplied_config_path || user_specific_conf_file
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    @settings = nil
         | 
| 23 | 
            +
                    @errors = []
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  # Return the correct confdir. We check for being root on *nix,
         | 
| 27 | 
            +
                  # else the user path. We do not include a check for running
         | 
| 28 | 
            +
                  # as Adminstrator since non-development scenarios for Puppet Server
         | 
| 29 | 
            +
                  # on Windows are unsupported.
         | 
| 30 | 
            +
                  # Note that Puppet Server runs as the [pe-]puppet user but to
         | 
| 31 | 
            +
                  # start/stop it you must be root.
         | 
| 32 | 
            +
                  def user_specific_conf_dir
         | 
| 33 | 
            +
                    if running_as_root?
         | 
| 34 | 
            +
                      '/etc/puppetlabs/puppet'
         | 
| 35 | 
            +
                    else
         | 
| 36 | 
            +
                      "#{ENV['HOME']}/.puppetlabs/etc/puppet"
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def user_specific_conf_file
         | 
| 41 | 
            +
                    user_specific_conf_dir + '/puppet.conf'
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def load
         | 
| 45 | 
            +
                    if explicitly_given_config_file_or_default_config_exists?
         | 
| 46 | 
            +
                      results = parse_text(File.read(@config_path))
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    results ||= {}
         | 
| 50 | 
            +
                    results[:main] ||= {}
         | 
| 51 | 
            +
                    results[:master] ||= {}
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    overrides = results[:main].merge(results[:master])
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                    @settings = resolve_settings(overrides).freeze
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  # Resolve the cacert, cakey, and cacrl settings from default values,
         | 
| 59 | 
            +
                  # with any overrides for the specific settings or their dependent
         | 
| 60 | 
            +
                  # settings (ssldir, cadir) taken into account.
         | 
| 61 | 
            +
                  def resolve_settings(overrides = {})
         | 
| 62 | 
            +
                    unresolved_setting = /\$[a-z_]+/
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    # Returning the key for unknown keys (rather than nil) is required to
         | 
| 65 | 
            +
                    # keep unknown settings in the string for later verification.
         | 
| 66 | 
            +
                    substitutions = Hash.new {|h, k| k }
         | 
| 67 | 
            +
                    settings = {}
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                    confdir = user_specific_conf_dir
         | 
| 70 | 
            +
                    settings[:confdir] = substitutions['$confdir'] = confdir
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    ssldir = overrides.fetch(:ssldir, '$confdir/ssl')
         | 
| 73 | 
            +
                    settings[:ssldir] = substitutions['$ssldir'] = ssldir.sub('$confdir', confdir)
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    cadir = overrides.fetch(:cadir, '$ssldir/ca')
         | 
| 76 | 
            +
                    settings[:cadir] = substitutions['$cadir'] = cadir.sub(unresolved_setting, substitutions)
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    settings[:cacert] = overrides.fetch(:cacert, '$cadir/ca_crt.pem')
         | 
| 79 | 
            +
                    settings[:cakey] = overrides.fetch(:cakey, '$cadir/ca_key.pem')
         | 
| 80 | 
            +
                    settings[:cacrl] = overrides.fetch(:cacrl, '$cadir/ca_crl.pem')
         | 
| 81 | 
            +
                    settings[:serial] = overrides.fetch(:serial, '$cadir/serial')
         | 
| 82 | 
            +
                    settings[:cert_inventory] = overrides.fetch(:cert_inventory, '$cadir/inventory.txt')
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    settings.each_pair do |key, value|
         | 
| 85 | 
            +
                      settings[key] = value.sub(unresolved_setting, substitutions)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                      if match = settings[key].match(unresolved_setting)
         | 
| 88 | 
            +
                        @errors << "Could not parse #{match[0]} in #{value}, " +
         | 
| 89 | 
            +
                                   'valid settings to be interpolated are ' +
         | 
| 90 | 
            +
                                   '$ssldir or $cadir'
         | 
| 91 | 
            +
                      end
         | 
| 92 | 
            +
                    end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                    return settings
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  # Parse an inifile formatted String. Only captures \word character
         | 
| 98 | 
            +
                  # class keys/section names but nearly any character values (excluding
         | 
| 99 | 
            +
                  # leading whitespace) up to one of whitespace, opening curly brace, or
         | 
| 100 | 
            +
                  # hash sign (Our concern being to capture filesystem path values).
         | 
| 101 | 
            +
                  # Put values without a section into :main.
         | 
| 102 | 
            +
                  #
         | 
| 103 | 
            +
                  # Return Hash of Symbol section names with Symbol setting keys and
         | 
| 104 | 
            +
                  # String values.
         | 
| 105 | 
            +
                  def parse_text(text)
         | 
| 106 | 
            +
                    res = {}
         | 
| 107 | 
            +
                    current_section = :main
         | 
| 108 | 
            +
                    text.each_line do |line|
         | 
| 109 | 
            +
                      case line
         | 
| 110 | 
            +
                      when /^\s*\[(\w+)\].*/
         | 
| 111 | 
            +
                        current_section = $1.to_sym
         | 
| 112 | 
            +
                      when /^\s*(\w+)\s*=\s*([^\s{#]+).*$/
         | 
| 113 | 
            +
                        # Using a Hash with a default key breaks RSpec expectations.
         | 
| 114 | 
            +
                        res[current_section] ||= {}
         | 
| 115 | 
            +
                        res[current_section][$1.to_sym] = $2
         | 
| 116 | 
            +
                      end
         | 
| 117 | 
            +
                    end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                    res
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
             | 
| 123 | 
            +
                 private
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                  def explicitly_given_config_file_or_default_config_exists?
         | 
| 126 | 
            +
                    !@using_default_location || File.exist?(@config_path)
         | 
| 127 | 
            +
                  end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                  def running_as_root?
         | 
| 130 | 
            +
                    !Gem.win_platform? && Process::UID.eid == 0
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
              end
         | 
| 134 | 
            +
            end
         | 
| @@ -0,0 +1,211 @@ | |
| 1 | 
            +
            require 'etc'
         | 
| 2 | 
            +
            require 'fileutils'
         | 
| 3 | 
            +
            require 'optparse'
         | 
| 4 | 
            +
            require 'puppetserver/ca/x509_loader'
         | 
| 5 | 
            +
            require 'puppetserver/ca/puppet_config'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Puppetserver
         | 
| 8 | 
            +
              module Ca
         | 
| 9 | 
            +
                class SetupAction
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  SUMMARY = "Set up the CA's key, certs, and crls"
         | 
| 12 | 
            +
                  BANNER = <<-BANNER
         | 
| 13 | 
            +
            Usage:
         | 
| 14 | 
            +
              puppetserver ca setup [--help|--version]
         | 
| 15 | 
            +
              puppetserver ca setup [--config PATH]
         | 
| 16 | 
            +
                  --private-key PATH --cert-bundle PATH --crl-chain PATH
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            Description:
         | 
| 19 | 
            +
            Given a private key, cert bundle, and a crl chain,
         | 
| 20 | 
            +
            validate and import to the Puppet Server CA.
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            To determine the target location the default puppet.conf
         | 
| 23 | 
            +
            is consulted for custom values. If using a custom puppet.conf
         | 
| 24 | 
            +
            provide it with the --config flag
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            Options:
         | 
| 27 | 
            +
            BANNER
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def initialize(logger)
         | 
| 30 | 
            +
                    @logger = logger
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def run(input)
         | 
| 34 | 
            +
                    bundle_path = input['cert-bundle']
         | 
| 35 | 
            +
                    key_path = input['private-key']
         | 
| 36 | 
            +
                    chain_path = input['crl-chain']
         | 
| 37 | 
            +
                    config_path = input['config']
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    files = [bundle_path, key_path, chain_path, config_path].compact
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    errors = validate_file_paths(files)
         | 
| 42 | 
            +
                    return 1 if log_possible_errors(errors)
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    loader = X509Loader.new(bundle_path, key_path, chain_path)
         | 
| 45 | 
            +
                    return 1 if log_possible_errors(loader.errors)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    puppet = PuppetConfig.parse(config_path)
         | 
| 48 | 
            +
                    return 1 if log_possible_errors(puppet.errors)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    user, group = find_user_and_group
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                    if !File.exist?(puppet.settings[:cadir])
         | 
| 53 | 
            +
                      FileUtils.mkdir_p(puppet.settings[:cadir], mode: 0750)
         | 
| 54 | 
            +
                      FileUtils.chown(user, group, puppet.settings[:cadir])
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    write_file(puppet.settings[:cacert], loader.certs, user, group, 0640)
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    write_file(puppet.settings[:cakey], loader.key, user, group, 0640)
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                    write_file(puppet.settings[:cacrl], loader.crls, user, group, 0640)
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                    if !File.exist?(puppet.settings[:serial])
         | 
| 64 | 
            +
                      write_file(puppet.settings[:serial], "001", user, group, 0640)
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    if !File.exist?(puppet.settings[:cert_inventory])
         | 
| 68 | 
            +
                      write_file(puppet.settings[:cert_inventory],
         | 
| 69 | 
            +
                                 "", user, group, 0640)
         | 
| 70 | 
            +
                    end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    return 0
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def find_user_and_group
         | 
| 76 | 
            +
                    if !running_as_root?
         | 
| 77 | 
            +
                      return Process.euid, Process.egid
         | 
| 78 | 
            +
                    else
         | 
| 79 | 
            +
                      if pe_puppet_exists?
         | 
| 80 | 
            +
                        return 'pe-puppet', 'pe-puppet'
         | 
| 81 | 
            +
                      else
         | 
| 82 | 
            +
                        return 'puppet', 'puppet'
         | 
| 83 | 
            +
                      end
         | 
| 84 | 
            +
                    end
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  def running_as_root?
         | 
| 88 | 
            +
                    !Gem.win_platform? && Process.euid == 0
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  def pe_puppet_exists?
         | 
| 92 | 
            +
                    !!(Etc.getpwnam('pe-puppet') rescue nil)
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  def write_file(path, one_or_more_objects, user, group, mode)
         | 
| 96 | 
            +
                    if File.exist?(path)
         | 
| 97 | 
            +
                      @logger.warn("#{path} exists, overwriting")
         | 
| 98 | 
            +
                    end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                    File.open(path, 'w', mode) do |f|
         | 
| 101 | 
            +
                      Array(one_or_more_objects).each do |object|
         | 
| 102 | 
            +
                        f.puts object.to_s
         | 
| 103 | 
            +
                      end
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                    FileUtils.chown(user, group, path)
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  def log_possible_errors(maybe_errors)
         | 
| 110 | 
            +
                    errors = Array(maybe_errors).compact
         | 
| 111 | 
            +
                    unless errors.empty?
         | 
| 112 | 
            +
                      @logger.err "Error:"
         | 
| 113 | 
            +
                      errors.each do |message|
         | 
| 114 | 
            +
                        @logger.err "    #{message}"
         | 
| 115 | 
            +
                      end
         | 
| 116 | 
            +
                      return true
         | 
| 117 | 
            +
                    end
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  def parse(cli_args)
         | 
| 121 | 
            +
                    parser, inputs, unparsed = parse_inputs(cli_args)
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                    if !unparsed.empty?
         | 
| 124 | 
            +
                      @logger.err 'Error:'
         | 
| 125 | 
            +
                      @logger.err 'Unknown arguments or flags:'
         | 
| 126 | 
            +
                      unparsed.each do |arg|
         | 
| 127 | 
            +
                        @logger.err "    #{arg}"
         | 
| 128 | 
            +
                      end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                      @logger.err ''
         | 
| 131 | 
            +
                      @logger.err parser.help
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                      exit_code = 1
         | 
| 134 | 
            +
                    else
         | 
| 135 | 
            +
                      exit_code = validate_inputs(inputs, parser.help)
         | 
| 136 | 
            +
                    end
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                    return inputs, exit_code
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                  def validate_inputs(input, usage)
         | 
| 142 | 
            +
                    exit_code = nil
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                    if input.values_at('cert-bundle', 'private-key', 'crl-chain').any?(&:nil?)
         | 
| 145 | 
            +
                      @logger.err 'Error:'
         | 
| 146 | 
            +
                      @logger.err 'Missing required argument'
         | 
| 147 | 
            +
                      @logger.err '    --cert-bundle, --private-key, --crl-chain are required'
         | 
| 148 | 
            +
                      @logger.err ''
         | 
| 149 | 
            +
                      @logger.err usage
         | 
| 150 | 
            +
                      exit_code = 1
         | 
| 151 | 
            +
                    end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                    exit_code
         | 
| 154 | 
            +
                  end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                  def parse_inputs(inputs)
         | 
| 157 | 
            +
                    parsed = {}
         | 
| 158 | 
            +
                    unparsed = []
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                    parser = self.class.parser(parsed)
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                    begin
         | 
| 163 | 
            +
                      parser.order!(inputs) do |nonopt|
         | 
| 164 | 
            +
                        unparsed << nonopt
         | 
| 165 | 
            +
                      end
         | 
| 166 | 
            +
                    rescue OptionParser::ParseError => e
         | 
| 167 | 
            +
                      unparsed += e.args
         | 
| 168 | 
            +
                      unparsed << inputs.shift unless inputs.first =~ /^-{1,2}/
         | 
| 169 | 
            +
                      retry
         | 
| 170 | 
            +
                    end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                    return parser, parsed, unparsed
         | 
| 173 | 
            +
                  end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  def self.parser(parsed = {})
         | 
| 176 | 
            +
                    OptionParser.new do |opts|
         | 
| 177 | 
            +
                      opts.banner = BANNER
         | 
| 178 | 
            +
                      opts.on('--help', 'Display this setup specific help output') do |help|
         | 
| 179 | 
            +
                        parsed['help'] = true
         | 
| 180 | 
            +
                      end
         | 
| 181 | 
            +
                      opts.on('--version', 'Output the version') do |v|
         | 
| 182 | 
            +
                        parsed['version'] = true
         | 
| 183 | 
            +
                      end
         | 
| 184 | 
            +
                      opts.on('--config CONF', 'Path to puppet.conf') do |conf|
         | 
| 185 | 
            +
                        parsed['config'] = conf
         | 
| 186 | 
            +
                      end
         | 
| 187 | 
            +
                      opts.on('--private-key KEY', 'Path to PEM encoded key') do |key|
         | 
| 188 | 
            +
                        parsed['private-key'] = key
         | 
| 189 | 
            +
                      end
         | 
| 190 | 
            +
                      opts.on('--cert-bundle BUNDLE', 'Path to PEM encoded bundle') do |bundle|
         | 
| 191 | 
            +
                        parsed['cert-bundle'] = bundle
         | 
| 192 | 
            +
                      end
         | 
| 193 | 
            +
                      opts.on('--crl-chain CHAIN', 'Path to PEM encoded chain') do |chain|
         | 
| 194 | 
            +
                        parsed['crl-chain'] = chain
         | 
| 195 | 
            +
                      end
         | 
| 196 | 
            +
                    end
         | 
| 197 | 
            +
                  end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                  def validate_file_paths(one_or_more_paths)
         | 
| 200 | 
            +
                    errors = []
         | 
| 201 | 
            +
                    Array(one_or_more_paths).each do |path|
         | 
| 202 | 
            +
                      if !File.exist?(path) || !File.readable?(path)
         | 
| 203 | 
            +
                        errors << "Could not read file '#{path}'"
         | 
| 204 | 
            +
                      end
         | 
| 205 | 
            +
                    end
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                    errors
         | 
| 208 | 
            +
                  end
         | 
| 209 | 
            +
                end
         | 
| 210 | 
            +
              end
         | 
| 211 | 
            +
            end
         | 
| @@ -0,0 +1,131 @@ | |
| 1 | 
            +
            require 'openssl'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Puppetserver
         | 
| 4 | 
            +
              module Ca
         | 
| 5 | 
            +
                # Load, validate, and store x509 objects needed by the Puppet Server CA.
         | 
| 6 | 
            +
                class X509Loader
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  attr_reader :errors, :certs, :key, :crls
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def initialize(bundle_path, key_path, chain_path)
         | 
| 11 | 
            +
                    @errors = []
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    @certs = load_certs(bundle_path)
         | 
| 14 | 
            +
                    @key = load_key(key_path)
         | 
| 15 | 
            +
                    @crls = load_crls(chain_path)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    validate(@certs, @key, @crls)
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  # Only do as much validation as is possible, assume whoever tried to
         | 
| 21 | 
            +
                  # load the objects wrote errors about any invalid ones, but that bundle
         | 
| 22 | 
            +
                  # and chain may be empty arrays and pkey may be nil.
         | 
| 23 | 
            +
                  def validate(bundle, pkey, chain)
         | 
| 24 | 
            +
                    if !chain.empty? && !bundle.empty?
         | 
| 25 | 
            +
                      validate_crl_and_cert(chain.first, bundle.first)
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    if pkey && !bundle.empty?
         | 
| 29 | 
            +
                      validate_cert_and_key(pkey, bundle.first)
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    unless bundle.empty?
         | 
| 33 | 
            +
                      validate_full_chain(bundle, chain)
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def load_certs(bundle_path)
         | 
| 38 | 
            +
                    certs, errs = [], []
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    bundle_string = File.read(bundle_path)
         | 
| 41 | 
            +
                    cert_strings = bundle_string.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m)
         | 
| 42 | 
            +
                    cert_strings.each do |cert_string|
         | 
| 43 | 
            +
                      begin
         | 
| 44 | 
            +
                        certs << OpenSSL::X509::Certificate.new(cert_string)
         | 
| 45 | 
            +
                      rescue OpenSSL::X509::CertificateError
         | 
| 46 | 
            +
                        errs << "Could not parse entry:\n#{cert_string}"
         | 
| 47 | 
            +
                      end
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    if certs.empty?
         | 
| 51 | 
            +
                      errs << "Could not detect any certs within #{bundle_path}"
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                    unless errs.empty?
         | 
| 55 | 
            +
                      @errors << "Could not parse #{bundle_path}"
         | 
| 56 | 
            +
                      @errors += errs
         | 
| 57 | 
            +
                    end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    return certs
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  def load_key(key_path)
         | 
| 63 | 
            +
                    begin
         | 
| 64 | 
            +
                      OpenSSL::PKey.read(File.read(key_path))
         | 
| 65 | 
            +
                    rescue ArgumentError, OpenSSL::PKey::PKeyError => e
         | 
| 66 | 
            +
                      @errors << "Could not parse #{key_path}"
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                      return nil
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                  end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  def load_crls(chain_path)
         | 
| 73 | 
            +
                    errs, crls = [], []
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    chain_string = File.read(chain_path)
         | 
| 76 | 
            +
                    crl_strings = chain_string.scan(/-----BEGIN X509 CRL-----.*?-----END X509 CRL-----/m)
         | 
| 77 | 
            +
                    crl_strings.map do |crl_string|
         | 
| 78 | 
            +
                      begin
         | 
| 79 | 
            +
                        crls << OpenSSL::X509::CRL.new(crl_string)
         | 
| 80 | 
            +
                      rescue OpenSSL::X509::CRLError
         | 
| 81 | 
            +
                        errs << "Could not parse entry:\n#{crl_string}"
         | 
| 82 | 
            +
                      end
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    if crls.empty?
         | 
| 86 | 
            +
                      errs << "Could not detect any crls within #{chain_path}"
         | 
| 87 | 
            +
                    end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    unless errs.empty?
         | 
| 90 | 
            +
                      @errors << "Could not parse #{chain_path}"
         | 
| 91 | 
            +
                      @errors += errs
         | 
| 92 | 
            +
                    end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                    return crls
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  def validate_cert_and_key(key, cert)
         | 
| 98 | 
            +
                    unless cert.check_private_key(key)
         | 
| 99 | 
            +
                      @errors << 'Private key and certificate do not match'
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                  def validate_crl_and_cert(crl, cert)
         | 
| 104 | 
            +
                    unless crl.issuer == cert.subject
         | 
| 105 | 
            +
                      @errors << 'Leaf CRL was not issued by leaf certificate'
         | 
| 106 | 
            +
                    end
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  # By creating an X509::Store and validating the leaf cert with it we:
         | 
| 110 | 
            +
                  #   - Ensure a full chain of trust (root to leaf) is within the bundle
         | 
| 111 | 
            +
                  #   - If provided, there are CRLs for the CAs
         | 
| 112 | 
            +
                  #   - If provided, no CAs within the chain of trust have been revoked
         | 
| 113 | 
            +
                  # However this does allow for:
         | 
| 114 | 
            +
                  #   - Additional, ignored, certs and CRLs in the bundle/chain
         | 
| 115 | 
            +
                  #   - certs and CRLs in any order (as long as the leaf cert is first)
         | 
| 116 | 
            +
                  def validate_full_chain(certs, crls)
         | 
| 117 | 
            +
                    store = OpenSSL::X509::Store.new
         | 
| 118 | 
            +
                    certs.each {|cert| store.add_cert(cert) }
         | 
| 119 | 
            +
                    if !crls.empty?
         | 
| 120 | 
            +
                      store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
         | 
| 121 | 
            +
                      crls.each {|crl| store.add_crl(crl) }
         | 
| 122 | 
            +
                    end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    unless store.verify(certs.first)
         | 
| 125 | 
            +
                      @errors << 'Leaf certificate could not be validated'
         | 
| 126 | 
            +
                      @errors << "Validating cert store returned: #{store.error} - #{store.error_string}"
         | 
| 127 | 
            +
                    end
         | 
| 128 | 
            +
                  end
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
              end
         | 
| 131 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: puppetserver-ca
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Puppet, Inc.
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2018- | 
| 11 | 
            +
            date: 2018-07-19 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: bundler
         | 
| @@ -55,7 +55,8 @@ dependencies: | |
| 55 55 | 
             
            description: 
         | 
| 56 56 | 
             
            email:
         | 
| 57 57 | 
             
            - release@puppet.com
         | 
| 58 | 
            -
            executables: | 
| 58 | 
            +
            executables:
         | 
| 59 | 
            +
            - puppetserver-ca
         | 
| 59 60 | 
             
            extensions: []
         | 
| 60 61 | 
             
            extra_rdoc_files: []
         | 
| 61 62 | 
             
            files:
         | 
| @@ -70,9 +71,15 @@ files: | |
| 70 71 | 
             
            - Rakefile
         | 
| 71 72 | 
             
            - bin/console
         | 
| 72 73 | 
             
            - bin/setup
         | 
| 74 | 
            +
            - exe/puppetserver-ca
         | 
| 73 75 | 
             
            - lib/puppetserver/ca.rb
         | 
| 76 | 
            +
            - lib/puppetserver/ca/cli.rb
         | 
| 77 | 
            +
            - lib/puppetserver/ca/logger.rb
         | 
| 78 | 
            +
            - lib/puppetserver/ca/puppet_config.rb
         | 
| 79 | 
            +
            - lib/puppetserver/ca/setup_action.rb
         | 
| 74 80 | 
             
            - lib/puppetserver/ca/stub.rb
         | 
| 75 81 | 
             
            - lib/puppetserver/ca/version.rb
         | 
| 82 | 
            +
            - lib/puppetserver/ca/x509_loader.rb
         | 
| 76 83 | 
             
            - puppetserver-ca.gemspec
         | 
| 77 84 | 
             
            homepage: https://github.com/puppetlabs/puppetserver-ca-cli/
         | 
| 78 85 | 
             
            licenses:
         | 
| @@ -89,12 +96,12 @@ required_ruby_version: !ruby/object:Gem::Requirement | |
| 89 96 | 
             
                  version: '0'
         | 
| 90 97 | 
             
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 91 98 | 
             
              requirements:
         | 
| 92 | 
            -
              - - " | 
| 99 | 
            +
              - - ">="
         | 
| 93 100 | 
             
                - !ruby/object:Gem::Version
         | 
| 94 | 
            -
                  version:  | 
| 101 | 
            +
                  version: '0'
         | 
| 95 102 | 
             
            requirements: []
         | 
| 96 103 | 
             
            rubyforge_project: 
         | 
| 97 | 
            -
            rubygems_version: 2.5. | 
| 104 | 
            +
            rubygems_version: 2.5.1
         | 
| 98 105 | 
             
            signing_key: 
         | 
| 99 106 | 
             
            specification_version: 4
         | 
| 100 107 | 
             
            summary: A simple CLI tool for interacting with Puppet Server's Certificate Authority
         |