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", path: State.path, issuer: Issuer.fetch) ⇒ Authority

Create an authority forn the given hostname.



60
61
62
63
64
65
66
67
68
69
# File 'lib/localhost/authority.rb', line 60

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

Instance Attribute Details

#hostnameObject (readonly)

The hostname of the certificate authority.



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

def hostname
  @hostname
end

#issuerObject (readonly)

Returns the value of attribute issuer.



71
72
73
# File 'lib/localhost/authority.rb', line 71

def issuer
  @issuer
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.



47
48
49
50
51
52
53
54
55
# File 'lib/localhost/authority.rb', line 47

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

.list(path = State.path) ⇒ Object

List all certificate authorities in the given directory.



31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/localhost/authority.rb', line 31

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

.pathObject



23
24
25
# File 'lib/localhost/authority.rb', line 23

def self.path
	State.path
end

Instance Method Details

#as_jsonObject



256
257
258
# File 'lib/localhost/authority.rb', line 256

def as_json(...)
	self.to_h
end

#certificateObject

Generates a self-signed certificate if one does not already exist for the given hostname.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/localhost/authority.rb', line 120

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

#certificate_pathObject



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

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

#client_context(*args) ⇒ Object



191
192
193
194
195
196
197
198
199
# File 'lib/localhost/authority.rb', line 191

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



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

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

#keyObject



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

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

#key=(key) ⇒ Object

Set the private key.



101
102
103
# File 'lib/localhost/authority.rb', line 101

def key= key
	@key = key
end

#key_pathObject



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

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

#load(path = @path) ⇒ Object

Load the certificate and key from the given path.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/localhost/authority.rb', line 205

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

#save(path = @path) ⇒ Object

Save the certificate and key to the given path.



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/localhost/authority.rb', line 226

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

#server_context(*arguments) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/localhost/authority.rb', line 164

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

#storeObject

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



151
152
153
154
155
156
157
158
159
# File 'lib/localhost/authority.rb', line 151

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

#subjectObject



106
107
108
# File 'lib/localhost/authority.rb', line 106

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

#subject=(subject) ⇒ Object

Set the subject name for the certificate.



113
114
115
# File 'lib/localhost/authority.rb', line 113

def subject= subject
	@subject = subject
end

#to_hObject



247
248
249
250
251
252
253
254
# File 'lib/localhost/authority.rb', line 247

def to_h
	{
		hostname: @hostname,
		certificate_path: certificate_path,
		key_path: key_path,
		expires_at: certificate.not_after,
	}
end

#to_jsonObject



260
261
262
# File 'lib/localhost/authority.rb', line 260

def to_json(...)
	self.as_json.to_json(...)
end