Localhost SourceLocalhostAuthority

class Authority

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

Definitions

def self.path(env = ENV, old_root: nil)

Where to store the key pair on the filesystem. This is a subdirectory of $XDG_STATE_HOME, or ~/.local/state/ when that's not defined.

Ensures that the directory to store the certificate exists. If the legacy directory (~/.localhost/) exists, it is moved into the new XDG Basedir compliant directory.

After May 2025, the old_root option may be removed.

Implementation

def self.path(env = ENV, old_root: nil)
	path = File.expand_path("localhost.rb", env.fetch("XDG_STATE_HOME", "~/.local/state"))
	
	unless File.directory?(path)
		FileUtils.mkdir_p(path, mode: 0700)
	end
	
	# Migrates the legacy dir ~/.localhost/ to the XDG compliant directory
	old_root ||= File.expand_path("~/.localhost")
	if File.directory?(old_root)
		FileUtils.mv(Dir.glob(File.join(old_root, "*")), path, force: true)
		FileUtils.rmdir(old_root)
	end
	
	return path
end

def self.list(root = self.path)

List all certificate authorities in the given directory:

Implementation

def self.list(root = self.path)
	return to_enum(:list, root) unless block_given?
	
	Dir.glob("*.crt", base: root) do |path|
		name = File.basename(path, ".crt")
		
		authority = self.new(name, root: root)
		
		if authority.load
			yield authority
		end
	end
end

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

Fetch (load or create) a certificate with the given hostname. See #initialize for the format of the arguments.

Implementation

def self.fetch(*arguments, **options)
	authority = self.new(*arguments, **options)
	
	unless authority.load
		authority.save
	end
	
	return authority
end

def initialize(hostname = "localhost", root: self.class.path)

Create an authority forn the given hostname.

Signature

parameter hostname String

The common name to use for the certificate.

parameter root String

The root path for loading and saving the certificate.

Implementation

def initialize(hostname = "localhost", root: self.class.path)
	@root = root
	@hostname = hostname
	
	@key = nil
	@name = nil
	@certificate = nil
	@store = nil
end

attr :hostname

The hostname of the certificate authority.

def key_path

The private key path.

Implementation

def key_path
	File.join(@root, "#{@hostname}.key")
end

def certificate_path

The public certificate path.

Implementation

def certificate_path
	File.join(@root, "#{@hostname}.crt")
end

def key

The private key.

Implementation

def key
	@key ||= OpenSSL::PKey::RSA.new(BITS)
end

def name

The certificate name.

Implementation

def name
	@name ||= OpenSSL::X509::Name.parse("/O=Development/CN=#{@hostname}")
end

def certificate

The public certificate.

Signature

returns OpenSSL::X509::Certificate

A self-signed certificate.

Implementation

def certificate
	@certificate ||= OpenSSL::X509::Certificate.new.tap do |certificate|
		certificate.subject = self.name
		# We use the same issuer as the subject, which makes this certificate self-signed:
		certificate.issuer = self.name
		
		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 = certificate
		
		certificate.extensions = [
			extension_factory.create_extension("basicConstraints", "CA:FALSE", true),
			extension_factory.create_extension("subjectKeyIdentifier", "hash"),
		]
		
		certificate.add_extension extension_factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
		certificate.add_extension extension_factory.create_extension("subjectAltName", "DNS: #{@hostname}")
		
		certificate.sign self.key, OpenSSL::Digest::SHA256.new
	end
end

def store

The certificate store which is used for validating the server certificate.

Implementation

def store
	@store ||= OpenSSL::X509::Store.new.tap do |store|
		store.add_cert(self.certificate)
	end
end

def server_context(*arguments)

Signature

returns OpenSSL::SSL::SSLContext

An context suitable for implementing a secure server.

Implementation

def server_context(*arguments)
	OpenSSL::SSL::SSLContext.new(*arguments).tap do |context|
		context.key = self.key
		context.cert = self.certificate
		
		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 client_context(*args)

Signature

returns OpenSSL::SSL::SSLContext

An context suitable for connecting to a secure server using this authority.

Implementation

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