summaryrefslogtreecommitdiffstats
path: root/lib/puppet
diff options
context:
space:
mode:
Diffstat (limited to 'lib/puppet')
-rw-r--r--lib/puppet/ssl/certificate_authority.rb151
-rw-r--r--lib/puppet/ssl/certificate_factory.rb134
-rw-r--r--lib/puppet/ssl/host.rb104
-rw-r--r--lib/puppet/ssl/key.rb15
4 files changed, 356 insertions, 48 deletions
diff --git a/lib/puppet/ssl/certificate_authority.rb b/lib/puppet/ssl/certificate_authority.rb
index 63bce6088..18f881ae3 100644
--- a/lib/puppet/ssl/certificate_authority.rb
+++ b/lib/puppet/ssl/certificate_authority.rb
@@ -1,5 +1,150 @@
-require 'puppet/ssl'
+require 'puppet/ssl/host'
-# The class that knows how to sign certificates.
-class Puppet::SSL::CertificateAuthority
+# The class that knows how to sign certificates. It's just a
+# special case of the SSL::Host -- it's got a sign method,
+# and it reads its info from a different location.
+class Puppet::SSL::CertificateAuthority < Puppet::SSL::Host
+ require 'puppet/ssl/certificate_factory'
+
+ # Provide the path to our password, and read our special ca key.
+ def read_key
+ return nil unless FileTest.exist?(Puppet[:cakey])
+
+ key = Puppet::SSL::Key.new(:ca)
+ key.password_file = Puppet[:capass]
+ key.read(Puppet[:cakey])
+
+ return key
+ end
+
+ # Generate and write the key out.
+ def generate_key
+ @key = Key.new(name)
+ @key.generate
+ Puppet.settings.write(:cacert) do |f|
+ f.print @key.to_s
+ end
+ true
+ end
+
+ # Read the special path to our key.
+ def read_certificate
+ return nil unless FileTest.exist?(Puppet[:cacert])
+ cert = Puppet::SSL::Certificate.new(name)
+ cert.read(Puppet[:cacert])
+
+ return cert
+ end
+
+ # The CA creates a self-signed certificate, rather than relying
+ # on someone else to do the work.
+ def generate_certificate
+ request = CertificateRequest.new(name)
+ request.generate(key)
+
+ # Create a self-signed certificate.
+ @certificate = sign(request, :ca, true)
+
+ Puppet.settings.write(:cacert) do |f|
+ f.print @certificate.to_s
+ end
+
+ return true
+ end
+
+ def initialize
+ # Always name the ca after the host we're running on.
+ super(Puppet[:certname])
+
+ setup_ca
+ end
+
+ # Sign a given certificate request.
+ def sign(host, cert_type = :service, self_signed = false)
+ # This is only used by the CA for self-signing.
+ if host.is_a?(Puppet::SSL::CertificateRequest)
+ csr = host
+ host = csr.name
+ issuer = csr.content
+ else
+ unless csr = Puppet::SSL::CertificateRequest.find(host, :in => :ca_file)
+ raise Puppet::Error, "Could not find certificate request for %s" % host
+ end
+ issuer = certificate.content
+ end
+
+ raise Puppet::Error, "Certificate request for #{host} does not match its own public key" unless csr.content.verify(csr.content.public_key)
+ raise ArgumentError, "Cannot find CA certificate; cannot sign certificate for %s" % host unless self_signed or certificate
+
+ cert = Puppet::SSL::Certificate.new(host)
+ cert.content = Puppet::SSL::CertificateFactory.new(cert_type, csr.content, issuer, next_serial).result
+
+ # Save the now-signed cert, unless it's a self-signed cert, since we
+ # assume it goes somewhere else.
+ cert.save(:in => :ca_file) unless self_signed
+ end
+
+ private
+
+ # Do all of the initialization necessary to set up our
+ # ca.
+ def setup_ca
+ generate_key unless key
+
+ # Make sure we've got a password protecting our private key.
+ generate_password unless read_password
+
+ # And then make sure we've got the whole kaboodle. This will
+ # create a self-signed CA certificate if we don't already have one,
+ # and it will just read it in if we do.
+ generate_certificate unless certificate
+ end
+
+ # Generate a new password for the CA.
+ def generate_password
+ pass = ""
+ 20.times { pass += (rand(74) + 48).chr }
+
+ begin
+ Puppet.settings.write(:capass) { |f| f.print pass }
+ rescue Errno::EACCES => detail
+ raise Puppet::Error, "Could not write CA password: %s" % detail.to_s
+ end
+
+ @password = pass
+
+ return pass
+ end
+
+ # Read the next serial from the serial file, and increment the
+ # file so this one is considered used.
+ def next_serial
+ serial = nil
+ Puppet.settings.readwritelock(:serial) { |f|
+ if FileTest.exist?(Puppet[:serial])
+ serial = File.read(Puppet.settings[:serial]).chomp.hex
+ else
+ serial = 0x0
+ end
+
+ # We store the next valid serial, not the one we just used.
+ f << "%04X" % (serial + 1)
+ }
+
+ return serial
+ end
+
+ # Get the CA password.
+ def read_password
+ unless defined?(@password) and @password
+ path = Puppet[:capass]
+ return nil unless FileTest.exist?(path)
+
+ raise(Puppet::Error, "Could not read CA passfile %s" % path) unless FileTest.readable?(path)
+
+ @password = File.read(path)
+ end
+
+ @password
+ end
end
diff --git a/lib/puppet/ssl/certificate_factory.rb b/lib/puppet/ssl/certificate_factory.rb
new file mode 100644
index 000000000..abdeb8a2c
--- /dev/null
+++ b/lib/puppet/ssl/certificate_factory.rb
@@ -0,0 +1,134 @@
+# The tedious class that does all the manipulations to the
+# certificate to correctly sign it. Yay.
+class Puppet::SSL::CertificateFactory
+ # How we convert from various units to the required seconds.
+ UNITMAP = {
+ "y" => 365 * 24 * 60 * 60,
+ "d" => 24 * 60 * 60,
+ "h" => 60 * 60,
+ "s" => 1
+ }
+
+ def initialize(cert_type, csr, issuer, serial)
+ @cert_type, @csr, @issuer = cert_type, csr, issuer
+
+ @name = @csr.subject
+
+ @cert = OpenSSL::X509::Certificate.new
+
+ @cert.version = 2 # X509v3
+ @cert.subject = csr.subject
+ @cert.issuer = issuer.subject
+ @cert.public_key = csr.public_key
+ @cert.serial = serial
+
+ build_extensions()
+
+ set_ttl
+ end
+
+ private
+
+ # This is pretty ugly, but I'm not really sure it's even possible to do
+ # it any other way.
+ def build_extensions
+ @ef = OpenSSL::X509::ExtensionFactory.new
+
+ @ef.subject_certificate = @cert
+
+ if @issuer.is_a?(OpenSSL::X509::Request) # It's a self-signed cert
+ @ef.issuer_certificate = @cert
+ else
+ @ef.issuer_certificate = @issuer
+ end
+
+ @subject_alt_name = []
+ @key_usage = nil
+ @ext_key_usage = nil
+ @extensions = []
+
+ method = "add_#{@cert_type.to_s}_extensions"
+
+ raise ArgumentError, "%s is an invalid certificate type" % @cert_type unless respond_to?(method)
+
+ send(method)
+
+ @extensions << @ef.create_extension("nsComment", "Puppet Ruby/OpenSSL Generated Certificate")
+ @extensions << @ef.create_extension("basicConstraints", @basic_constraint, true)
+ @extensions << @ef.create_extension("subjectKeyIdentifier", "hash")
+ @extensions << @ef.create_extension("keyUsage", @key_usage.join(",")) if @key_usage
+ @extensions << @ef.create_extension("extendedKeyUsage", @ext_key_usage.join(",")) if @ext_key_usage
+ @extensions << @ef.create_extension("subjectAltName", @subject_alt_name.join(",")) if ! @subject_alt_name.empty?
+
+ @cert.extensions = @extensions
+
+ # for some reason this _must_ be the last extension added
+ @extensions << ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always") if @cert_type == :ca
+ end
+
+ # TTL for new certificates in seconds. If config param :ca_ttl is set,
+ # use that, otherwise use :ca_days for backwards compatibility
+ def ttl
+ ttl = Puppet.settings[:ca_ttl]
+
+ return ttl unless ttl.is_a?(String)
+
+ raise ArgumentError, "Invalid ca_ttl #{ttl}" unless ttl =~ /^(\d+)(y|d|h|s)$/
+
+ return $1.to_i * UNITMAP[$2]
+ end
+
+ def set_ttl
+ # Make the certificate valid as of yesterday, because
+ # so many people's clocks are out of sync.
+ from = Time.now - (60*60*24)
+ @cert.not_before = from
+ @cert.not_after = from + ttl
+ end
+
+ # Woot! We're a CA.
+ def add_ca_extensions
+ @basic_constraint = "CA:TRUE"
+ @key_usage = %w{cRLSign keyCertSign}
+ end
+
+ # We're a terminal CA, probably not self-signed.
+ def add_terminalsubca_extensions
+ @basic_constraint = "CA:TRUE,pathlen:0"
+ @key_usage = %w{cRLSign keyCertSign}
+ end
+
+ # We're a normal server.
+ def add_server_extensions
+ @basic_constraint = "CA:FALSE"
+ dnsnames = Puppet[:certdnsnames]
+ name = @name.to_s.sub(%r{/CN=},'')
+ if dnsnames != ""
+ dnsnames.split(':').each { |d| subject_alt_name << 'DNS:' + d }
+ @subject_alt_name << 'DNS:' + name # Add the fqdn as an alias
+ elsif name == Facter.value(:fqdn) # we're a CA server, and thus probably the server
+ @subject_alt_name << 'DNS:' + "puppet" # Add 'puppet' as an alias
+ @subject_alt_name << 'DNS:' + name # Add the fqdn as an alias
+ @subject_alt_name << 'DNS:' + name.sub(/^[^.]+./, "puppet.") # add puppet.domain as an alias
+ end
+ @key_usage = %w{digitalSignature keyEncipherment}
+ @ext_key_usage = %w{serverAuth clientAuth emailProtection}
+ end
+
+ # Um, no idea.
+ def add_ocsp_extensions
+ @basic_constraint = "CA:FALSE"
+ @key_usage = %w{nonRepudiation digitalSignature}
+ @ext_key_usage = %w{serverAuth OCSPSigning}
+ end
+
+ # Normal client.
+ def add_client_extensions
+ @basic_constraint = "CA:FALSE"
+ @key_usage = %w{nonRepudiation digitalSignature keyEncipherment}
+ @ext_key_usage = %w{clientAuth emailProtection}
+
+ @extensions << @ef.create_extension("nsCertType", "client,email")
+ end
+end
+
diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb
index bae33b23e..a50355509 100644
--- a/lib/puppet/ssl/host.rb
+++ b/lib/puppet/ssl/host.rb
@@ -2,6 +2,7 @@ require 'puppet/ssl'
require 'puppet/ssl/key'
require 'puppet/ssl/certificate'
require 'puppet/ssl/certificate_request'
+require 'puppet/util/constant_inflector'
# The class that manages all aspects of our SSL certificates --
# private keys, public keys, requests, etc.
@@ -11,41 +12,68 @@ class Puppet::SSL::Host
CertificateRequest = Puppet::SSL::CertificateRequest
Certificate = Puppet::SSL::Certificate
- attr_reader :name
+ extend Puppet::Util::ConstantInflector
+ attr_reader :name
attr_accessor :ca
- # Is this a ca host, meaning that all of its files go in the CA collections?
- def ca?
- ca
- end
+ # A bit of metaprogramming that we use to define all of
+ # the methods for managing our ssl-related files.
+ def self.manage_file(name, &block)
+ var = "@%s" % name
- # Read our cert if necessary, fail if we can't find it (since it should
- # be created by someone else and returned through 'find').
- def certificate
- unless @certificate ||= Certificate.find(name)
- return nil
+ maker = "generate_%s" % name
+ reader = "read_%s" % name
+
+ classname = file2constant(name.to_s)
+
+ begin
+ klass = const_get(classname)
+ rescue
+ raise Puppet::DevError, "Cannot map %s to a valid constant" % name
end
- @certificate.content
- end
- # Read or create, then return, our certificate request.
- def certificate_request
- unless @certificate_request ||= CertificateRequest.find(name)
- return nil
+ # Define the method that creates it.
+ define_method(maker, &block)
+
+ # Define the reading method.
+ define_method(reader) do
+ klass.find(self.name)
end
- @certificate_request.content
- end
- # Remove all traces of this ssl host
- def destroy
- [key, certificate, certificate_request].each do |instance|
- instance.class.destroy(instance) if instance
+ # Define the overall method, which just calls the reader and maker
+ # as appropriate.
+ define_method(name) do
+ unless cert = instance_variable_get(var)
+ return nil unless cert = send(reader)
+ instance_variable_set(var, cert)
+ end
+ cert.content
end
end
- # Request a signed certificate from a ca, if we can find one.
- def generate_certificate
+ # This is the private key; we can create it from scratch
+ # with no inputs.
+ manage_file :key do
+ @key = Key.new(name)
+ @key.generate
+ @key.save
+ true
+ end
+
+ # Our certificate request requires the key but that's all.
+ manage_file :certificate_request do
+ generate_key unless key
+ @certificate_request = CertificateRequest.new(name)
+ @certificate_request.generate(key)
+ @certificate_request.save
+ return true
+ end
+
+ # Our certificate itself might not successfully "generate", since
+ # that generation is actually accomplished by a CA signing the
+ # stored CSR.
+ manage_file :certificate do
generate_certificate_request unless certificate_request
@certificate = Certificate.new(name)
@@ -57,30 +85,16 @@ class Puppet::SSL::Host
end
end
- # Generate and save a new certificate request.
- def generate_certificate_request
- generate_key unless key
- @certificate_request = CertificateRequest.new(name)
- @certificate_request.generate(key)
- @certificate_request.save
- return true
- end
-
- # Generate and save a new key.
- def generate_key
- @key = Key.new(name)
- @key.generate
- @key.save
- return true
+ # Is this a ca host, meaning that all of its files go in the CA collections?
+ def ca?
+ ca
end
- # Read or create, then return, our key. The public key is part
- # of the private key. We
- def key
- unless @key ||= Key.find(name)
- return nil
+ # Remove all traces of this ssl host
+ def destroy
+ [key, certificate, certificate_request].each do |instance|
+ instance.class.destroy(instance) if instance
end
- @key.content
end
def initialize(name)
diff --git a/lib/puppet/ssl/key.rb b/lib/puppet/ssl/key.rb
index a9c8717f8..b8943a776 100644
--- a/lib/puppet/ssl/key.rb
+++ b/lib/puppet/ssl/key.rb
@@ -8,9 +8,24 @@ class Puppet::SSL::Key < Puppet::SSL::Base
extend Puppet::Indirector
indirects :key, :terminus_class => :file
+ attr_accessor :password_file
+
# Knows how to create keys with our system defaults.
def generate
Puppet.info "Creating a new SSL key for %s" % name
@content = OpenSSL::PKey::RSA.new(Puppet[:keylength])
end
+
+ # Optionally support specifying a password file.
+ def read(path)
+ return super unless password_file
+
+ begin
+ password = ::File.read(password_file)
+ rescue => detail
+ raise Puppet::Error, "Could not read password for %s: %s" % [name, detail]
+ end
+
+ @content = wrapped_class.new(::File.read(path), password)
+ end
end