2
require 'puppet/ssl/host'
3
require 'puppet/ssl/certificate_request'
5
# The class that knows how to sign certificates. It creates
6
# a 'special' SSL::Host whose name is 'ca', thus indicating
7
# that, well, it's the CA. There's some magic in the
8
# indirector/ssl_file terminus base class that does that
10
# This class mostly just signs certs for us, but
11
# it can also be seen as a general interface into all of the
13
class Puppet::SSL::CertificateAuthority
14
# We will only sign extensions on this whitelist, ever. Any CSR with a
15
# requested extension that we don't recognize is rejected, against the risk
16
# that it will introduce some security issue through our ignorance of it.
18
# Adding an extension to this whitelist simply means we will consider it
19
# further, not that we will always accept a certificate with an extension
20
# requested on this list.
21
RequestExtensionWhitelist = %w{subjectAltName}
23
require 'puppet/ssl/certificate_factory'
24
require 'puppet/ssl/inventory'
25
require 'puppet/ssl/certificate_revocation_list'
26
require 'puppet/ssl/certificate_authority/interface'
27
require 'puppet/network/authstore'
31
class CertificateVerificationError < RuntimeError
32
attr_accessor :error_code
39
def self.singleton_instance
41
@singleton_instance ||= new
45
class CertificateSigningError < RuntimeError
54
return false unless Puppet[:ca]
55
return false unless Puppet.run_mode.master?
59
# If this process can function as a CA, then return a singleton
67
attr_reader :name, :host
69
# Create and run an applicator. I wanted to build an interface where you could do
70
# something like 'ca.apply(:generate).to(:all) but I don't think it's really possible.
71
def apply(method, options)
72
raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" unless options[:to]
73
applier = Interface.new(method, options)
77
# If autosign is configured, then autosign all CSRs that match our configuration.
79
return unless auto = autosign?
82
store = autosign_store(auto) if auto != true
84
Puppet::SSL::CertificateRequest.indirection.search("*").each do |csr|
85
sign(csr.name) if auto == true or store.allowed?(csr.name, "127.1.1.1")
89
# Do we autosign? This returns true, false, or a filename.
91
auto = Puppet[:autosign]
92
return false if ['false', false].include?(auto)
93
return true if ['true', true].include?(auto)
95
raise ArgumentError, "The autosign configuration '#{auto}' must be a fully qualified file" unless auto =~ /^\//
96
FileTest.exist?(auto) && auto
99
# Create an AuthStore for autosigning.
100
def autosign_store(file)
101
auth = Puppet::Network::AuthStore.new
102
File.readlines(file).each do |line|
103
next if line =~ /^\s*#/
104
next if line =~ /^\s*$/
105
auth.allow(line.chomp)
111
# Retrieve (or create, if necessary) the certificate revocation list.
113
unless defined?(@crl)
114
unless @crl = Puppet::SSL::CertificateRevocationList.indirection.find(Puppet::SSL::CA_NAME)
115
@crl = Puppet::SSL::CertificateRevocationList.new(Puppet::SSL::CA_NAME)
116
@crl.generate(host.certificate.content, host.key.content)
117
Puppet::SSL::CertificateRevocationList.indirection.save(@crl)
123
# Delegate this to our Host class.
125
Puppet::SSL::Host.destroy(name)
128
# Generate a new certificate.
129
def generate(name, options = {})
130
raise ArgumentError, "A Certificate already exists for #{name}" if Puppet::SSL::Certificate.indirection.find(name)
131
host = Puppet::SSL::Host.new(name)
133
# Pass on any requested subjectAltName field.
134
san = options[:dns_alt_names]
136
host = Puppet::SSL::Host.new(name)
137
host.generate_certificate_request(:dns_alt_names => san)
141
# Generate our CA certificate.
142
def generate_ca_certificate
143
generate_password unless password?
145
host.generate_key unless host.key
147
# Create a new cert request. We do this specially, because we don't want
148
# to actually save the request anywhere.
149
request = Puppet::SSL::CertificateRequest.new(host.name)
151
# We deliberately do not put any subjectAltName in here: the CA
152
# certificate absolutely does not need them. --daniel 2011-10-13
153
request.generate(host.key)
155
# Create a self-signed certificate.
156
@certificate = sign(host.name, false, request)
158
# And make sure we initialize our CRL.
163
Puppet.settings.use :main, :ssl, :ca
165
@name = Puppet[:certname]
167
@host = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name)
172
# Retrieve (or create, if necessary) our inventory manager.
174
@inventory ||= Puppet::SSL::Inventory.new
177
# Generate a new password for the CA.
178
def generate_password
180
20.times { pass += (rand(74) + 48).chr }
183
Puppet.settings.write(:capass) { |f| f.print pass }
184
rescue Errno::EACCES => detail
185
raise Puppet::Error, "Could not write CA password: #{detail}"
193
# List all signed certificates.
195
Puppet::SSL::Certificate.indirection.search("*").collect { |c| c.name }
198
# Read the next serial from the serial file, and increment the
199
# file so this one is considered used.
203
# This is slightly odd. If the file doesn't exist, our readwritelock creates
204
# it, but with a mode we can't actually read in some cases. So, use
205
# a default before the lock.
206
serial = 0x1 unless FileTest.exist?(Puppet[:serial])
208
Puppet.settings.readwritelock(:serial) { |f|
209
serial ||= File.read(Puppet.settings[:serial]).chomp.hex if FileTest.exist?(Puppet[:serial])
211
# We store the next valid serial, not the one we just used.
212
f << "%04X" % (serial + 1)
218
# Does the password file exist?
220
FileTest.exist? Puppet[:capass]
223
# Print a given host's certificate as text.
225
(cert = Puppet::SSL::Certificate.indirection.find(name)) ? cert.to_text : nil
228
# Revoke a given certificate.
230
raise ArgumentError, "Cannot revoke certificates when the CRL is disabled" unless crl
232
if cert = Puppet::SSL::Certificate.indirection.find(name)
233
serial = cert.content.serial
234
elsif ! serial = inventory.serial(name)
235
raise ArgumentError, "Could not find a serial number for #{name}"
237
crl.revoke(serial, host.key.content)
240
# This initializes our CA so it actually works. This should be a private
241
# method, except that you can't any-instance stub private methods, which is
242
# *awesome*. This method only really exists to provide a stub-point during
245
generate_ca_certificate unless @host.certificate
248
# Sign a given certificate request.
249
def sign(hostname, allow_dns_alt_names = false, self_signing_csr = nil)
250
# This is a self-signed certificate
252
# # This is a self-signed certificate, which is for the CA. Since this
253
# # forces the certificate to be self-signed, anyone who manages to trick
254
# # the system into going through this path gets a certificate they could
255
# # generate anyway. There should be no security risk from that.
256
csr = self_signing_csr
260
allow_dns_alt_names = true if hostname == Puppet[:certname].downcase
261
unless csr = Puppet::SSL::CertificateRequest.indirection.find(hostname)
262
raise ArgumentError, "Could not find certificate request for #{hostname}"
266
issuer = host.certificate.content
268
# Make sure that the CSR conforms to our internal signing policies.
269
# This will raise if the CSR doesn't conform, but just in case...
270
check_internal_signing_policies(hostname, csr, allow_dns_alt_names) or
271
raise CertificateSigningError.new(hostname), "CSR had an unknown failure checking internal signing policies, will not sign!"
274
cert = Puppet::SSL::Certificate.new(hostname)
275
cert.content = Puppet::SSL::CertificateFactory.
276
build(cert_type, csr, issuer, next_serial)
277
cert.content.sign(host.key.content, OpenSSL::Digest::SHA1.new)
279
Puppet.notice "Signed certificate request for #{hostname}"
281
# Add the cert to the inventory before we save it, since
282
# otherwise we could end up with it being duplicated, if
283
# this is the first time we build the inventory file.
286
# Save the now-signed cert. This should get routed correctly depending
287
# on the certificate type.
288
Puppet::SSL::Certificate.indirection.save(cert)
290
# And remove the CSR if this wasn't self signed.
291
Puppet::SSL::CertificateRequest.indirection.destroy(csr.name) unless self_signing_csr
296
def check_internal_signing_policies(hostname, csr, allow_dns_alt_names)
297
# Reject unknown request extensions.
298
unknown_req = csr.request_extensions.
299
reject {|x| RequestExtensionWhitelist.include? x["oid"] }
301
if unknown_req and not unknown_req.empty?
302
names = unknown_req.map {|x| x["oid"] }.sort.uniq.join(", ")
303
raise CertificateSigningError.new(hostname), "CSR has request extensions that are not permitted: #{names}"
306
# Wildcards: we don't allow 'em at any point.
308
# The stringification here makes the content visible, and saves us having
309
# to scrobble through the content of the CSR subject field to make sure it
310
# is what we expect where we expect it.
311
if csr.content.subject.to_s.include? '*'
312
raise CertificateSigningError.new(hostname), "CSR subject contains a wildcard, which is not allowed: #{csr.content.subject.to_s}"
315
unless csr.subject_alt_names.empty?
316
# If you alt names are allowed, they are required. Otherwise they are
317
# disallowed. Self-signed certs are implicitly trusted, however.
318
unless allow_dns_alt_names
319
raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains subject alternative names (#{csr.subject_alt_names.join(', ')}), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{csr.name}` to sign this request."
322
# If subjectAltNames are present, validate that they are only for DNS
323
# labels, not any other kind.
324
unless csr.subject_alt_names.all? {|x| x =~ /^DNS:/ }
325
raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains a subjectAltName outside the DNS label space: #{csr.subject_alt_names.join(', ')}. To continue, this CSR needs to be cleaned."
328
# Check for wildcards in the subjectAltName fields too.
329
if csr.subject_alt_names.any? {|x| x.include? '*' }
330
raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' subjectAltName contains a wildcard, which is not allowed: #{csr.subject_alt_names.join(', ')} To continue, this CSR needs to be cleaned."
334
return true # good enough for us!
337
# Verify a given host's certificate.
339
unless cert = Puppet::SSL::Certificate.indirection.find(name)
340
raise ArgumentError, "Could not find a certificate for #{name}"
342
store = OpenSSL::X509::Store.new
343
store.add_file Puppet[:cacert]
344
store.add_crl crl.content if self.crl
345
store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
346
store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK if Puppet.settings[:certificate_revocation]
348
raise CertificateVerificationError.new(store.error), store.error_string unless store.verify(cert.content)
351
def fingerprint(name, md = :MD5)
352
unless cert = Puppet::SSL::Certificate.indirection.find(name) || Puppet::SSL::CertificateRequest.indirection.find(name)
353
raise ArgumentError, "Could not find a certificate or csr for #{name}"
358
# List the waiting certificate requests.
360
Puppet::SSL::CertificateRequest.indirection.search("*").collect { |r| r.name }