lib/puppet/ssl/certificate_authority.rb in puppet-2.7.5 vs lib/puppet/ssl/certificate_authority.rb in puppet-2.7.6
- old
+ new
@@ -9,10 +9,19 @@
# for us.
# This class mostly just signs certs for us, but
# it can also be seen as a general interface into all of the
# SSL stuff.
class Puppet::SSL::CertificateAuthority
+ # We will only sign extensions on this whitelist, ever. Any CSR with a
+ # requested extension that we don't recognize is rejected, against the risk
+ # that it will introduce some security issue through our ignorance of it.
+ #
+ # Adding an extension to this whitelist simply means we will consider it
+ # further, not that we will always accept a certificate with an extension
+ # requested on this list.
+ RequestExtensionWhitelist = %w{subjectAltName}
+
require 'puppet/ssl/certificate_factory'
require 'puppet/ssl/inventory'
require 'puppet/ssl/certificate_revocation_list'
require 'puppet/ssl/certificate_authority/interface'
require 'puppet/network/authstore'
@@ -31,10 +40,18 @@
synchronize do
@singleton_instance ||= new
end
end
+ class CertificateSigningError < RuntimeError
+ attr_accessor :host
+
+ def initialize(host)
+ @host = host
+ end
+ end
+
def self.ca?
return false unless Puppet[:ca]
return false unless Puppet.run_mode.master?
true
end
@@ -52,11 +69,10 @@
# Create and run an applicator. I wanted to build an interface where you could do
# something like 'ca.apply(:generate).to(:all) but I don't think it's really possible.
def apply(method, options)
raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" unless options[:to]
applier = Interface.new(method, options)
-
applier.apply(self)
end
# If autosign is configured, then autosign all CSRs that match our configuration.
def autosign
@@ -108,33 +124,38 @@
def destroy(name)
Puppet::SSL::Host.destroy(name)
end
# Generate a new certificate.
- def generate(name)
+ def generate(name, options = {})
raise ArgumentError, "A Certificate already exists for #{name}" if Puppet::SSL::Certificate.indirection.find(name)
host = Puppet::SSL::Host.new(name)
- host.generate_certificate_request
+ # Pass on any requested subjectAltName field.
+ san = options[:dns_alt_names]
- sign(name)
+ host = Puppet::SSL::Host.new(name)
+ host.generate_certificate_request(:dns_alt_names => san)
+ sign(name, !!san)
end
# Generate our CA certificate.
def generate_ca_certificate
generate_password unless password?
host.generate_key unless host.key
- # Create a new cert request. We do this
- # specially, because we don't want to actually
- # save the request anywhere.
+ # Create a new cert request. We do this specially, because we don't want
+ # to actually save the request anywhere.
request = Puppet::SSL::CertificateRequest.new(host.name)
+
+ # We deliberately do not put any subjectAltName in here: the CA
+ # certificate absolutely does not need them. --daniel 2011-10-13
request.generate(host.key)
# Create a self-signed certificate.
- @certificate = sign(host.name, :ca, request)
+ @certificate = sign(host.name, false, request)
# And make sure we initialize our CRL.
crl
end
@@ -223,24 +244,38 @@
def setup
generate_ca_certificate unless @host.certificate
end
# Sign a given certificate request.
- def sign(hostname, cert_type = :server, self_signing_csr = nil)
+ def sign(hostname, allow_dns_alt_names = false, self_signing_csr = nil)
# This is a self-signed certificate
if self_signing_csr
+ # # This is a self-signed certificate, which is for the CA. Since this
+ # # forces the certificate to be self-signed, anyone who manages to trick
+ # # the system into going through this path gets a certificate they could
+ # # generate anyway. There should be no security risk from that.
csr = self_signing_csr
+ cert_type = :ca
issuer = csr.content
else
+ allow_dns_alt_names = true if hostname == Puppet[:certname].downcase
unless csr = Puppet::SSL::CertificateRequest.indirection.find(hostname)
raise ArgumentError, "Could not find certificate request for #{hostname}"
end
+
+ cert_type = :server
issuer = host.certificate.content
+
+ # Make sure that the CSR conforms to our internal signing policies.
+ # This will raise if the CSR doesn't conform, but just in case...
+ check_internal_signing_policies(hostname, csr, allow_dns_alt_names) or
+ raise CertificateSigningError.new(hostname), "CSR had an unknown failure checking internal signing policies, will not sign!"
end
cert = Puppet::SSL::Certificate.new(hostname)
- cert.content = Puppet::SSL::CertificateFactory.new(cert_type, csr.content, issuer, next_serial).result
+ cert.content = Puppet::SSL::CertificateFactory.
+ build(cert_type, csr, issuer, next_serial)
cert.content.sign(host.key.content, OpenSSL::Digest::SHA1.new)
Puppet.notice "Signed certificate request for #{hostname}"
# Add the cert to the inventory before we save it, since
@@ -254,9 +289,50 @@
# And remove the CSR if this wasn't self signed.
Puppet::SSL::CertificateRequest.indirection.destroy(csr.name) unless self_signing_csr
cert
+ end
+
+ def check_internal_signing_policies(hostname, csr, allow_dns_alt_names)
+ # Reject unknown request extensions.
+ unknown_req = csr.request_extensions.
+ reject {|x| RequestExtensionWhitelist.include? x["oid"] }
+
+ if unknown_req and not unknown_req.empty?
+ names = unknown_req.map {|x| x["oid"] }.sort.uniq.join(", ")
+ raise CertificateSigningError.new(hostname), "CSR has request extensions that are not permitted: #{names}"
+ end
+
+ # Wildcards: we don't allow 'em at any point.
+ #
+ # The stringification here makes the content visible, and saves us having
+ # to scrobble through the content of the CSR subject field to make sure it
+ # is what we expect where we expect it.
+ if csr.content.subject.to_s.include? '*'
+ raise CertificateSigningError.new(hostname), "CSR subject contains a wildcard, which is not allowed: #{csr.content.subject.to_s}"
+ end
+
+ unless csr.subject_alt_names.empty?
+ # If you alt names are allowed, they are required. Otherwise they are
+ # disallowed. Self-signed certs are implicitly trusted, however.
+ unless allow_dns_alt_names
+ 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."
+ end
+
+ # If subjectAltNames are present, validate that they are only for DNS
+ # labels, not any other kind.
+ unless csr.subject_alt_names.all? {|x| x =~ /^DNS:/ }
+ 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."
+ end
+
+ # Check for wildcards in the subjectAltName fields too.
+ if csr.subject_alt_names.any? {|x| x.include? '*' }
+ 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."
+ end
+ end
+
+ return true # good enough for us!
end
# Verify a given host's certificate.
def verify(name)
unless cert = Puppet::SSL::Certificate.indirection.find(name)