class Localhost::Authority

Represents a single public/private key pair for a given hostname.

def self.fetch(*arguments, **options)

See {#initialize} for the format of the arguments.
Fetch (load or create) a certificate with the given hostname.
def self.fetch(*arguments, **options)
	authority = self.new(*arguments, **options)
	
	unless authority.load
		authority.save
	end
	
	return authority
end

def self.list(path = State.path)

@yields [Authority] Each certificate authority in the directory.
@parameter path [String] The path to the directory containing the certificate authorities.

List all certificate authorities in the given directory.
def self.list(path = State.path)
	return to_enum(:list, path) unless block_given?
	
	Dir.glob("*.crt", base: path) do |certificate_path|
		hostname = File.basename(certificate_path, ".crt")
		
		authority = self.new(hostname, path: path)
		
		if authority.load
			yield authority
		end
	end
end

def self.path

@returns [String] The path to the directory containing the certificate authorities.
def self.path
	State.path
end

def certificate

@returns [OpenSSL::X509::Certificate] A self-signed certificate.

Generates a self-signed certificate if one does not already exist for the given hostname.
def certificate
	issuer = @issuer || self
	
	@certificate ||= OpenSSL::X509::Certificate.new.tap do |certificate|
		certificate.subject = self.subject
		certificate.issuer = issuer.subject
		
		certificate.public_key = self.key.public_key
		
		certificate.serial = Time.now.to_i
		certificate.version = 2
		
		certificate.not_before = Time.now
		certificate.not_after = Time.now + (3600 * 24 * 365)
		
		extension_factory = OpenSSL::X509::ExtensionFactory.new
		extension_factory.subject_certificate = certificate
		extension_factory.issuer_certificate = @issuer&.certificate || certificate
		
		certificate.add_extension extension_factory.create_extension("basicConstraints", "CA:FALSE", true)
		certificate.add_extension extension_factory.create_extension("subjectKeyIdentifier", "hash")
		certificate.add_extension extension_factory.create_extension("subjectAltName", "DNS: #{@hostname}")
		certificate.add_extension extension_factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
		
		certificate.sign issuer.key, OpenSSL::Digest::SHA256.new
	end
end

def certificate_path

@returns [String] The path to the public certificate.
def certificate_path
	File.join(@path, "#{@hostname}.crt")
end

def client_context(*args)

@returns [OpenSSL::SSL::SSLContext] An context suitable for connecting to a secure server using this authority.
def client_context(*args)
	OpenSSL::SSL::SSLContext.new(*args).tap do |context|
		context.cert_store = self.store
		
		context.set_params(
			verify_mode: OpenSSL::SSL::VERIFY_PEER,
		)
	end
end

def dh_key

@returns [OpenSSL::PKey::DH] A Diffie-Hellman key suitable for secure key exchange.
def dh_key
	@dh_key ||= OpenSSL::PKey::DH.new(BITS)
end

def initialize(hostname = "localhost", path: State.path, issuer: Issuer.fetch)

@parameter path [String] The path path for loading and saving the certificate.
@parameter hostname [String] The common name to use for the certificate.
Create an authority forn the given hostname.
def initialize(hostname = "localhost", path: State.path, issuer: Issuer.fetch)
	@path = path
	@hostname = hostname
	@issuer = issuer
	
	@subject = nil
	@key = nil
	@certificate = nil
	@store = nil
end

def key

@returns [OpenSSL::PKey::RSA] The private key.
def key
	@key ||= OpenSSL::PKey::RSA.new(BITS)
end

def key= key

@parameter key [OpenSSL::PKey::RSA] The private key.

Set the private key.
def key= key
	@key = key
end

def key_path

@returns [String] The path to the private key.
def key_path
	File.join(@path, "#{@hostname}.key")
end

def load(path = @path)

@returns [Boolean] Whether the certificate and key were successfully loaded.
@parameter path [String] The path to the certificate and key.

Load the certificate and key from the given path.
def load(path = @path)
	certificate_path = File.join(path, "#{@hostname}.crt")
	key_path = File.join(path, "#{@hostname}.key")
	
	return false unless File.exist?(certificate_path) and File.exist?(key_path)
	
	certificate = OpenSSL::X509::Certificate.new(File.read(certificate_path))
	key = OpenSSL::PKey::RSA.new(File.read(key_path))
	
	# Certificates with old version need to be regenerated.
	return false if certificate.version < 2
	
	@certificate = certificate
	@key = key
	
	return true
end

def save(path = @path)

@parameter path [String] The path to save the certificate and key.

Save the certificate and key to the given path.
def save(path = @path)
	lockfile_path = File.join(path, "#{@hostname}.lock")
	
	File.open(lockfile_path, File::RDWR|File::CREAT, 0644) do |lockfile|
		lockfile.flock(File::LOCK_EX)
		
		File.write(
			File.join(path, "#{@hostname}.crt"),
			self.certificate.to_pem
		)
		
		File.write(
			File.join(path, "#{@hostname}.key"),
			self.key.to_pem
		)
	end
	
	return true
end

def server_context(*arguments)

@returns [OpenSSL::SSL::SSLContext] An context suitable for implementing a secure server.
def server_context(*arguments)
	OpenSSL::SSL::SSLContext.new(*arguments).tap do |context|
		context.key = self.key
		context.cert = self.certificate
		
		if @issuer
			context.extra_chain_cert = [@issuer.certificate]
		end
		
		context.session_id_context = "localhost"
		
		if context.respond_to? :tmp_dh_callback=
			context.tmp_dh_callback = proc {self.dh_key}
		end
		
		if context.respond_to? :ecdh_curves=
			context.ecdh_curves = "P-256:P-384:P-521"
		end
		
		context.set_params(
			ciphers: SERVER_CIPHERS,
			verify_mode: OpenSSL::SSL::VERIFY_NONE,
		)
	end
end

def store

@returns [OpenSSL::X509::Store] The certificate store with the issuer certificate.

The certificate store which is used for validating the server certificate.
def store
	@store ||= OpenSSL::X509::Store.new.tap do |store|
		if @issuer
			store.add_cert(@issuer.certificate)
		else
			store.add_cert(self.certificate)
		end
	end
end

def subject

@returns [OpenSSL::X509::Name] The subject name for the certificate.
def subject
	@subject ||= OpenSSL::X509::Name.parse("/O=localhost.rb/CN=#{@hostname}")
end

def subject= subject

@parameter subject [OpenSSL::X509::Name] The subject name.

Set the subject name for the certificate.
def subject= subject
	@subject = subject
end