class Localhost::Issuer

Represents a local Root Certificate Authority used to sign development certificates.

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

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

def certificate

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

The public certificate.
def certificate
	@certificate ||= OpenSSL::X509::Certificate.new.tap do |certificate|
		certificate.subject = self.subject
		# We use the same issuer as the subject, which makes this certificate self-signed:
		certificate.issuer = self.subject
		
		certificate.public_key = self.key.public_key
		
		certificate.serial = Time.now.to_i
		certificate.version = 2
		
		certificate.not_before = Time.now - 10
		certificate.not_after = Time.now + VALIDITY
		
		extension_factory = ::OpenSSL::X509::ExtensionFactory.new
		extension_factory.subject_certificate = certificate
		extension_factory.issuer_certificate = certificate
		
		certificate.add_extension extension_factory.create_extension("basicConstraints", "CA:TRUE", true)
		certificate.add_extension extension_factory.create_extension("keyUsage", "keyCertSign, cRLSign", true)
		certificate.add_extension extension_factory.create_extension("subjectKeyIdentifier", "hash")
		certificate.add_extension extension_factory.create_extension("authorityKeyIdentifier", "keyid:always", false)
		
		certificate.sign self.key, OpenSSL::Digest::SHA256.new
	end
end

def certificate_path

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

def initialize(name = "development", path: State.path)

@parameter path [String] The path path for loading and saving the certificate.
@parameter name [String] The common name to use for the certificate.

Initialize the issuer with the given name.
def initialize(name = "development", path: State.path)
	@name = name
	@path = path
	
	@subject = nil
	@key = nil
	@certificate = nil
end

def key

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

def key_path

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

def load(path = @root)

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

Load the certificate and key from the given path.
def load(path = @root)
	certificate_path = self.certificate_path
	key_path = self.key_path
	
	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))
	
	@certificate = certificate
	@key = key
	
	return true
end

def lockfile_path

@returns [String] The path to the lockfile.
def lockfile_path
	File.join(@path, "#{@name}.lock")
end

def save(path = @root)

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

Save the certificate and key to the given path.
def save(path = @root)
	lockfile_path = self.lockfile_path
	
	File.open(lockfile_path, File::RDWR|File::CREAT, 0644) do |lockfile|
		lockfile.flock(File::LOCK_EX)
		
		File.write(
			self.certificate_path,
			self.certificate.to_pem
		)
		
		File.write(
			self.key_path,
			self.key.to_pem
		)
	end
	
	return true
end

def subject

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

def subject= subject

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

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