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, "#{@cert_type} is an invalid certificate 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)$/

    $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

