diff options
Diffstat (limited to 'lib/puppet')
-rw-r--r-- | lib/puppet/ssl/certificate_authority.rb | 151 | ||||
-rw-r--r-- | lib/puppet/ssl/certificate_factory.rb | 134 | ||||
-rw-r--r-- | lib/puppet/ssl/host.rb | 104 | ||||
-rw-r--r-- | lib/puppet/ssl/key.rb | 15 |
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 |