openvoxserver-ca 3.0.0.pre.rc1
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 +7 -0
- data/.github/dependabot.yml +17 -0
- data/.github/release.yml +41 -0
- data/.github/workflows/gem_release.yaml +106 -0
- data/.github/workflows/prepare_release.yml +28 -0
- data/.github/workflows/release.yml +28 -0
- data/.github/workflows/unit_tests.yaml +45 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +15 -0
- data/CODEOWNERS +4 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +15 -0
- data/Gemfile +20 -0
- data/LICENSE +202 -0
- data/README.md +118 -0
- data/Rakefile +30 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/puppetserver-ca +10 -0
- data/lib/puppetserver/ca/action/clean.rb +109 -0
- data/lib/puppetserver/ca/action/delete.rb +286 -0
- data/lib/puppetserver/ca/action/enable.rb +140 -0
- data/lib/puppetserver/ca/action/generate.rb +330 -0
- data/lib/puppetserver/ca/action/import.rb +196 -0
- data/lib/puppetserver/ca/action/list.rb +253 -0
- data/lib/puppetserver/ca/action/migrate.rb +97 -0
- data/lib/puppetserver/ca/action/prune.rb +289 -0
- data/lib/puppetserver/ca/action/revoke.rb +108 -0
- data/lib/puppetserver/ca/action/setup.rb +188 -0
- data/lib/puppetserver/ca/action/sign.rb +146 -0
- data/lib/puppetserver/ca/certificate_authority.rb +418 -0
- data/lib/puppetserver/ca/cli.rb +145 -0
- data/lib/puppetserver/ca/config/puppet.rb +309 -0
- data/lib/puppetserver/ca/config/puppetserver.rb +84 -0
- data/lib/puppetserver/ca/errors.rb +40 -0
- data/lib/puppetserver/ca/host.rb +176 -0
- data/lib/puppetserver/ca/local_certificate_authority.rb +304 -0
- data/lib/puppetserver/ca/logger.rb +49 -0
- data/lib/puppetserver/ca/stub.rb +17 -0
- data/lib/puppetserver/ca/utils/cli_parsing.rb +67 -0
- data/lib/puppetserver/ca/utils/config.rb +61 -0
- data/lib/puppetserver/ca/utils/file_system.rb +109 -0
- data/lib/puppetserver/ca/utils/http_client.rb +232 -0
- data/lib/puppetserver/ca/utils/inventory.rb +84 -0
- data/lib/puppetserver/ca/utils/signing_digest.rb +27 -0
- data/lib/puppetserver/ca/version.rb +5 -0
- data/lib/puppetserver/ca/x509_loader.rb +170 -0
- data/lib/puppetserver/ca.rb +7 -0
- data/openvoxserver-ca.gemspec +31 -0
- data/tasks/spec.rake +15 -0
- data/tasks/vox.rake +19 -0
- metadata +154 -0
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'openssl'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
require 'puppetserver/ca/errors'
|
6
|
+
|
7
|
+
module Puppetserver
|
8
|
+
module Ca
|
9
|
+
module Utils
|
10
|
+
# Utilities for doing HTTPS against the CA that wraps Net::HTTP constructs
|
11
|
+
class HttpClient
|
12
|
+
|
13
|
+
attr_reader :store
|
14
|
+
|
15
|
+
# Not all connections require a client cert to be present.
|
16
|
+
# For example, when querying the status endpoint.
|
17
|
+
def initialize(logger, settings, with_client_cert: true)
|
18
|
+
@default_headers = make_headers(ENV['HOME'])
|
19
|
+
@logger = logger
|
20
|
+
@store = make_store(settings[:localcacert],
|
21
|
+
settings[:certificate_revocation],
|
22
|
+
settings[:hostcrl])
|
23
|
+
|
24
|
+
if with_client_cert
|
25
|
+
@cert = load_cert(settings[:hostcert])
|
26
|
+
@key = load_key(settings[:hostprivkey])
|
27
|
+
else
|
28
|
+
@cert = nil
|
29
|
+
@key = nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def load_cert(path)
|
34
|
+
load_with_errors(path, 'hostcert') do |content|
|
35
|
+
OpenSSL::X509::Certificate.new(content)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def load_key(path)
|
40
|
+
load_with_errors(path, 'hostprivkey') do |content|
|
41
|
+
OpenSSL::PKey.read(content)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Takes an instance URL (defined lower in the file), and creates a
|
46
|
+
# connection. The given block is passed our own Connection object.
|
47
|
+
# The Connection object should have HTTP verbs defined on it that take
|
48
|
+
# a body (and optional overrides). Returns whatever the block given returned.
|
49
|
+
def with_connection(url, &block)
|
50
|
+
request = ->(conn) { block.call(Connection.new(conn, url, @logger, @default_headers)) }
|
51
|
+
|
52
|
+
begin
|
53
|
+
Net::HTTP.start(url.host, url.port,
|
54
|
+
use_ssl: true, cert_store: @store,
|
55
|
+
cert: @cert, key: @key,
|
56
|
+
&request)
|
57
|
+
rescue StandardError => e
|
58
|
+
raise ConnectionFailed.create(e,
|
59
|
+
"Failed connecting to #{url.full_url}\n" +
|
60
|
+
" Root cause: #{e.message}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def make_headers(home)
|
67
|
+
headers = {
|
68
|
+
'User-Agent' => 'PuppetserverCaCli',
|
69
|
+
'Content-Type' => 'application/json',
|
70
|
+
'Accept' => 'application/json'
|
71
|
+
}
|
72
|
+
|
73
|
+
token_path = "#{home}/.puppetlabs/token"
|
74
|
+
if File.exist?(token_path)
|
75
|
+
headers['X-Authentication'] = File.read(token_path)
|
76
|
+
end
|
77
|
+
|
78
|
+
headers
|
79
|
+
end
|
80
|
+
|
81
|
+
def load_with_errors(path, setting, &block)
|
82
|
+
begin
|
83
|
+
content = File.read(path)
|
84
|
+
block.call(content)
|
85
|
+
rescue Errno::ENOENT => e
|
86
|
+
raise FileNotFound.create(e,
|
87
|
+
"Could not find '#{setting}' at '#{path}'")
|
88
|
+
|
89
|
+
rescue OpenSSL::OpenSSLError => e
|
90
|
+
raise InvalidX509Object.create(e,
|
91
|
+
"Could not parse '#{setting}' at '#{path}'.\n" +
|
92
|
+
" OpenSSL returned: #{e.message}")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Helper class that wraps a Net::HTTP connection, a HttpClient::URL
|
97
|
+
# and defines methods named after HTTP verbs that are called on the
|
98
|
+
# saved connection, returning a Result.
|
99
|
+
class Connection
|
100
|
+
|
101
|
+
def initialize(net_http_connection, url_struct, logger, default_headers)
|
102
|
+
@conn = net_http_connection
|
103
|
+
@url = url_struct
|
104
|
+
@logger = logger
|
105
|
+
@default_headers = default_headers
|
106
|
+
end
|
107
|
+
|
108
|
+
def get(url_overide = nil, header_overrides = {})
|
109
|
+
url = url_overide || @url
|
110
|
+
headers = @default_headers.merge(header_overrides)
|
111
|
+
|
112
|
+
@logger.debug("Making a GET request at #{url.full_url}")
|
113
|
+
|
114
|
+
request = Net::HTTP::Get.new(url.to_uri, headers)
|
115
|
+
result = @conn.request(request)
|
116
|
+
Result.new(result.code, result.body)
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
def put(body, url_override = nil, header_overrides = {})
|
121
|
+
url = url_override || @url
|
122
|
+
headers = @default_headers.merge(header_overrides)
|
123
|
+
|
124
|
+
@logger.debug("Making a PUT request at #{url.full_url}")
|
125
|
+
|
126
|
+
request = Net::HTTP::Put.new(url.to_uri, headers)
|
127
|
+
request.body = body
|
128
|
+
result = @conn.request(request)
|
129
|
+
|
130
|
+
Result.new(result.code, result.body)
|
131
|
+
end
|
132
|
+
|
133
|
+
def post(body, url_override = nil, header_overrides = {})
|
134
|
+
url = url_override || @url
|
135
|
+
headers = @default_headers.merge(header_overrides)
|
136
|
+
|
137
|
+
@logger.debug("Making a POST request at #{url.full_url}")
|
138
|
+
|
139
|
+
request = Net::HTTP::Post.new(url.to_uri, headers)
|
140
|
+
request.body = body
|
141
|
+
result = @conn.request(request)
|
142
|
+
|
143
|
+
Result.new(result.code, result.body)
|
144
|
+
end
|
145
|
+
|
146
|
+
def delete(url_override = nil, header_overrides = {})
|
147
|
+
url = url_override || @url
|
148
|
+
headers = @default_headers.merge(header_overrides)
|
149
|
+
|
150
|
+
@logger.debug("Making a DELETE request at #{url.full_url}")
|
151
|
+
|
152
|
+
result = @conn.request(Net::HTTP::Delete.new(url.to_uri, headers))
|
153
|
+
|
154
|
+
Result.new(result.code, result.body)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Just provide the bits of Net::HTTPResponse we care about
|
159
|
+
Result = Struct.new(:code, :body)
|
160
|
+
|
161
|
+
# Like URI, but not... maybe of suspicious value
|
162
|
+
URL = Struct.new(:protocol, :host, :port,
|
163
|
+
:endpoint, :version,
|
164
|
+
:resource_type, :resource_name, :query) do
|
165
|
+
def full_url
|
166
|
+
url = protocol + '://' + host + ':' + port + '/' +
|
167
|
+
[endpoint, version, resource_type, resource_name].compact.join('/')
|
168
|
+
|
169
|
+
url = url + "?" + URI.encode_www_form(query) unless query.nil? || query.empty?
|
170
|
+
return url
|
171
|
+
end
|
172
|
+
|
173
|
+
def to_uri
|
174
|
+
URI(full_url)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def make_store(bundle, crl_usage, crls = nil)
|
179
|
+
store = OpenSSL::X509::Store.new
|
180
|
+
store.purpose = OpenSSL::X509::PURPOSE_ANY
|
181
|
+
store.add_file(bundle)
|
182
|
+
|
183
|
+
if crl_usage != :ignore
|
184
|
+
|
185
|
+
flags = OpenSSL::X509::V_FLAG_CRL_CHECK
|
186
|
+
if crl_usage == :chain
|
187
|
+
flags |= OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
|
188
|
+
end
|
189
|
+
|
190
|
+
store.flags = flags
|
191
|
+
delimiter = /-----BEGIN X509 CRL-----.*?-----END X509 CRL-----/m
|
192
|
+
File.read(crls).scan(delimiter).each do |crl|
|
193
|
+
store.add_crl(OpenSSL::X509::CRL.new(crl))
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
store
|
198
|
+
end
|
199
|
+
|
200
|
+
# Queries the simple status endpoint for the status of the CA service.
|
201
|
+
# Returns true if it receives back a response of "running", and false if
|
202
|
+
# no connection can be made, or a different response is received.
|
203
|
+
def self.check_server_online(settings, logger)
|
204
|
+
status_url = URL.new('https', settings[:ca_server], settings[:ca_port], 'status', 'v1', 'simple', 'ca')
|
205
|
+
begin
|
206
|
+
# Generating certs offline is necessary if the server cert has been destroyed
|
207
|
+
# or compromised. Since querying the status endpoint does not require a client cert, and
|
208
|
+
# we commonly won't have one, don't require one for creating the connection.
|
209
|
+
# Additionally, we want to ensure the server is stopped before migrating the CA dir to
|
210
|
+
# avoid issues with writing to the CA dir and moving it.
|
211
|
+
self.new(logger, settings, with_client_cert: false).with_connection(status_url) do |conn|
|
212
|
+
result = conn.get
|
213
|
+
if result.body == "running"
|
214
|
+
logger.err "Puppetserver service is running. Please stop it before attempting to run this command."
|
215
|
+
true
|
216
|
+
else
|
217
|
+
false
|
218
|
+
end
|
219
|
+
end
|
220
|
+
rescue Puppetserver::Ca::ConnectionFailed => e
|
221
|
+
if e.wrapped.is_a? Errno::ECONNREFUSED
|
222
|
+
return false
|
223
|
+
else
|
224
|
+
raise e
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Puppetserver
|
4
|
+
module Ca
|
5
|
+
module Utils
|
6
|
+
module Inventory
|
7
|
+
|
8
|
+
# Note that the inventory file may have multiple entries for the same certname,
|
9
|
+
# so it should only provide the latest cert for the given certname.
|
10
|
+
def self.parse_inventory_file(path, logger)
|
11
|
+
unless File.exist?(path)
|
12
|
+
logger.err("Could not find inventory at #{path}")
|
13
|
+
return [{}, true]
|
14
|
+
end
|
15
|
+
inventory = {}
|
16
|
+
errored = false
|
17
|
+
File.readlines(path).each do |line|
|
18
|
+
# Shouldn't be any blank lines, but skip them if there are
|
19
|
+
next if line.strip.empty?
|
20
|
+
|
21
|
+
items = line.strip.split
|
22
|
+
if items.count != 4
|
23
|
+
logger.err("Invalid entry found in inventory.txt: #{line}")
|
24
|
+
errored = true
|
25
|
+
next
|
26
|
+
end
|
27
|
+
unless items[0].match(/^(?:0x)?[A-Fa-f0-9]+$/)
|
28
|
+
logger.err("Invalid serial found in inventory.txt line: #{line}")
|
29
|
+
errored = true
|
30
|
+
next
|
31
|
+
end
|
32
|
+
serial = items[0].hex
|
33
|
+
not_before = nil
|
34
|
+
not_after = nil
|
35
|
+
begin
|
36
|
+
not_before = Time.parse(items[1])
|
37
|
+
rescue ArgumentError
|
38
|
+
logger.err("Invalid not_before time found in inventory.txt line: #{line}")
|
39
|
+
errored = true
|
40
|
+
next
|
41
|
+
end
|
42
|
+
begin
|
43
|
+
not_after = Time.parse(items[2])
|
44
|
+
rescue ArgumentError
|
45
|
+
logger.err("Invalid not_after time found in inventory.txt line: #{line}")
|
46
|
+
errored = true
|
47
|
+
next
|
48
|
+
end
|
49
|
+
unless items[3].start_with?('/CN=')
|
50
|
+
logger.err("Invalid certname found in inventory.txt line: #{line}")
|
51
|
+
errored = true
|
52
|
+
next
|
53
|
+
end
|
54
|
+
certname = items[3][4..-1]
|
55
|
+
|
56
|
+
if !inventory.keys.include?(certname)
|
57
|
+
inventory[certname] = {
|
58
|
+
:serial => serial,
|
59
|
+
:old_serials => [],
|
60
|
+
:not_before => not_before,
|
61
|
+
:not_after => not_after,
|
62
|
+
}
|
63
|
+
else
|
64
|
+
if not_after >= inventory[certname][:not_after]
|
65
|
+
# This is a newer cert than the one we currently have recorded,
|
66
|
+
# so save the previous serial in :old_serials
|
67
|
+
inventory[certname][:old_serials] << inventory[certname][:serial]
|
68
|
+
inventory[certname][:serial] = serial
|
69
|
+
inventory[certname][:not_before] = not_before
|
70
|
+
inventory[certname][:not_after] = not_after
|
71
|
+
else
|
72
|
+
# This somehow is an older cert (shouldn't really be possible as we just append
|
73
|
+
# to the file with each new cert and we are reading it order)
|
74
|
+
inventory[certname][:old_serials] << serial
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
[inventory, errored]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Puppetserver
|
2
|
+
module Ca
|
3
|
+
module Utils
|
4
|
+
class SigningDigest
|
5
|
+
|
6
|
+
attr_reader :errors, :digest
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@errors = []
|
10
|
+
if OpenSSL::Digest.const_defined?('SHA256')
|
11
|
+
@digest = OpenSSL::Digest::SHA256.new
|
12
|
+
elsif OpenSSL::Digest.const_defined?('SHA1')
|
13
|
+
@digest = OpenSSL::Digest::SHA1.new
|
14
|
+
elsif OpenSSL::Digest.const_defined?('SHA512')
|
15
|
+
@digest = OpenSSL::Digest::SHA512.new
|
16
|
+
elsif OpenSSL::Digest.const_defined?('SHA384')
|
17
|
+
@digest = OpenSSL::Digest::SHA384.new
|
18
|
+
elsif OpenSSL::Digest.const_defined?('SHA224')
|
19
|
+
@digest = OpenSSL::Digest::SHA224.new
|
20
|
+
else
|
21
|
+
@errors << "Error: No FIPS 140-2 compliant digest algorithm in OpenSSL::Digest"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,170 @@
|
|
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, :cert, :key, :crls, :crl
|
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
|
+
@cert = find_signing_cert
|
17
|
+
@crl = find_leaf_crl
|
18
|
+
|
19
|
+
validate(@certs, @key, @crls)
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_signing_cert
|
23
|
+
return if @key.nil? || @certs.empty?
|
24
|
+
|
25
|
+
signing_cert = @certs.find do |cert|
|
26
|
+
cert.check_private_key(@key)
|
27
|
+
end
|
28
|
+
|
29
|
+
if signing_cert.nil?
|
30
|
+
@errors << "Could not find certificate matching private key"
|
31
|
+
end
|
32
|
+
|
33
|
+
signing_cert
|
34
|
+
end
|
35
|
+
|
36
|
+
# Find a CRL in the chain issued by the signing cert
|
37
|
+
#
|
38
|
+
# @return [OpenSSL::X509::CRL] If a CRL is found.
|
39
|
+
# @return [nil] If no CRL is found.
|
40
|
+
def find_leaf_crl
|
41
|
+
return if @crls.empty? || @cert.nil?
|
42
|
+
|
43
|
+
leaf_crl = @crls.find do |crl|
|
44
|
+
crl.issuer == @cert.subject
|
45
|
+
end
|
46
|
+
|
47
|
+
leaf_crl
|
48
|
+
end
|
49
|
+
|
50
|
+
# Only do as much validation as is possible, assume whoever tried to
|
51
|
+
# load the objects wrote errors about any invalid ones, but that bundle
|
52
|
+
# and chain may be empty arrays and pkey may be nil.
|
53
|
+
def validate(bundle, pkey, chain)
|
54
|
+
if !@crl.nil? && !@cert.nil?
|
55
|
+
validate_crl_and_cert(@crl, @cert)
|
56
|
+
end
|
57
|
+
|
58
|
+
if pkey && !@cert.nil?
|
59
|
+
validate_cert_and_key(pkey, @cert)
|
60
|
+
end
|
61
|
+
|
62
|
+
unless bundle.empty? || @cert.nil? || @crl.nil?
|
63
|
+
validate_full_chain(bundle, chain)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def load_certs(bundle_path)
|
68
|
+
certs, errs = [], []
|
69
|
+
|
70
|
+
bundle_string = File.read(bundle_path)
|
71
|
+
cert_strings = bundle_string.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----/m)
|
72
|
+
cert_strings.each do |cert_string|
|
73
|
+
begin
|
74
|
+
certs << OpenSSL::X509::Certificate.new(cert_string)
|
75
|
+
rescue OpenSSL::X509::CertificateError
|
76
|
+
errs << "Could not parse entry:\n#{cert_string}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
if certs.empty?
|
81
|
+
errs << "Could not detect any certs within #{bundle_path}"
|
82
|
+
end
|
83
|
+
|
84
|
+
unless errs.empty?
|
85
|
+
@errors << "Could not parse #{bundle_path}"
|
86
|
+
@errors += errs
|
87
|
+
end
|
88
|
+
|
89
|
+
return certs
|
90
|
+
end
|
91
|
+
|
92
|
+
def load_key(key_path)
|
93
|
+
begin
|
94
|
+
OpenSSL::PKey.read(File.read(key_path))
|
95
|
+
rescue ArgumentError, OpenSSL::PKey::PKeyError => e
|
96
|
+
@errors << "Could not parse #{key_path}"
|
97
|
+
|
98
|
+
return nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def load_crls(chain_path)
|
103
|
+
errs, crls = [], []
|
104
|
+
|
105
|
+
chain_string = File.read(chain_path)
|
106
|
+
crl_strings = chain_string.scan(/-----BEGIN X509 CRL-----.*?-----END X509 CRL-----/m)
|
107
|
+
crl_strings.map do |crl_string|
|
108
|
+
begin
|
109
|
+
crls << OpenSSL::X509::CRL.new(crl_string)
|
110
|
+
rescue OpenSSL::X509::CRLError
|
111
|
+
errs << "Could not parse entry:\n#{crl_string}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
if crls.empty?
|
116
|
+
errs << "Could not detect any crls within #{chain_path}"
|
117
|
+
end
|
118
|
+
|
119
|
+
unless errs.empty?
|
120
|
+
@errors << "Could not parse #{chain_path}"
|
121
|
+
@errors += errs
|
122
|
+
end
|
123
|
+
|
124
|
+
return crls
|
125
|
+
end
|
126
|
+
|
127
|
+
# Replace the CRL for the signing cert of this loader
|
128
|
+
#
|
129
|
+
# @param new_crl [OpenSSL::X509::CRL]
|
130
|
+
# @return [void]
|
131
|
+
def crl=(new_crl)
|
132
|
+
@crl = new_crl
|
133
|
+
@crls = [new_crl] + @crls.reject {|c| c.issuer == new_crl.issuer }
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate_cert_and_key(key, cert)
|
137
|
+
unless cert.check_private_key(key)
|
138
|
+
@errors << 'Private key and certificate do not match'
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def validate_crl_and_cert(crl, cert)
|
143
|
+
unless crl.issuer == cert.subject
|
144
|
+
@errors << 'Leaf CRL was not issued by leaf certificate'
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# By creating an X509::Store and validating the leaf cert with it we:
|
149
|
+
# - Ensure a full chain of trust (root to leaf) is within the bundle
|
150
|
+
# - If provided, there are CRLs for the CAs
|
151
|
+
# - If provided, no CAs within the chain of trust have been revoked
|
152
|
+
# However this does allow for:
|
153
|
+
# - Additional, ignored, certs and CRLs in the bundle/chain
|
154
|
+
# - certs and CRLs in any order
|
155
|
+
def validate_full_chain(certs, crls)
|
156
|
+
store = OpenSSL::X509::Store.new
|
157
|
+
certs.each {|cert| store.add_cert(cert) }
|
158
|
+
if !crls.empty?
|
159
|
+
store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
|
160
|
+
crls.each {|crl| store.add_crl(crl) }
|
161
|
+
end
|
162
|
+
|
163
|
+
unless store.verify(@cert)
|
164
|
+
@errors << 'Leaf certificate could not be validated'
|
165
|
+
@errors << "Validating cert store returned: #{store.error} - #{store.error_string}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "puppetserver/ca/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "openvoxserver-ca"
|
8
|
+
spec.version = Puppetserver::Ca::VERSION
|
9
|
+
spec.authors = ["OpenVox Project"]
|
10
|
+
spec.email = ["openvox@voxpupuli.org"]
|
11
|
+
spec.license = "Apache-2.0"
|
12
|
+
|
13
|
+
spec.summary = %q{A simple CLI tool for interacting with OpenVox Server's Certificate Authority}
|
14
|
+
spec.homepage = "https://github.com/OpenVoxProject/openvoxserver-ca/"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_runtime_dependency "openfact", [">= 5.0.0", "< 6"]
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", ">= 1.16"
|
26
|
+
spec.add_development_dependency "rake", ">= 12.3.3"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
28
|
+
|
29
|
+
# openvoxserver 7 uses jruby 9.3 which is compatible with MRI ruby 2.6
|
30
|
+
spec.required_ruby_version = '>= 2.6.0'
|
31
|
+
end
|
data/tasks/spec.rake
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
desc 'Run rspec test in sequential order'
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
desc 'Run rspec test in random order'
|
10
|
+
RSpec::Core::RakeTask.new(:spec_random) do |t|
|
11
|
+
t.rspec_opts = '--order random'
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
puts 'Could not load rspec'
|
15
|
+
end
|
data/tasks/vox.rake
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :vox do
|
4
|
+
desc 'Update the version in preparation for a release'
|
5
|
+
task 'version:bump:full', [:version] do |_, args|
|
6
|
+
abort 'You must provide a tag.' if args[:version].nil? || args[:version].empty?
|
7
|
+
version = args[:version]
|
8
|
+
abort "#{version} does not appear to be a valid version string in x.y.z format" unless Gem::Version.correct?(version)
|
9
|
+
|
10
|
+
# Update lib/facter/version.rb and openvox.gemspec
|
11
|
+
puts "Setting version to #{version}"
|
12
|
+
|
13
|
+
data = File.read('lib/puppetserver/ca/version.rb')
|
14
|
+
new_data = data.sub(/VERSION = "\d+\.\d+\.\d+(\.rc\d+)?"/, %(VERSION = "#{version}"))
|
15
|
+
warn 'Failed to update version in lib/facter/version.rb' if data == new_data
|
16
|
+
|
17
|
+
File.write('lib/puppetserver/ca/version.rb', new_data)
|
18
|
+
end
|
19
|
+
end
|