puppetserver-ca 0.0.1.pre.dev1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|