summaryrefslogtreecommitdiffstats
path: root/lib/puppet/ssl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/puppet/ssl')
-rw-r--r--lib/puppet/ssl/base.rb51
-rw-r--r--lib/puppet/ssl/certificate.rb19
-rw-r--r--lib/puppet/ssl/certificate_authority.rb285
-rw-r--r--lib/puppet/ssl/certificate_authority/interface.rb110
-rw-r--r--lib/puppet/ssl/certificate_factory.rb145
-rw-r--r--lib/puppet/ssl/certificate_request.rb36
-rw-r--r--lib/puppet/ssl/certificate_revocation_list.rb72
-rw-r--r--lib/puppet/ssl/host.rb185
-rw-r--r--lib/puppet/ssl/inventory.rb52
-rw-r--r--lib/puppet/ssl/key.rb50
10 files changed, 1005 insertions, 0 deletions
diff --git a/lib/puppet/ssl/base.rb b/lib/puppet/ssl/base.rb
new file mode 100644
index 000000000..80bfcae84
--- /dev/null
+++ b/lib/puppet/ssl/base.rb
@@ -0,0 +1,51 @@
+require 'puppet/ssl'
+
+# The base class for wrapping SSL instances.
+class Puppet::SSL::Base
+ def self.wraps(klass)
+ @wrapped_class = klass
+ end
+
+ def self.wrapped_class
+ raise(Puppet::DevError, "%s has not declared what class it wraps" % self) unless defined?(@wrapped_class)
+ @wrapped_class
+ end
+
+ attr_accessor :name, :content
+
+ # Is this file for the CA?
+ def ca?
+ name == Puppet::SSL::Host.ca_name
+ end
+
+ def generate
+ raise Puppet::DevError, "%s did not override 'generate'" % self.class
+ end
+
+ def initialize(name)
+ @name = name
+ end
+
+ # Read content from disk appropriately.
+ def read(path)
+ @content = wrapped_class.new(File.read(path))
+ end
+
+ # Convert our thing to pem.
+ def to_s
+ return "" unless content
+ content.to_pem
+ end
+
+ # Provide the full text of the thing we're dealing with.
+ def to_text
+ return "" unless content
+ content.to_text
+ end
+
+ private
+
+ def wrapped_class
+ self.class.wrapped_class
+ end
+end
diff --git a/lib/puppet/ssl/certificate.rb b/lib/puppet/ssl/certificate.rb
new file mode 100644
index 000000000..16af85d06
--- /dev/null
+++ b/lib/puppet/ssl/certificate.rb
@@ -0,0 +1,19 @@
+require 'puppet/ssl/base'
+
+# Manage certificates themselves. This class has no
+# 'generate' method because the CA is responsible
+# for turning CSRs into certificates; we can only
+# retrieve them from the CA (or not, as is often
+# the case).
+class Puppet::SSL::Certificate < Puppet::SSL::Base
+ # This is defined from the base class
+ wraps OpenSSL::X509::Certificate
+
+ extend Puppet::Indirector
+ indirects :certificate, :terminus_class => :file
+
+ def expiration
+ return nil unless content
+ return content.not_after
+ end
+end
diff --git a/lib/puppet/ssl/certificate_authority.rb b/lib/puppet/ssl/certificate_authority.rb
new file mode 100644
index 000000000..6947af11c
--- /dev/null
+++ b/lib/puppet/ssl/certificate_authority.rb
@@ -0,0 +1,285 @@
+require 'puppet/ssl/host'
+require 'puppet/ssl/certificate_request'
+require 'puppet/util/cacher'
+
+# The class that knows how to sign certificates. It creates
+# a 'special' SSL::Host whose name is 'ca', thus indicating
+# that, well, it's the CA. There's some magic in the
+# indirector/ssl_file terminus base class that does that
+# 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
+ require 'puppet/ssl/certificate_factory'
+ require 'puppet/ssl/inventory'
+ require 'puppet/ssl/certificate_revocation_list'
+
+ require 'puppet/ssl/certificate_authority/interface'
+
+ extend Puppet::Util::Cacher
+
+ def self.ca?
+ return false unless Puppet[:ca]
+ return false unless Puppet[:name] == "puppetmasterd"
+ return true
+ end
+
+ # If this process can function as a CA, then return a singleton
+ # instance.
+ def self.instance
+ return nil unless ca?
+
+ attr_cache(:instance) { new }
+ end
+
+ attr_reader :name, :host
+
+ # 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)
+ unless options[:to]
+ raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all"
+ end
+ applier = Interface.new(method, options[:to])
+
+ applier.apply(self)
+ end
+
+ # If autosign is configured, then autosign all CSRs that match our configuration.
+ def autosign
+ return unless auto = autosign?
+
+ store = nil
+ if auto != true
+ store = autosign_store(auto)
+ end
+
+ Puppet::SSL::CertificateRequest.search("*").each do |csr|
+ sign(csr.name) if auto == true or store.allowed?(csr.name, "127.1.1.1")
+ end
+ end
+
+ # Do we autosign? This returns true, false, or a filename.
+ def autosign?
+ auto = Puppet[:autosign]
+ return false if ['false', false].include?(auto)
+ return true if ['true', true].include?(auto)
+
+ raise ArgumentError, "The autosign configuration '%s' must be a fully qualified file" % auto unless auto =~ /^\//
+ if FileTest.exist?(auto)
+ return auto
+ else
+ return false
+ end
+ end
+
+ # Create an AuthStore for autosigning.
+ def autosign_store(file)
+ auth = Puppet::Network::AuthStore.new
+ File.readlines(file).each do |line|
+ next if line =~ /^\s*#/
+ next if line =~ /^\s*$/
+ auth.allow(line.chomp)
+ end
+
+ auth
+ end
+
+ # Retrieve (or create, if necessary) the certificate revocation list.
+ def crl
+ unless defined?(@crl)
+ unless @crl = Puppet::SSL::CertificateRevocationList.find("ca")
+ @crl = Puppet::SSL::CertificateRevocationList.new("ca")
+ @crl.generate(host.certificate.content, host.key.content)
+ @crl.save
+ end
+ end
+ @crl
+ end
+
+ # Delegate this to our Host class.
+ def destroy(name)
+ Puppet::SSL::Host.destroy(name)
+ end
+
+ # Generate a new certificate.
+ def generate(name)
+ raise ArgumentError, "A Certificate already exists for %s" % name if Puppet::SSL::Certificate.find(name)
+ host = Puppet::SSL::Host.new(name)
+
+ host.generate_certificate_request
+
+ sign(name)
+ 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.
+ request = Puppet::SSL::CertificateRequest.new(host.name)
+ request.generate(host.key)
+
+ # Create a self-signed certificate.
+ @certificate = sign(host.name, :ca, request)
+
+ # And make sure we initialize our CRL.
+ crl()
+ end
+
+ def initialize
+ Puppet.settings.use :main, :ssl, :ca
+
+ @name = Puppet[:certname]
+
+ @host = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name)
+
+ setup()
+ end
+
+ # Retrieve (or create, if necessary) our inventory manager.
+ def inventory
+ unless defined?(@inventory)
+ @inventory = Puppet::SSL::Inventory.new
+ end
+ @inventory
+ 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
+
+ # List all signed certificates.
+ def list
+ Puppet::SSL::Certificate.search("*").collect { |c| c.name }
+ end
+
+ # Read the next serial from the serial file, and increment the
+ # file so this one is considered used.
+ def next_serial
+ serial = nil
+
+ # This is slightly odd. If the file doesn't exist, our readwritelock creates
+ # it, but with a mode we can't actually read in some cases. So, use
+ # a default before the lock.
+ unless FileTest.exist?(Puppet[:serial])
+ serial = 0x0
+ end
+
+ Puppet.settings.readwritelock(:serial) { |f|
+ if FileTest.exist?(Puppet[:serial])
+ serial ||= File.read(Puppet.settings[:serial]).chomp.hex
+ end
+
+ # We store the next valid serial, not the one we just used.
+ f << "%04X" % (serial + 1)
+ }
+
+ return serial
+ end
+
+ # Does the password file exist?
+ def password?
+ FileTest.exist? Puppet[:capass]
+ end
+
+ # Print a given host's certificate as text.
+ def print(name)
+ if cert = Puppet::SSL::Certificate.find(name)
+ return cert.to_text
+ else
+ return nil
+ end
+ end
+
+ # Revoke a given certificate.
+ def revoke(name)
+ raise ArgumentError, "Cannot revoke certificates when the CRL is disabled" unless crl
+
+ if cert = Puppet::SSL::Certificate.find(name)
+ serial = cert.content.serial
+ elsif ! serial = inventory.serial(name)
+ raise ArgumentError, "Could not find a serial number for %s" % name
+ end
+ crl.revoke(serial, host.key.content)
+ end
+
+ # This initializes our CA so it actually works. This should be a private
+ # method, except that you can't any-instance stub private methods, which is
+ # *awesome*. This method only really exists to provide a stub-point during
+ # testing.
+ def setup
+ generate_ca_certificate unless @host.certificate
+ end
+
+ # Sign a given certificate request.
+ def sign(hostname, cert_type = :server, self_signing_csr = nil)
+ # This is a self-signed certificate
+ if self_signing_csr
+ csr = self_signing_csr
+ issuer = csr.content
+ else
+ unless csr = Puppet::SSL::CertificateRequest.find(hostname)
+ raise ArgumentError, "Could not find certificate request for %s" % hostname
+ end
+ issuer = host.certificate.content
+ end
+
+ cert = Puppet::SSL::Certificate.new(hostname)
+ cert.content = Puppet::SSL::CertificateFactory.new(cert_type, csr.content, issuer, next_serial).result
+ cert.content.sign(host.key.content, OpenSSL::Digest::SHA1.new)
+
+ Puppet.notice "Signed certificate request for %s" % hostname
+
+ # Add the cert to the inventory before we save it, since
+ # otherwise we could end up with it being duplicated, if
+ # this is the first time we build the inventory file.
+ inventory.add(cert)
+
+ # Save the now-signed cert. This should get routed correctly depending
+ # on the certificate type.
+ cert.save
+
+ # And remove the CSR if this wasn't self signed.
+ Puppet::SSL::CertificateRequest.destroy(csr.name) unless self_signing_csr
+
+ return cert
+ end
+
+ # Verify a given host's certificate.
+ def verify(name)
+ unless cert = Puppet::SSL::Certificate.find(name)
+ raise ArgumentError, "Could not find a certificate for %s" % name
+ end
+ store = OpenSSL::X509::Store.new
+ store.add_file Puppet[:cacert]
+ store.add_crl crl.content if self.crl
+ store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
+
+ unless store.verify(cert.content)
+ raise "Certificate for %s failed verification" % name
+ end
+ end
+
+ # List the waiting certificate requests.
+ def waiting?
+ Puppet::SSL::CertificateRequest.search("*").collect { |r| r.name }
+ end
+end
diff --git a/lib/puppet/ssl/certificate_authority/interface.rb b/lib/puppet/ssl/certificate_authority/interface.rb
new file mode 100644
index 000000000..b355e21f0
--- /dev/null
+++ b/lib/puppet/ssl/certificate_authority/interface.rb
@@ -0,0 +1,110 @@
+# This class is basically a hidden class that knows how to act
+# on the CA. It's only used by the 'puppetca' executable, and its
+# job is to provide a CLI-like interface to the CA class.
+class Puppet::SSL::CertificateAuthority::Interface
+ INTERFACE_METHODS = [:destroy, :list, :revoke, :generate, :sign, :print, :verify]
+
+ class InterfaceError < ArgumentError; end
+
+ attr_reader :method, :subjects
+
+ # Actually perform the work.
+ def apply(ca)
+ unless subjects or method == :list
+ raise ArgumentError, "You must provide hosts or :all when using %s" % method
+ end
+
+ begin
+ if respond_to?(method)
+ return send(method, ca)
+ end
+
+ (subjects == :all ? ca.list : subjects).each do |host|
+ ca.send(method, host)
+ end
+ rescue InterfaceError
+ raise
+ rescue => detail
+ puts detail.backtrace if Puppet[:trace]
+ Puppet.err "Could not call %s: %s" % [method, detail]
+ end
+ end
+
+ def generate(ca)
+ raise InterfaceError, "It makes no sense to generate all hosts; you must specify a list" if subjects == :all
+
+ subjects.each do |host|
+ ca.generate(host)
+ end
+ end
+
+ def initialize(method, subjects)
+ self.method = method
+ self.subjects = subjects
+ end
+
+ # List the hosts.
+ def list(ca)
+ unless subjects
+ puts ca.waiting?.join("\n")
+ return nil
+ end
+
+ signed = ca.list
+ requests = ca.waiting?
+
+ if subjects == :all
+ hosts = [signed, requests].flatten
+ else
+ hosts = subjects
+ end
+
+ hosts.uniq.sort.each do |host|
+ if signed.include?(host)
+ puts "+ " + host
+ else
+ puts host
+ end
+ end
+ end
+
+ # Set the method to apply.
+ def method=(method)
+ raise ArgumentError, "Invalid method %s to apply" % method unless INTERFACE_METHODS.include?(method)
+ @method = method
+ end
+
+ # Print certificate information.
+ def print(ca)
+ (subjects == :all ? ca.list : subjects).each do |host|
+ if value = ca.print(host)
+ puts value
+ else
+ Puppet.err "Could not find certificate for %s" % host
+ end
+ end
+ end
+
+ # Sign a given certificate.
+ def sign(ca)
+ list = subjects == :all ? ca.waiting? : subjects
+ raise InterfaceError, "No waiting certificate requests to sign" if list.empty?
+ list.each do |host|
+ ca.sign(host)
+ end
+ end
+
+ # Set the list of hosts we're operating on. Also supports keywords.
+ def subjects=(value)
+ unless value == :all or value.is_a?(Array)
+ raise ArgumentError, "Subjects must be an array or :all; not %s" % value
+ end
+
+ if value.is_a?(Array) and value.empty?
+ value = nil
+ end
+
+ @subjects = value
+ end
+end
+
diff --git a/lib/puppet/ssl/certificate_factory.rb b/lib/puppet/ssl/certificate_factory.rb
new file mode 100644
index 000000000..41155fd41
--- /dev/null
+++ b/lib/puppet/ssl/certificate_factory.rb
@@ -0,0 +1,145 @@
+require 'puppet/ssl'
+
+# 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
+ }
+
+ attr_reader :name, :cert_type, :csr, :issuer, :serial
+
+ def initialize(cert_type, csr, issuer, serial)
+ @cert_type, @csr, @issuer, @serial = cert_type, csr, issuer, serial
+
+ @name = @csr.subject
+ end
+
+ # Actually generate our certificate.
+ def result
+ @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
+
+ @cert
+ 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"
+
+ begin
+ send(method)
+ rescue NoMethodError
+ raise ArgumentError, "%s is an invalid certificate type" % @cert_type
+ end
+
+ @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/certificate_request.rb b/lib/puppet/ssl/certificate_request.rb
new file mode 100644
index 000000000..34cae5a3e
--- /dev/null
+++ b/lib/puppet/ssl/certificate_request.rb
@@ -0,0 +1,36 @@
+require 'puppet/ssl/base'
+
+# Manage certificate requests.
+class Puppet::SSL::CertificateRequest < Puppet::SSL::Base
+ wraps OpenSSL::X509::Request
+
+ extend Puppet::Indirector
+ indirects :certificate_request, :terminus_class => :file
+
+ # How to create a certificate request with our system defaults.
+ def generate(key)
+ Puppet.info "Creating a new SSL certificate request for %s" % name
+
+ # Support either an actual SSL key, or a Puppet key.
+ key = key.content if key.is_a?(Puppet::SSL::Key)
+
+ csr = OpenSSL::X509::Request.new
+ csr.version = 0
+ csr.subject = OpenSSL::X509::Name.new([["CN", name]])
+ csr.public_key = key.public_key
+ csr.sign(key, OpenSSL::Digest::MD5.new)
+
+ raise Puppet::Error, "CSR sign verification failed; you need to clean the certificate request for %s on the server" % name unless csr.verify(key.public_key)
+
+ @content = csr
+ end
+
+ def save
+ super()
+
+ # Try to autosign the CSR.
+ if ca = Puppet::SSL::CertificateAuthority.instance
+ ca.autosign
+ end
+ end
+end
diff --git a/lib/puppet/ssl/certificate_revocation_list.rb b/lib/puppet/ssl/certificate_revocation_list.rb
new file mode 100644
index 000000000..3029c14a4
--- /dev/null
+++ b/lib/puppet/ssl/certificate_revocation_list.rb
@@ -0,0 +1,72 @@
+require 'puppet/ssl/base'
+require 'puppet/indirector'
+
+# Manage the CRL.
+class Puppet::SSL::CertificateRevocationList < Puppet::SSL::Base
+ wraps OpenSSL::X509::CRL
+
+ extend Puppet::Indirector
+ indirects :certificate_revocation_list, :terminus_class => :file
+
+ # Knows how to create a CRL with our system defaults.
+ def generate(cert, cakey)
+ Puppet.info "Creating a new certificate revocation list"
+ @content = wrapped_class.new
+ @content.issuer = cert.subject
+ @content.version = 1
+
+ # Init the CRL number.
+ crlNum = OpenSSL::ASN1::Integer(0)
+ @content.extensions = [OpenSSL::X509::Extension.new("crlNumber", crlNum)]
+
+ # Set last/next update
+ @content.last_update = Time.now
+ # Keep CRL valid for 5 years
+ @content.next_update = Time.now + 5 * 365*24*60*60
+
+ @content.sign(cakey, OpenSSL::Digest::SHA1.new)
+
+ @content
+ end
+
+ # The name doesn't actually matter; there's only one CRL.
+ # We just need the name so our Indirector stuff all works more easily.
+ def initialize(fakename)
+ raise Puppet::Error, "Cannot manage the CRL when :cacrl is set to false" if [false, "false"].include?(Puppet[:cacrl])
+
+ @name = "crl"
+ end
+
+ # Revoke the certificate with serial number SERIAL issued by this
+ # CA, then write the CRL back to disk. The REASON must be one of the
+ # OpenSSL::OCSP::REVOKED_* reasons
+ def revoke(serial, cakey, reason = OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE)
+ Puppet.notice "Revoked certificate with serial %s" % serial
+ time = Time.now
+
+ # Add our revocation to the CRL.
+ revoked = OpenSSL::X509::Revoked.new
+ revoked.serial = serial
+ revoked.time = time
+ enum = OpenSSL::ASN1::Enumerated(reason)
+ ext = OpenSSL::X509::Extension.new("CRLReason", enum)
+ revoked.add_extension(ext)
+ @content.add_revoked(revoked)
+
+ # Increment the crlNumber
+ e = @content.extensions.find { |e| e.oid == 'crlNumber' }
+ ext = @content.extensions.reject { |e| e.oid == 'crlNumber' }
+ crlNum = OpenSSL::ASN1::Integer(e ? e.value.to_i + 1 : 0)
+ ext << OpenSSL::X509::Extension.new("crlNumber", crlNum)
+ @content.extensions = ext
+
+ # Set last/next update
+ @content.last_update = time
+ # Keep CRL valid for 5 years
+ @content.next_update = time + 5 * 365*24*60*60
+
+ @content.sign(cakey, OpenSSL::Digest::SHA1.new)
+
+ save
+ end
+end
diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb
new file mode 100644
index 000000000..e366bfbdd
--- /dev/null
+++ b/lib/puppet/ssl/host.rb
@@ -0,0 +1,185 @@
+require 'puppet/ssl'
+require 'puppet/ssl/key'
+require 'puppet/ssl/certificate'
+require 'puppet/ssl/certificate_request'
+require 'puppet/ssl/certificate_revocation_list'
+require 'puppet/util/constant_inflector'
+
+# The class that manages all aspects of our SSL certificates --
+# private keys, public keys, requests, etc.
+class Puppet::SSL::Host
+ # Yay, ruby's strange constant lookups.
+ Key = Puppet::SSL::Key
+ Certificate = Puppet::SSL::Certificate
+ CertificateRequest = Puppet::SSL::CertificateRequest
+ CertificateRevocationList = Puppet::SSL::CertificateRevocationList
+
+ extend Puppet::Util::ConstantInflector
+
+ attr_reader :name
+ attr_accessor :ca
+
+ attr_writer :key, :certificate, :certificate_request
+
+ CA_NAME = "ca"
+
+ # This is the constant that people will use to mark that a given host is
+ # a certificate authority.
+ def self.ca_name
+ CA_NAME
+ end
+
+ class << self
+ attr_reader :ca_location
+ end
+
+ # Configure how our various classes interact with their various terminuses.
+ def self.configure_indirection(terminus, cache = nil)
+ Certificate.terminus_class = terminus
+ CertificateRequest.terminus_class = terminus
+ CertificateRevocationList.terminus_class = terminus
+
+ if cache
+ # This is weird; we don't actually cache our keys or CRL, we
+ # use what would otherwise be the cache as our normal
+ # terminus.
+ Key.terminus_class = cache
+ else
+ Key.terminus_class = terminus
+ end
+
+ if cache
+ Certificate.cache_class = cache
+ CertificateRequest.cache_class = cache
+ CertificateRevocationList.cache_class = cache
+ end
+ end
+
+ # Specify how we expect to interact with our certificate authority.
+ def self.ca_location=(mode)
+ raise ArgumentError, "CA Mode can only be :local, :remote, or :none" unless [:local, :remote, :none].include?(mode)
+
+ @ca_mode = mode
+
+ case @ca_mode
+ when :local:
+ # Our ca is local, so we use it as the ultimate source of information
+ # And we cache files locally.
+ configure_indirection :ca, :file
+ when :remote:
+ configure_indirection :rest, :file
+ when :none:
+ # We have no CA, so we just look in the local file store.
+ configure_indirection :file
+ end
+ end
+
+ # Remove all traces of a given host
+ def self.destroy(name)
+ [Key, Certificate, CertificateRequest].inject(false) do |result, klass|
+ if klass.destroy(name)
+ result = true
+ end
+ result
+ end
+ end
+
+ # Search for more than one host, optionally only specifying
+ # an interest in hosts with a given file type.
+ # This just allows our non-indirected class to have one of
+ # indirection methods.
+ def self.search(options = {})
+ classes = [Key, CertificateRequest, Certificate]
+ if klass = options[:for]
+ classlist = [klass].flatten
+ else
+ classlist = [Key, CertificateRequest, Certificate]
+ end
+
+ # Collect the results from each class, flatten them, collect all of the names, make the name list unique,
+ # then create a Host instance for each one.
+ classlist.collect { |klass| klass.search }.flatten.collect { |r| r.name }.uniq.collect do |name|
+ new(name)
+ end
+ end
+
+ # Is this a ca host, meaning that all of its files go in the CA location?
+ def ca?
+ ca
+ end
+
+ def key
+ return nil unless @key ||= Key.find(name)
+ @key
+ end
+
+ # This is the private key; we can create it from scratch
+ # with no inputs.
+ def generate_key
+ @key = Key.new(name)
+ @key.generate
+ @key.save
+ true
+ end
+
+ def certificate_request
+ return nil unless @certificate_request ||= CertificateRequest.find(name)
+ @certificate_request
+ end
+
+ # Our certificate request requires the key but that's all.
+ def generate_certificate_request
+ generate_key unless key
+ @certificate_request = CertificateRequest.new(name)
+ @certificate_request.generate(key.content)
+ @certificate_request.save
+ return true
+ end
+
+ def certificate
+ return nil unless @certificate ||= Certificate.find(name)
+ @certificate
+ end
+
+ # Generate all necessary parts of our ssl host.
+ def generate
+ generate_key unless key
+ generate_certificate_request unless certificate_request
+
+ # If we can get a CA instance, then we're a valid CA, and we
+ # should use it to sign our request; else, just try to read
+ # the cert.
+ if ! certificate() and ca = Puppet::SSL::CertificateAuthority.instance
+ ca.sign(self.name)
+ end
+ end
+
+ def initialize(name = nil)
+ @name = name || Puppet[:certname]
+ @key = @certificate = @certificate_request = nil
+ @ca = (name == self.class.ca_name)
+ end
+
+ # Extract the public key from the private key.
+ def public_key
+ key.content.public_key
+ end
+
+ # Create/return a store that uses our SSL info to validate
+ # connections.
+ def ssl_store(purpose = OpenSSL::X509::PURPOSE_ANY)
+ store = OpenSSL::X509::Store.new
+ store.purpose = purpose
+
+ store.add_file(Puppet[:localcacert])
+
+ # If there's a CRL, add it to our store.
+ if crl = Puppet::SSL::CertificateRevocationList.find("ca")
+ store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK
+ store.add_crl(crl.content)
+ end
+ return store
+ end
+end
+
+require 'puppet/ssl/certificate_authority'
diff --git a/lib/puppet/ssl/inventory.rb b/lib/puppet/ssl/inventory.rb
new file mode 100644
index 000000000..38cbf46e9
--- /dev/null
+++ b/lib/puppet/ssl/inventory.rb
@@ -0,0 +1,52 @@
+require 'puppet/ssl'
+require 'puppet/ssl/certificate'
+
+# Keep track of all of our known certificates.
+class Puppet::SSL::Inventory
+ attr_reader :path
+
+ # Add a certificate to our inventory.
+ def add(cert)
+ cert = cert.content if cert.is_a?(Puppet::SSL::Certificate)
+
+ # Create our file, if one does not already exist.
+ rebuild unless FileTest.exist?(@path)
+
+ Puppet.settings.write(:cert_inventory, "a") do |f|
+ f.print format(cert)
+ end
+ end
+
+ # Format our certificate for output.
+ def format(cert)
+ iso = '%Y-%m-%dT%H:%M:%S%Z'
+ return "0x%04x %s %s %s\n" % [cert.serial, cert.not_before.strftime(iso), cert.not_after.strftime(iso), cert.subject]
+ end
+
+ def initialize
+ @path = Puppet[:cert_inventory]
+ end
+
+ # Rebuild the inventory from scratch. This should happen if
+ # the file is entirely missing or if it's somehow corrupted.
+ def rebuild
+ Puppet.notice "Rebuilding inventory file"
+
+ Puppet.settings.write(:cert_inventory) do |f|
+ f.print "# Inventory of signed certificates\n# SERIAL NOT_BEFORE NOT_AFTER SUBJECT\n"
+ end
+
+ Puppet::SSL::Certificate.search("*").each { |cert| add(cert) }
+ end
+
+ # Find the serial number for a given certificate.
+ def serial(name)
+ return nil unless FileTest.exist?(@path)
+
+ File.readlines(@path).each do |line|
+ next unless line =~ /^(\S+).+\/CN=#{name}$/
+
+ return Integer($1)
+ end
+ end
+end
diff --git a/lib/puppet/ssl/key.rb b/lib/puppet/ssl/key.rb
new file mode 100644
index 000000000..a1d436090
--- /dev/null
+++ b/lib/puppet/ssl/key.rb
@@ -0,0 +1,50 @@
+require 'puppet/ssl/base'
+require 'puppet/indirector'
+
+# Manage private and public keys as a pair.
+class Puppet::SSL::Key < Puppet::SSL::Base
+ wraps OpenSSL::PKey::RSA
+
+ 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].to_i)
+ end
+
+ def initialize(name)
+ super
+
+ if ca?
+ @password_file = Puppet[:capass]
+ else
+ @password_file = Puppet[:passfile]
+ end
+ end
+
+ def password
+ return nil unless password_file and FileTest.exist?(password_file)
+
+ ::File.read(password_file)
+ end
+
+ # Optionally support specifying a password file.
+ def read(path)
+ return super unless password_file
+
+ #@content = wrapped_class.new(::File.read(path), password)
+ @content = wrapped_class.new(::File.read(path), password)
+ end
+
+ def to_s
+ if pass = password
+ @content.export(OpenSSL::Cipher::DES.new(:EDE3, :CBC), pass)
+ else
+ return super
+ end
+ end
+end