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
|