Class: Localhost::Authority

Inherits:
Object
  • Object
show all
Defined in:
lib/localhost/authority.rb

Overview

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

Constant Summary collapse

BITS =
1024*2
SERVER_CIPHERS =
"EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5".freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hostname = "localhost", root: self.class.path) ⇒ Authority

Create an authority forn the given hostname.



50
51
52
53
54
55
56
57
58
# File 'lib/localhost/authority.rb', line 50

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

Instance Attribute Details

#hostnameObject (readonly)

The hostname of the certificate authority.



61
62
63
# File 'lib/localhost/authority.rb', line 61

def hostname
  @hostname
end

Class Method Details

.fetch(*arguments, **options) ⇒ Object

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



37
38
39
40
41
42
43
44
45
# File 'lib/localhost/authority.rb', line 37

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

.list(root = self.path) ⇒ Object

List all certificate authorities in the given directory:



21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/localhost/authority.rb', line 21

def self.list(root = self.path)
	return to_enum(:list) 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

.pathObject



16
17
18
# File 'lib/localhost/authority.rb', line 16

def self.path
	File.expand_path("~/.localhost")
end

Instance Method Details

#certificateObject

The public certificate.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/localhost/authority.rb', line 103

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 * 10)
		
		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

#certificate_pathObject

The public certificate path.



79
80
81
# File 'lib/localhost/authority.rb', line 79

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

#client_context(*args) ⇒ Object



168
169
170
171
172
173
174
175
176
# File 'lib/localhost/authority.rb', line 168

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

#dh_keyObject



69
70
71
# File 'lib/localhost/authority.rb', line 69

def dh_key
	@dh_key ||= OpenSSL::PKey::DH.new(BITS)
end

#ecdh_keyObject



65
66
67
# File 'lib/localhost/authority.rb', line 65

def ecdh_key
	@ecdh_key ||= OpenSSL::PKey::EC.new "prime256v1"
end

#keyObject

The private key.



84
85
86
# File 'lib/localhost/authority.rb', line 84

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

#key=(key) ⇒ Object



88
89
90
# File 'lib/localhost/authority.rb', line 88

def key= key
	@key = key
end

#key_pathObject

The private key path.



74
75
76
# File 'lib/localhost/authority.rb', line 74

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

#load(path = @root) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/localhost/authority.rb', line 178

def load(path = @root)
	if File.directory?(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
end

#nameObject

The certificate name.



93
94
95
# File 'lib/localhost/authority.rb', line 93

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

#name=(name) ⇒ Object



97
98
99
# File 'lib/localhost/authority.rb', line 97

def name= name
	@name = name
end

#save(path = @root) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/localhost/authority.rb', line 198

def save(path = @root)
	Dir.mkdir(path, 0700) unless File.directory?(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
end

#server_context(*arguments) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/localhost/authority.rb', line 143

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'
		elsif context.respond_to? :tmp_ecdh_callback=
			context.tmp_ecdh_callback = proc {self.ecdh_key}
		end
		
		context.set_params(
			ciphers: SERVER_CIPHERS,
			verify_mode: OpenSSL::SSL::VERIFY_NONE,
		)
	end
end

#storeObject

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



134
135
136
137
138
# File 'lib/localhost/authority.rb', line 134

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