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.



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

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.



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

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.



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

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:



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

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



27
28
29
# File 'lib/localhost/authority.rb', line 27

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

Instance Method Details

#certificateObject

The public certificate.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/localhost/authority.rb', line 114

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 = 1
    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.



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

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

#client_context(*args) ⇒ Object



179
180
181
182
183
184
185
186
187
# File 'lib/localhost/authority.rb', line 179

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



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

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

#ecdh_keyObject



76
77
78
# File 'lib/localhost/authority.rb', line 76

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

#keyObject

The private key.



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

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

#key=(key) ⇒ Object



99
100
101
# File 'lib/localhost/authority.rb', line 99

def key= key
  @key = key
end

#key_pathObject

The private key path.



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

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

#load(path = @root) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/localhost/authority.rb', line 189

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.



104
105
106
# File 'lib/localhost/authority.rb', line 104

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

#name=(name) ⇒ Object



108
109
110
# File 'lib/localhost/authority.rb', line 108

def name= name
  @name = name
end

#save(path = @root) ⇒ Object



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/localhost/authority.rb', line 209

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



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/localhost/authority.rb', line 154

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.



145
146
147
148
149
# File 'lib/localhost/authority.rb', line 145

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