certificate-depot 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/TODO +4 -0
- data/bin/depot +9 -0
- data/lib/certificate_depot.rb +261 -0
- data/lib/certificate_depot/certificate.rb +239 -0
- data/lib/certificate_depot/keypair.rb +43 -0
- data/lib/certificate_depot/log.rb +61 -0
- data/lib/certificate_depot/runner.rb +138 -0
- data/lib/certificate_depot/server.rb +287 -0
- data/lib/certificate_depot/store.rb +55 -0
- data/lib/certificate_depot/worker.rb +161 -0
- metadata +94 -0
data/TODO
ADDED
data/bin/depot
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'monitor'
|
4
|
+
|
5
|
+
# = CertificateDepot
|
6
|
+
#
|
7
|
+
# The CertificateDepot manages a single depot of certificates. For more than a
|
8
|
+
# casual understanding of these terms we need to explain a bit more about
|
9
|
+
# certificates first. If you already know how PKI works, you can skip the
|
10
|
+
# following paragraphs.
|
11
|
+
#
|
12
|
+
# == Certificate Authorities
|
13
|
+
#
|
14
|
+
# “[A] certificate authority or certification authority (CA) is an entity that
|
15
|
+
# issues digital certificates for use by other parties.”
|
16
|
+
# – http://en.wikipedia.org/wiki/Certificate_authority
|
17
|
+
#
|
18
|
+
# When a CA says that party A is who they say they are and you trust the CA
|
19
|
+
# then you can assume they are in fact party A.
|
20
|
+
#
|
21
|
+
# In a Public Key Infrastructure this means that the CA has a private key
|
22
|
+
# which only he knows. He uses this key to sign certificates stating that the
|
23
|
+
# person owning the certificate is who the certificate says they are. Because
|
24
|
+
# there is a public key associated with the certificate anyone can use a
|
25
|
+
# crytographic challenge to make sure the owner has the private key to this
|
26
|
+
# certificate.
|
27
|
+
#
|
28
|
+
# For example: I have a certificate that says that I'm the person with the
|
29
|
+
# email address robert@example.com. I show my certificate to Rick. Rick gets
|
30
|
+
# my public key from the certificate and sends me a challenge. I compute the
|
31
|
+
# right response and send it to Rick. Rick checks the response and knows I'm
|
32
|
+
# the rightful owner of the certificate.
|
33
|
+
#
|
34
|
+
# A PKI infrastructure has many more uses, but we will only focus on
|
35
|
+
# authentication in our examples.
|
36
|
+
#
|
37
|
+
# There are two ways in which a CA can make sure his trust network stays
|
38
|
+
# valid. Each certificate has a limited period in which it's valid. Even if
|
39
|
+
# the CA forgets that he has issued the certificate it will stop being valid
|
40
|
+
# after a while. Large CA's claim this also protects against evolving
|
41
|
+
# attack vectors against the crytography used behind the certificates. Each
|
42
|
+
# certificate can be _revoked_ by the CA. A publicly available list of
|
43
|
+
# revoked certificates makes it possible to check whether a certain
|
44
|
+
# certificate is still valid according to the CA.
|
45
|
+
#
|
46
|
+
# == Certificate types
|
47
|
+
#
|
48
|
+
# Certificate authorities came up with lots of additional features. One of
|
49
|
+
# these is a certificate type. To be more precise, there is an extension
|
50
|
+
# to the certificate that tells in which cases you should accept the key
|
51
|
+
# contained in the certificate. This allows the CA to limit the use of a
|
52
|
+
# certificate to just digital signatures, encipherment, or certain types
|
53
|
+
# of authentication.
|
54
|
+
#
|
55
|
+
# For simplicity Certificate Depot only knows three types; CA, Server, and
|
56
|
+
# Client.
|
57
|
+
#
|
58
|
+
# == A certificate depot
|
59
|
+
#
|
60
|
+
# We've named the collection of all information needed to run a CA a
|
61
|
+
# certificate depot. It holds the CA certificate and private key, all issued
|
62
|
+
# certificates, the revokation list, and several files need to manage these.
|
63
|
+
class CertificateDepot
|
64
|
+
autoload :Certificate, 'certificate_depot/certificate'
|
65
|
+
autoload :Keypair, 'certificate_depot/keypair'
|
66
|
+
autoload :Log, 'certificate_depot/log'
|
67
|
+
autoload :Runner, 'certificate_depot/runner'
|
68
|
+
autoload :Server, 'certificate_depot/server'
|
69
|
+
autoload :Store, 'certificate_depot/store'
|
70
|
+
autoload :Worker, 'certificate_depot/worker'
|
71
|
+
|
72
|
+
# Initialize a new depot with the path to the depot directory.
|
73
|
+
#
|
74
|
+
# depot = CertificateDepot.new('/var/lib/certificate-depot/example')
|
75
|
+
def initialize(path)
|
76
|
+
@config = OpenSSL::Config.load(self.class.openssl_config_path(path))
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns the label with a descriptive name for the depot. This is usually
|
80
|
+
# the common name of the CA.
|
81
|
+
def label
|
82
|
+
@config['ca']['label']
|
83
|
+
end
|
84
|
+
|
85
|
+
# Path to the depot directory.
|
86
|
+
def path
|
87
|
+
@config[label]['path']
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns an instance of CertificateDepot::Certificate containing the
|
91
|
+
# certificate of the certificate authority.
|
92
|
+
def ca_certificate
|
93
|
+
@ca_certificate ||= CertificateDepot::Certificate.from_file(self.class.certificate_path(path))
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns an instance of OpenSSL::PKey::RSA containing the private key
|
97
|
+
# of the certificate authority.
|
98
|
+
def ca_private_key
|
99
|
+
@ca_private_key ||= OpenSSL::PKey::RSA.new(File.read(self.class.key_path(path)))
|
100
|
+
end
|
101
|
+
|
102
|
+
# Generates a new RSA keypair and certificate.
|
103
|
+
#
|
104
|
+
# === Defaults
|
105
|
+
#
|
106
|
+
# By default the certificate issuer is the CA of the depot and it's also
|
107
|
+
# signed by the CA. The serial number is the next available serial number
|
108
|
+
# in the depot.
|
109
|
+
#
|
110
|
+
# See CertificateDepot::Certificate#generate for all available options.
|
111
|
+
def generate_keypair_and_certificate(options={})
|
112
|
+
keypair = CertificateDepot::Keypair.generate
|
113
|
+
certificate = nil
|
114
|
+
|
115
|
+
certificates.synchronize do
|
116
|
+
attributes = options
|
117
|
+
attributes[:ca_certificate] = ca_certificate
|
118
|
+
attributes[:public_key] = keypair.public_key
|
119
|
+
attributes[:private_key] = ca_private_key
|
120
|
+
attributes[:serial_number] = certificates.next_serial_number
|
121
|
+
certificate = CertificateDepot::Certificate.generate(attributes)
|
122
|
+
|
123
|
+
certificates << certificate
|
124
|
+
certificates.sync
|
125
|
+
end
|
126
|
+
|
127
|
+
[keypair, certificate]
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns an instance of CertificateDepot::Store representing all
|
131
|
+
# certificates in the depot.
|
132
|
+
def certificates
|
133
|
+
if @certificates.nil?
|
134
|
+
@certificates = CertificateDepot::Store.new(self.class.certificates_path(path))
|
135
|
+
@certificates.extend(MonitorMixin)
|
136
|
+
end; @certificates
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.create_directories(path)
|
140
|
+
FileUtils.mkdir_p(certificates_path(path))
|
141
|
+
FileUtils.mkdir_p(private_path(path))
|
142
|
+
FileUtils.chmod(0700, private_path(path))
|
143
|
+
FileUtils.chmod(0755, path)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Writes a configuration file to disk containing the path to the depot
|
147
|
+
# and its name.
|
148
|
+
def self.create_configuration(path, label)
|
149
|
+
File.open(openssl_config_path(path), 'w') do |file|
|
150
|
+
file.write("[ ca ]
|
151
|
+
label = #{label}
|
152
|
+
|
153
|
+
[ #{label} ]
|
154
|
+
path = #{path}
|
155
|
+
")
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Creates a CA certificate and keypair and writes it to disk.
|
160
|
+
def self.create_ca_certificate(path, label)
|
161
|
+
keypair = CertificateDepot::Keypair.generate
|
162
|
+
keypair.write_to(key_path(path))
|
163
|
+
|
164
|
+
attributes = {}
|
165
|
+
attributes[:type] = :ca
|
166
|
+
attributes[:public_key] = keypair.public_key
|
167
|
+
attributes[:private_key] = keypair.private_key
|
168
|
+
attributes[:organization] = label
|
169
|
+
certificate = CertificateDepot::Certificate.generate(attributes)
|
170
|
+
certificate.write_to(certificate_path(path))
|
171
|
+
end
|
172
|
+
|
173
|
+
# Creates a new depot on disk.
|
174
|
+
#
|
175
|
+
# depot = CertificateDepot.create('/var/lib/certificate-depot/example')
|
176
|
+
def self.create(path, label, options={})
|
177
|
+
attributes = options
|
178
|
+
create_directories(path)
|
179
|
+
create_configuration(path, label)
|
180
|
+
create_ca_certificate(path, label)
|
181
|
+
new(path)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Generates a new RSA keypair and certificate. See
|
185
|
+
# CertificateDepot#generate_keypair_and_certificate and
|
186
|
+
# CertificateDepot::Certificate.new for more information and possible
|
187
|
+
# options.
|
188
|
+
#
|
189
|
+
# keypair, certificate =
|
190
|
+
# CertificateDepot.generate_keypair_and_certificate(
|
191
|
+
# '/var/lib/certificate-depot/example', {
|
192
|
+
# :type => :client,
|
193
|
+
# :common_name => 'Robert Verkey',
|
194
|
+
# :email_address => 'robert@example.com'
|
195
|
+
# }
|
196
|
+
# )
|
197
|
+
def self.generate_keypair_and_certificate(path, options={})
|
198
|
+
depot = new(path)
|
199
|
+
depot.generate_keypair_and_certificate(options)
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns a string with an Apache configuration example for using TLS
|
203
|
+
# client certificate authentication.
|
204
|
+
def self.configuration_example(path)
|
205
|
+
"SSLEngine on
|
206
|
+
SSLOptions +StdEnvVars
|
207
|
+
SSLCertificateFile \"/etc/apache/ssl/certificates/example.com.pem\"
|
208
|
+
SSLVerifyClient require
|
209
|
+
SSLCACertificateFile \"#{certificate_path(path)}\""
|
210
|
+
end
|
211
|
+
|
212
|
+
# Starts a server. For available options see CertificateDepot::Server.new.
|
213
|
+
def self.start(path, options={})
|
214
|
+
CertificateDepot::Server.start(new(path), options)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Stops a running server. Using the options you can specify where to look
|
218
|
+
# for the pid file. See CertificateDepot::Server.new for more information
|
219
|
+
# about the options.
|
220
|
+
def self.stop(options={})
|
221
|
+
CertificateDepot::Server.stop(options)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Runs a command to the depot. Used by the command line tool to run
|
225
|
+
# commands.
|
226
|
+
def self.run(argv)
|
227
|
+
runner = ::CertificateDepot::Runner.new(argv)
|
228
|
+
runner.run
|
229
|
+
runner
|
230
|
+
end
|
231
|
+
|
232
|
+
# Returns the path to the configuration file given the depot path.
|
233
|
+
def self.openssl_config_path(path)
|
234
|
+
File.join(path, 'depot.cnf')
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns the path to the directory with private data given the depot path.
|
238
|
+
def self.private_path(path)
|
239
|
+
File.join(path, 'private')
|
240
|
+
end
|
241
|
+
|
242
|
+
# Returns the path to the generated certificates given the depot path.
|
243
|
+
def self.certificates_path(path)
|
244
|
+
File.join(path, 'certificates')
|
245
|
+
end
|
246
|
+
|
247
|
+
# Returns the path to the certificate revokation list given the depot path.
|
248
|
+
def self.crl_path(path)
|
249
|
+
File.join(path, 'crl.pem')
|
250
|
+
end
|
251
|
+
|
252
|
+
# Returns the path to the CA's private key given the depot path.
|
253
|
+
def self.key_path(path)
|
254
|
+
File.join(private_path(path), 'ca.key')
|
255
|
+
end
|
256
|
+
|
257
|
+
# Returns the path to the CA's certificate given the depot path.
|
258
|
+
def self.certificate_path(path)
|
259
|
+
File.join(certificates_path(path), 'ca.crt')
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
class CertificateDepot
|
2
|
+
# Represents an OpenSSL certificate. TLS/SSL certificates are a rather
|
3
|
+
# complicated mix of several standards. If you're not familiar with TLS
|
4
|
+
# certificates, it might help to start by reading the Wikipedia article on
|
5
|
+
# the subject: http://en.wikipedia.org/wiki/Public_key_certificate.
|
6
|
+
#
|
7
|
+
# certificate = CertificateDepot::Certificate.from_file('server.pem')
|
8
|
+
# certificate.issuer
|
9
|
+
class Certificate
|
10
|
+
# Our generated certificates are valid for 10 years by default.
|
11
|
+
DEFAULT_VALIDITY_PERIOD = 3600 * 24 * 365 * 10
|
12
|
+
# We create version 3 certificates. Count starts a 0 so 2 is actually 3.
|
13
|
+
X509v3 = 2
|
14
|
+
|
15
|
+
# Used to map programmer readable attributes to X509 subject attributes.
|
16
|
+
ATTRIBUTE_MAP = {
|
17
|
+
:common_name => 'CN',
|
18
|
+
:locality_name => 'L',
|
19
|
+
:state_or_province_name => 'ST',
|
20
|
+
:organization => 'O',
|
21
|
+
:organizational_unit_name => 'OU',
|
22
|
+
:country_name => 'C',
|
23
|
+
:street_address => 'STREET',
|
24
|
+
:domain_component => 'DC',
|
25
|
+
:user_id => 'UID',
|
26
|
+
:email_address => 'emailAddress'
|
27
|
+
}
|
28
|
+
|
29
|
+
attr_accessor :certificate
|
30
|
+
|
31
|
+
# Creates a new certificate instance. The certificate argument should be
|
32
|
+
# a OpenSSL::X509::Certificate instance.
|
33
|
+
def initialize(certificate=nil)
|
34
|
+
@certificate = certificate
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generates a new certificate. Possible and compulsory attributes depend
|
38
|
+
# on the type of certificate.
|
39
|
+
#
|
40
|
+
# === Client certificate
|
41
|
+
#
|
42
|
+
# Client certificates are used to authenticate a client to a server. They
|
43
|
+
# are generated by setting the <tt>:type</tt> attribute to
|
44
|
+
# <tt>:client</tt>.
|
45
|
+
#
|
46
|
+
# ==== Compulsory
|
47
|
+
#
|
48
|
+
# * <tt>:ca_certificate</tt> - A CertificateDepot::Certificate instance
|
49
|
+
# representing the Certification Authority.
|
50
|
+
# * <tt>:serial_number</tt> - The serial number to store in the
|
51
|
+
# certificate. This number should be unique for certificates issued
|
52
|
+
# by the CA.
|
53
|
+
# * <tt>:public_key</tt> - A public key which is the sister key of the
|
54
|
+
# private key used by the client.
|
55
|
+
#
|
56
|
+
# ==== Options
|
57
|
+
#
|
58
|
+
# You can choose to either supply an instance of OpenSSL::X509::Name as
|
59
|
+
# the <tt>:subject</tt> attribute or supply any of the attributes listed
|
60
|
+
# in ATTRIBUTE_MAP to generate the subject name. For example:
|
61
|
+
#
|
62
|
+
# generate(:common_name => 'John Doe', :email_address => 'john@example.com')
|
63
|
+
#
|
64
|
+
# === Server certificate
|
65
|
+
#
|
66
|
+
# Server certificates are used to authenticate a server to a client and
|
67
|
+
# set up a secure socket. They are generated by setting the <tt>:type</tt>
|
68
|
+
# attribute to <tt>:server</tt>.
|
69
|
+
#
|
70
|
+
# ==== Compulsory
|
71
|
+
#
|
72
|
+
# * <tt>:ca_certificate</tt> - A CertificateDepot::Certificate instance
|
73
|
+
# representing the Certification Authority.
|
74
|
+
# * <tt>:serial_number</tt> - The serial number to store in the
|
75
|
+
# certificate. This number should be unique for certificates issued
|
76
|
+
# by the CA.
|
77
|
+
# * <tt>:public_key</tt> - A public key which is the sister key of the
|
78
|
+
# private key used by the client.
|
79
|
+
# * <tt>:common_name</tt> - The common name has to match the hostname used
|
80
|
+
# for the server. It has to be either a complete match or a wildcard
|
81
|
+
# match. So <tt>*.example.com</tt> will match <tt>www.example.com</tt>
|
82
|
+
# and <tt>mail.example.com</tt> but <tt>example.com</tt> will only match
|
83
|
+
# <tt>example.com</tt>.
|
84
|
+
#
|
85
|
+
# ==== Options
|
86
|
+
#
|
87
|
+
# If you want to supply an instance of OpenSSL::X509::Name as the
|
88
|
+
# <tt>:subject</tt> of the certificate, please make sure you set the CN
|
89
|
+
# attribute to the correct value of the certificate will be worthless.
|
90
|
+
#
|
91
|
+
# You can choose to set any of the other X509 attributes as found in the
|
92
|
+
# ATTRIBUTE_MAP, but none of the are strictly necessary.
|
93
|
+
#
|
94
|
+
# === Certification Authority certificate
|
95
|
+
#
|
96
|
+
# CA certificates are used to sign all certificates issued by the CA. They
|
97
|
+
# are generated by setting the <tt>:type</tt> to <tt>:ca</tt>.
|
98
|
+
#
|
99
|
+
# ==== Options
|
100
|
+
#
|
101
|
+
# You can choose to either supply an instance of OpenSSL::X509::Name as
|
102
|
+
# the <tt>:subject</tt> attribute or supply any of the attributes listed
|
103
|
+
# in ATTRIBUTE_MAP to generate the subject name. For example:
|
104
|
+
#
|
105
|
+
# subject = OpenSSL::X509::Name.new
|
106
|
+
# subject.add_entry('CN', 'Certificate Depot CA')
|
107
|
+
# generate(:subject => subject)
|
108
|
+
#
|
109
|
+
# generate(:common_name => 'Certificate Depot CA')
|
110
|
+
#
|
111
|
+
# Note that this name will be used for both the subject and issuer
|
112
|
+
# field in the certificate because it's a so-called
|
113
|
+
# sign-signed certificate.
|
114
|
+
def generate(attributes={})
|
115
|
+
from = Time.now
|
116
|
+
to = Time.now + DEFAULT_VALIDITY_PERIOD
|
117
|
+
|
118
|
+
name = attributes[:subject] || OpenSSL::X509::Name.new
|
119
|
+
ATTRIBUTE_MAP.each do |internal, x509_attribute|
|
120
|
+
name.add_entry(x509_attribute, attributes[internal]) if attributes[internal]
|
121
|
+
end
|
122
|
+
|
123
|
+
case attributes[:type]
|
124
|
+
when :client, :server
|
125
|
+
issuer = attributes[:ca_certificate].subject
|
126
|
+
serial = attributes[:serial_number]
|
127
|
+
when :ca
|
128
|
+
issuer = name
|
129
|
+
serial = 0
|
130
|
+
else
|
131
|
+
raise ArgumentError, "Unknown certificate type #{attributes[:type]}, please specify either :client, :server, or :ca"
|
132
|
+
end
|
133
|
+
|
134
|
+
raise ArgumentError, "Please supply a serial number for the certificate to generate" unless serial
|
135
|
+
|
136
|
+
@certificate = OpenSSL::X509::Certificate.new
|
137
|
+
@certificate.subject = name
|
138
|
+
@certificate.issuer = issuer
|
139
|
+
@certificate.not_before = from
|
140
|
+
@certificate.not_after = to
|
141
|
+
@certificate.version = X509v3
|
142
|
+
@certificate.public_key = attributes[:public_key]
|
143
|
+
@certificate.serial = serial
|
144
|
+
|
145
|
+
extensions = []
|
146
|
+
factory = OpenSSL::X509::ExtensionFactory.new
|
147
|
+
factory.subject_certificate = @certificate
|
148
|
+
|
149
|
+
case attributes[:type]
|
150
|
+
when :server
|
151
|
+
factory.issuer_certificate = attributes[:ca_certificate].certificate
|
152
|
+
extensions << factory.create_extension('basicConstraints', 'CA:FALSE', true)
|
153
|
+
extensions << factory.create_extension('keyUsage', 'digitalSignature,keyEncipherment')
|
154
|
+
extensions << factory.create_extension('extendedKeyUsage', 'serverAuth,clientAuth,emailProtection')
|
155
|
+
when :client
|
156
|
+
factory.issuer_certificate = attributes[:ca_certificate].certificate
|
157
|
+
extensions << factory.create_extension('basicConstraints', 'CA:FALSE', true)
|
158
|
+
extensions << factory.create_extension('keyUsage', 'nonRepudiation,digitalSignature,keyEncipherment')
|
159
|
+
extensions << factory.create_extension('extendedKeyUsage', 'clientAuth')
|
160
|
+
when :ca
|
161
|
+
factory.issuer_certificate = @certificate
|
162
|
+
extensions << factory.create_extension('basicConstraints', 'CA:TRUE', true)
|
163
|
+
extensions << factory.create_extension('keyUsage', 'cRLSign,keyCertSign')
|
164
|
+
end
|
165
|
+
extensions << factory.create_extension('subjectKeyIdentifier', 'hash')
|
166
|
+
extensions << factory.create_extension('authorityKeyIdentifier', 'keyid,issuer:always')
|
167
|
+
|
168
|
+
@certificate.extensions = extensions
|
169
|
+
|
170
|
+
if attributes[:private_key]
|
171
|
+
@certificate.sign(attributes[:private_key], OpenSSL::Digest::SHA1.new)
|
172
|
+
end
|
173
|
+
|
174
|
+
@certificate
|
175
|
+
end
|
176
|
+
|
177
|
+
# Writes the certificate to file. The path should be a filename pointing to
|
178
|
+
# an existing directory. Note that this will overwrite files without
|
179
|
+
# asking.
|
180
|
+
def write_to(path)
|
181
|
+
File.open(path, 'w') { |file| file.write(@certificate.to_pem) }
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns the public key in the certificate.
|
185
|
+
def public_key
|
186
|
+
@certificate.public_key
|
187
|
+
end
|
188
|
+
|
189
|
+
# Returns the issuer for the certificate.
|
190
|
+
def issuer
|
191
|
+
@certificate.issuer
|
192
|
+
end
|
193
|
+
|
194
|
+
# Returns the subject of the certificate.
|
195
|
+
def subject
|
196
|
+
@certificate.subject
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns the serial number of the certificate.
|
200
|
+
def serial_number
|
201
|
+
@certificate.serial
|
202
|
+
end
|
203
|
+
|
204
|
+
# Used to support easy querying of subject attributes. See ATTRIBUTE_MAP
|
205
|
+
# for a complete list of supported attributes.
|
206
|
+
#
|
207
|
+
# certificate.common_name #=> '*.example.com'
|
208
|
+
def method_missing(method, *attributes, &block)
|
209
|
+
if x509_attribute = ATTRIBUTE_MAP[method.to_sym]
|
210
|
+
self[x509_attribute]
|
211
|
+
else
|
212
|
+
super
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Used to support easy querying of subject attributes. See ATTRIBUTE_MAP
|
217
|
+
# for a complete list of supported attributes.
|
218
|
+
#
|
219
|
+
# certificate['common_name'] #=> '*.example.com'
|
220
|
+
def [](key)
|
221
|
+
@certificate.subject.to_a.each do |name, value, type|
|
222
|
+
return value if name == key
|
223
|
+
end; nil
|
224
|
+
end
|
225
|
+
|
226
|
+
# Shortcut for CertificateDepot::Certificate#generate. See this method for
|
227
|
+
# more details.
|
228
|
+
def self.generate(attributes={})
|
229
|
+
certificate = new
|
230
|
+
certificate.generate(attributes)
|
231
|
+
certificate
|
232
|
+
end
|
233
|
+
|
234
|
+
# Instantiates a new instance using certificate data read from file.
|
235
|
+
def self.from_file(path)
|
236
|
+
new(OpenSSL::X509::Certificate.new(File.read(path)))
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class CertificateDepot
|
2
|
+
# Represents an OpenSSL RSA key. Because RSA is part of a PKI the private
|
3
|
+
# key is usually paired with the public key.
|
4
|
+
class Keypair
|
5
|
+
DEFAULT_LENGTH = 2048
|
6
|
+
|
7
|
+
attr_accessor :private_key
|
8
|
+
|
9
|
+
# Instantiate a new Keypair with a private key. The private key should be
|
10
|
+
# an instance of OpenSSL::PKey::RSA.
|
11
|
+
def initialize(private_key=nil)
|
12
|
+
@private_key = private_key
|
13
|
+
end
|
14
|
+
|
15
|
+
# Generates a new private and public keypair.
|
16
|
+
def generate
|
17
|
+
@private_key = OpenSSL::PKey::RSA.generate(DEFAULT_LENGTH)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the public key
|
21
|
+
def public_key
|
22
|
+
@private_key.public_key
|
23
|
+
end
|
24
|
+
|
25
|
+
# Writes the keypair to file. The path should be a filename pointing to
|
26
|
+
# an existing directory. Note that this will overwrite files without
|
27
|
+
# asking.
|
28
|
+
def write_to(path)
|
29
|
+
File.open(path, 'w') { |file| file.write(@private_key.to_pem) }
|
30
|
+
File.chmod(0400, path)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Shortcut method to generate a new keypair.
|
34
|
+
#
|
35
|
+
# keypair = CertificateDepot::Keypair.generate
|
36
|
+
# keypair.write_to('/var/lib/depot/storage/my-key.key')
|
37
|
+
def self.generate
|
38
|
+
keypair = new
|
39
|
+
keypair.generate
|
40
|
+
keypair
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class CertificateDepot
|
2
|
+
# Simple thead-safe logger implementation.
|
3
|
+
#
|
4
|
+
# log = Log.new('/var/log/depot.log', :level => Log::INFO)
|
5
|
+
# log.fatal('I am completely operational, and all my circuits are functioning perfectly.')
|
6
|
+
# log.close
|
7
|
+
class Log
|
8
|
+
DEBUG = 0
|
9
|
+
INFO = 1
|
10
|
+
WARN = 2
|
11
|
+
ERROR = 3
|
12
|
+
FATAL = 4
|
13
|
+
UNKNOWN = 5
|
14
|
+
# Used to stop logging altogether
|
15
|
+
SILENT = 9
|
16
|
+
|
17
|
+
# Holds the current log file
|
18
|
+
attr_accessor :file
|
19
|
+
# Holds the current log level
|
20
|
+
attr_accessor :level
|
21
|
+
|
22
|
+
# Creates a new Log instance.
|
23
|
+
def initialize(file, options={})
|
24
|
+
@file = file
|
25
|
+
@level = options[:level] || DEBUG
|
26
|
+
end
|
27
|
+
|
28
|
+
# Log if the error level is debug or lower.
|
29
|
+
def debug(*args); log(DEBUG, *args); end
|
30
|
+
# Log if the error level is info or lower.
|
31
|
+
def info(*args); log(INFO, *args); end
|
32
|
+
# Log if the error level is warn or lower.
|
33
|
+
def warn(*args); log(WARN, *args); end
|
34
|
+
# Log if the error level is error or lower.
|
35
|
+
def error(*args); log(ERROR, *args); end
|
36
|
+
# Log if the error level is fatal or lower.
|
37
|
+
def fatal(*args); log(FATAL, *args); end
|
38
|
+
# Log if the error level is unknown or lower.
|
39
|
+
def unknown(*args); log(UNKNOWN, *args); end
|
40
|
+
|
41
|
+
# Writes a message to the log is the current loglevel is equal or greater than the message_level.
|
42
|
+
#
|
43
|
+
# log.log(Log::DEBUG, "This is a debug message")
|
44
|
+
def log(message_level, *args)
|
45
|
+
@file.flock(File::LOCK_EX)
|
46
|
+
@file.write(self.class.format(*args)) if message_level >= level
|
47
|
+
@file.flock(File::LOCK_UN)
|
48
|
+
rescue IOError
|
49
|
+
end
|
50
|
+
|
51
|
+
# Close the logger
|
52
|
+
def close
|
53
|
+
@file.close
|
54
|
+
end
|
55
|
+
|
56
|
+
# Format the log message
|
57
|
+
def self.format(*args)
|
58
|
+
["[#{Process.pid.to_s.rjust(5)}] ", Time.now.strftime("%Y-%m-%d %H:%M:%S"), '| ', args.first, "\n"].join
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
class CertificateDepot
|
4
|
+
# The Runner class handles commands issued to the command-line utility.
|
5
|
+
class Runner
|
6
|
+
def initialize(argv)
|
7
|
+
@argv = argv
|
8
|
+
@options = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns an option parser.
|
12
|
+
def parser
|
13
|
+
@parser ||= OptionParser.new do |opts|
|
14
|
+
# ---------------------------------------------------------------------------
|
15
|
+
opts.banner = "Usage: depot [command] [options]"
|
16
|
+
opts.separator ""
|
17
|
+
opts.separator "Commands:"
|
18
|
+
opts.separator " init <path> [name] Create a new depot on disk. You probably want"
|
19
|
+
opts.separator " to run init as root to make sure your keys"
|
20
|
+
opts.separator " will be safe."
|
21
|
+
opts.separator ""
|
22
|
+
opts.separator " generate <path> Create a new certificate. Writes a pem"
|
23
|
+
opts.separator " with a private key and a certificate to"
|
24
|
+
opts.separator " standard output"
|
25
|
+
opts.separator " --type Create a client or server certificate"
|
26
|
+
opts.separator ""
|
27
|
+
opts.separator " config <path> Shows a configuration example for Apache for"
|
28
|
+
opts.separator " the depot."
|
29
|
+
opts.separator ""
|
30
|
+
opts.separator " start <path> Start a server."
|
31
|
+
opts.separator ""
|
32
|
+
opts.separator " stop Stop a running server."
|
33
|
+
opts.separator ""
|
34
|
+
opts.separator "Options:"
|
35
|
+
opts.on("-c", "--cn [COMMON_NAME]", "Set the common name to use in the generated certificate") do |common_name|
|
36
|
+
@options[:common_name] = common_name
|
37
|
+
end
|
38
|
+
opts.on("-e", "--email [EMAIL]", "Set the email to use in the generated certificate") do |email|
|
39
|
+
@options[:email_address] = email
|
40
|
+
end
|
41
|
+
opts.on( "-u", "--uid [USERID]", "Set the user id to use in the generated certificate" ) do |user_id|
|
42
|
+
@options[:user_id] = user_id
|
43
|
+
end
|
44
|
+
opts.on("-t", "--type [TYPE]", "Generate a certificate of a certain type (server|client)") do |type|
|
45
|
+
@options[:type] = type.intern
|
46
|
+
end
|
47
|
+
opts.on("-H", "--host [HOST]", "IP address or hostname to listen on (127.0.0.1)") do |host|
|
48
|
+
@options[:host] = host
|
49
|
+
end
|
50
|
+
opts.on("-P", "--port [PORT]", "The port to listen on (35553)") do |port|
|
51
|
+
@options[:port] = port.to_i
|
52
|
+
end
|
53
|
+
opts.on("-n", "--process-count [COUNT]", "The number of worker processes to spawn (2)") do |process_count|
|
54
|
+
@options[:process_count] = process_count.to_i
|
55
|
+
end
|
56
|
+
opts.on("-q", "--max-connection-queue [MAX]", "The number of requests to queue on the server (10)") do |max_connection_queue|
|
57
|
+
@options[:max_connection_queue] = max_connection_queue.to_i
|
58
|
+
end
|
59
|
+
opts.on("-p", "--pid-file [PID_FILE]", "The file to store the server PID in (/var/run/depot.pid)") do |pid_file|
|
60
|
+
@options[:pid_file] = pid_file
|
61
|
+
end
|
62
|
+
opts.on("-l", "--log-file [LOG_FILE]", "The file to store the server log in (/var/log/depot.log)") do |log_file|
|
63
|
+
@options[:log_file] = log_file
|
64
|
+
end
|
65
|
+
opts.on("-h", "--help", "Show help") do
|
66
|
+
puts opts
|
67
|
+
exit
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Utility method which returns false if there is a path in argv. When
|
73
|
+
# there is no path in argv it returns true and prins a warning.
|
74
|
+
def no_path(argv)
|
75
|
+
if argv.length == 0
|
76
|
+
puts "[!] Please specify the path to the depot you want to operate on"
|
77
|
+
true
|
78
|
+
else
|
79
|
+
false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Runs command with arguments. Commands and arguments are documented in
|
84
|
+
# the help message of the command-line utility.
|
85
|
+
def run_command(command, argv)
|
86
|
+
path = File.expand_path(argv[0].to_s)
|
87
|
+
case command
|
88
|
+
when :init
|
89
|
+
return if no_path(argv)
|
90
|
+
if argv[1]
|
91
|
+
label = argv[1..-1].join(' ')
|
92
|
+
else
|
93
|
+
label = path.split('/').last
|
94
|
+
end
|
95
|
+
CertificateDepot.create(path, label, @options)
|
96
|
+
when :generate
|
97
|
+
return if no_path(argv)
|
98
|
+
unless [:server, :client].include?(@options[:type])
|
99
|
+
puts "[!] Unknown certificate type `#{@options[:type]}', please specify either server or client with the --type option"
|
100
|
+
else
|
101
|
+
keypair, certificate = CertificateDepot.generate_keypair_and_certificate(path, @options)
|
102
|
+
puts keypair.private_key.to_s
|
103
|
+
puts certificate.certificate.to_s
|
104
|
+
end
|
105
|
+
when :config
|
106
|
+
return if no_path(argv)
|
107
|
+
puts CertificateDepot.configuration_example(path)
|
108
|
+
when :start
|
109
|
+
return if no_path(argv)
|
110
|
+
if CertificateDepot.start(path, @options)
|
111
|
+
puts "[!] Starting server"
|
112
|
+
else
|
113
|
+
puts "[!] Can't start the server"
|
114
|
+
end
|
115
|
+
when :stop
|
116
|
+
if CertificateDepot.stop(@options)
|
117
|
+
puts "[!] Stopping server"
|
118
|
+
else
|
119
|
+
puts "[!] Can't find a running server"
|
120
|
+
end
|
121
|
+
else
|
122
|
+
puts parser.to_s
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Runs the command found in the arguments. If the arguments don't contain
|
127
|
+
# a command the help message is show.
|
128
|
+
def run
|
129
|
+
argv = @argv.dup
|
130
|
+
parser.parse!(argv)
|
131
|
+
if command = argv.shift
|
132
|
+
run_command(command.to_sym, argv)
|
133
|
+
else
|
134
|
+
puts parser.to_s
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
class CertificateDepot
|
5
|
+
# The CertificateDepot server is a pre-forking server. This basically means
|
6
|
+
# that it forks a pre-configured number of workers.
|
7
|
+
#
|
8
|
+
# Server
|
9
|
+
# |_ worker
|
10
|
+
# |_ worker
|
11
|
+
# |_ ...
|
12
|
+
#
|
13
|
+
# Workers hang around until something connects to the socket. The first
|
14
|
+
# worker to accept the request serves it. When workers die the server starts
|
15
|
+
# spawning new processes until it matches the configured process count.
|
16
|
+
# Workers are identified by their process ID or PID.
|
17
|
+
#
|
18
|
+
# The server creates a pipe between itself and each of the workers. We call
|
19
|
+
# these pipes lifelines. When a worker goes down the lifeline is severed.
|
20
|
+
# This is used as a signal by the server to spawn new workers. If the server
|
21
|
+
# goes down the workers use the same trick to notice this.
|
22
|
+
class Server
|
23
|
+
attr_accessor :socket, :depot
|
24
|
+
|
25
|
+
POSSIBLE_PID_FILES = ['/var/run/depot.pid', File.expand_path('~/.depot.pid')]
|
26
|
+
POSSIBLE_LOG_FILES = ['/var/log/depot.log', File.expand_path('~/depot.log')]
|
27
|
+
READ_BUFFER_SIZE = 16 * 1024
|
28
|
+
DEFAULTS = {
|
29
|
+
:host => '127.0.0.1',
|
30
|
+
:port => 35553,
|
31
|
+
:process_count => 2,
|
32
|
+
:max_connection_queue => 10
|
33
|
+
}
|
34
|
+
|
35
|
+
# Create a new server instance. The first argument is a CertificateDepot
|
36
|
+
# instance. The second argument contains overrides to the DEFAULTS.
|
37
|
+
def initialize(depot, options={})
|
38
|
+
@depot = depot
|
39
|
+
|
40
|
+
# Override the default with user supplied options.
|
41
|
+
@options = options.dup
|
42
|
+
DEFAULTS.keys.each do |key|
|
43
|
+
@options[key] ||= DEFAULTS[key]
|
44
|
+
end
|
45
|
+
|
46
|
+
# If someone specifies a PID file we have to try that instead of the
|
47
|
+
# default.
|
48
|
+
if pid_file = @options.delete(:pid_file)
|
49
|
+
@options[:possible_pid_files] = [pid_file]
|
50
|
+
else
|
51
|
+
@options[:possible_pid_files] = POSSIBLE_PID_FILES
|
52
|
+
end
|
53
|
+
|
54
|
+
# If someone specifies a log file we have to try that instead of the
|
55
|
+
# default.
|
56
|
+
if log_file = @options.delete(:log_file)
|
57
|
+
@options[:possible_log_files] = [log_file]
|
58
|
+
else
|
59
|
+
@options[:possible_log_files] = POSSIBLE_LOG_FILES
|
60
|
+
end
|
61
|
+
|
62
|
+
# Contains the lifelines to all the workers. They are indexed by the
|
63
|
+
# worker's PID.
|
64
|
+
@lifelines = {}
|
65
|
+
|
66
|
+
# Workers are instances of CertificateDepot::Worker. They are indexed by
|
67
|
+
# their own PID.
|
68
|
+
@workers = {}
|
69
|
+
|
70
|
+
# Signals received by the process
|
71
|
+
@signals = []
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns a Log object for the server
|
75
|
+
def log
|
76
|
+
if @log.nil?
|
77
|
+
@options[:possible_log_files].each do |log_file|
|
78
|
+
begin
|
79
|
+
file = File.open(log_file, File::WRONLY|File::APPEND|File::CREAT)
|
80
|
+
@log = CertificateDepot::Log.new(file)
|
81
|
+
rescue Errno::EACCES
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end; @log
|
85
|
+
end
|
86
|
+
|
87
|
+
# Start behaving like a server. This method returns once the server has
|
88
|
+
# completely started.
|
89
|
+
#
|
90
|
+
# Forks a process and starts a runloop in the fork. The runloop does
|
91
|
+
# worker housekeeping. It does so in three phases. First it removes all
|
92
|
+
# non-functional workers from its internal structures. After that it
|
93
|
+
# spawns new workers if it needs to. Finally it sleeps for a while so the
|
94
|
+
# the runloop doesn't keep busy all the time.
|
95
|
+
def run
|
96
|
+
log.info("Starting Certificate Depot server")
|
97
|
+
trap_signals
|
98
|
+
setup_socket
|
99
|
+
save_pid_to_file(fork do
|
100
|
+
# Generate a new process group so we're no longer a child process
|
101
|
+
# of the TTY.
|
102
|
+
Process.setsid
|
103
|
+
reroute_stdio
|
104
|
+
loop do
|
105
|
+
break if signals_want_shutdown?
|
106
|
+
reap_workers
|
107
|
+
spawn_workers
|
108
|
+
sleep
|
109
|
+
end
|
110
|
+
cleanup
|
111
|
+
end)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Installs signal traps to listen for incoming signals to the process.
|
115
|
+
def trap_signals
|
116
|
+
trap(:QUIT) { @signals << :QUIT }
|
117
|
+
trap(:EXIT) { @signals << :EXIT }
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns true when the signals received by the process demand a shutdown
|
121
|
+
def signals_want_shutdown?
|
122
|
+
!@signals.empty?
|
123
|
+
end
|
124
|
+
|
125
|
+
# Make all output of interpreter go to the logfile
|
126
|
+
def reroute_stdio
|
127
|
+
$stdout = log.file
|
128
|
+
$stderr = log.file
|
129
|
+
end
|
130
|
+
|
131
|
+
# Write the PID of the process with the mainloop to the filesystem so we
|
132
|
+
# read it later on to signal the server to shutdown.
|
133
|
+
def save_pid_to_file(pid)
|
134
|
+
@options[:possible_pid_files].each do |pid_file|
|
135
|
+
begin
|
136
|
+
File.open(pid_file, 'w') { |file| file.write(pid.to_s) }
|
137
|
+
log.debug("Writing PID to `#{pid_file}'")
|
138
|
+
return pid_file
|
139
|
+
rescue Errno::EACCES
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Reads the PID of the process with the mainloop from the filesystem. Used
|
145
|
+
# for sending signals to a running server.
|
146
|
+
def load_pid_from_file
|
147
|
+
best_match = @options[:possible_pid_files].inject([]) do |matches, pid_file|
|
148
|
+
begin
|
149
|
+
log.debug("Considering reading PID from `#{pid_file}'")
|
150
|
+
possibility = [File.atime(pid_file), File.read(pid_file).to_i]
|
151
|
+
log.debug(" - created #{possibility[0]}, contains PID: #{possibility[1]}")
|
152
|
+
matches << possibility
|
153
|
+
rescue Errno::EACCES, Errno::ENOENT
|
154
|
+
end; matches
|
155
|
+
end.compact.sort.last
|
156
|
+
best_match[1] if best_match
|
157
|
+
end
|
158
|
+
|
159
|
+
# Removes all possible PID files.
|
160
|
+
def remove_pid_file
|
161
|
+
@options[:possible_pid_files].each do |pid_file|
|
162
|
+
begin
|
163
|
+
File.unlink(pid_file)
|
164
|
+
log.debug("Removed PID file `#{pid_file}'")
|
165
|
+
rescue Errno::EACCES, Errno::ENOENT
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Figures out if any workers died and deletes them from internal
|
171
|
+
# structures if they did.
|
172
|
+
def reap_workers
|
173
|
+
# Don't try to find more dead workers than the process count
|
174
|
+
@workers.length.times do
|
175
|
+
# We use +waitpid+ to find any child process which has exited. It
|
176
|
+
# immediately returns when there aren't any dead processes.
|
177
|
+
if pid = Process.waitpid(-1, Process::WNOHANG)
|
178
|
+
despawn_worker(pid)
|
179
|
+
else
|
180
|
+
return # Stop when we don't find any
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Deletes references to workers from the server instance
|
186
|
+
def despawn_worker(pid)
|
187
|
+
log.debug("Removing worker #{pid}")
|
188
|
+
@workers.delete(pid)
|
189
|
+
@lifelines.delete(pid)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Figures out how many workers are currently running and creates new ones
|
193
|
+
# if needed.
|
194
|
+
def spawn_workers
|
195
|
+
missing_workers.times do
|
196
|
+
worker = CertificateDepot::Worker.new(self)
|
197
|
+
|
198
|
+
lifeline = IO.pipe
|
199
|
+
|
200
|
+
pid = fork do
|
201
|
+
# We close the server side of the pipe in this process otherwise we
|
202
|
+
# don't get a EOF when reading from it.
|
203
|
+
lifeline.first.close
|
204
|
+
worker.lifeline = lifeline.last
|
205
|
+
worker.run
|
206
|
+
end
|
207
|
+
|
208
|
+
@workers[pid] = worker
|
209
|
+
@lifelines[pid] = lifeline.first
|
210
|
+
|
211
|
+
# We close the client side of the pipe in this process otherwise we
|
212
|
+
# don't get an EOF when reading from it.
|
213
|
+
lifeline.last.close
|
214
|
+
|
215
|
+
log.debug("Spawned worker #{pid}")
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Sleeps until someone wants the server main loop to wake up or when 2
|
220
|
+
# seconds go by. Workers can wake the server in two ways, either by
|
221
|
+
# writing anything to their lifeline or by severing the lifeline. The
|
222
|
+
# lifeline is severed when the worker dies.
|
223
|
+
def sleep
|
224
|
+
# Returns with active IO objects if any of them are written to.
|
225
|
+
# Otherwise it times out after two seconds.
|
226
|
+
if needy = IO.select(@lifelines.values, nil, @lifelines.values, 2)
|
227
|
+
log.debug("Detected activity on: #{needy.inspect}")
|
228
|
+
# Read everything coming in on the lifelines and discard it because
|
229
|
+
# the contents doesn't matter.
|
230
|
+
needy.flatten.each do |lifeline|
|
231
|
+
loop { lifeline.read_nonblock(READ_BUFFER_SIZE) } unless lifeline.closed?
|
232
|
+
end if needy
|
233
|
+
end
|
234
|
+
rescue EOFError, Errno::EAGAIN, Errno::EINTR, Errno::EBADF, IOError
|
235
|
+
end
|
236
|
+
|
237
|
+
# Cleanup all server resources.
|
238
|
+
def cleanup
|
239
|
+
log.info("Shutting down")
|
240
|
+
@lifelines.each do |pid, lifeline|
|
241
|
+
begin
|
242
|
+
lifeline.close
|
243
|
+
rescue IOError
|
244
|
+
end
|
245
|
+
end
|
246
|
+
socket.close
|
247
|
+
remove_pid_file
|
248
|
+
end
|
249
|
+
|
250
|
+
# Creates the socket the server listens on and binds it to the configured
|
251
|
+
# host and port.
|
252
|
+
def setup_socket
|
253
|
+
self.socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
254
|
+
address = Socket.pack_sockaddr_in(@options[:port], @options[:host])
|
255
|
+
socket.bind(address)
|
256
|
+
socket.listen(@options[:max_connection_queue])
|
257
|
+
log.info("Listening on #{@options[:host]}:#{@options[:port]}")
|
258
|
+
end
|
259
|
+
|
260
|
+
# Returns the number of workers that need to be created in order to get to
|
261
|
+
# the configured process count.
|
262
|
+
def missing_workers
|
263
|
+
@options[:process_count] - @workers.length
|
264
|
+
end
|
265
|
+
|
266
|
+
# Sends the QUIT signal to the server process.
|
267
|
+
def kill
|
268
|
+
Process.kill(:QUIT, load_pid_from_file)
|
269
|
+
true
|
270
|
+
rescue Errno::ESRCH
|
271
|
+
false
|
272
|
+
end
|
273
|
+
|
274
|
+
# Creates a new server instance and starts listening on its configured
|
275
|
+
# host and port. Returns once the server was started.
|
276
|
+
def self.start(depot, options={})
|
277
|
+
server = new(depot, options)
|
278
|
+
server.run
|
279
|
+
end
|
280
|
+
|
281
|
+
# Finds the server PID and kills it causing the workers to go down as well.
|
282
|
+
def self.stop(options={})
|
283
|
+
server = new(nil, options)
|
284
|
+
server.kill
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class CertificateDepot
|
2
|
+
# Manages a directory with certificates. It's mainly used by the depot to
|
3
|
+
# generate a unique serial for its certificates.
|
4
|
+
class Store
|
5
|
+
attr_accessor :path
|
6
|
+
|
7
|
+
# Creates a new Store instance. The path should be a directory containing
|
8
|
+
# certificates in PEM format.
|
9
|
+
def initialize(path)
|
10
|
+
@path = path
|
11
|
+
@certificates = []
|
12
|
+
load
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the number of certificates in the store.
|
16
|
+
def size
|
17
|
+
@certificates.size
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns an unused serial which can be used to generate a new certificate
|
21
|
+
# for the store.
|
22
|
+
def next_serial_number
|
23
|
+
size + 1
|
24
|
+
end
|
25
|
+
|
26
|
+
# Append a certificate to the store.
|
27
|
+
def <<(certificate)
|
28
|
+
@certificates << certificate
|
29
|
+
end
|
30
|
+
|
31
|
+
# Writes all unsaved certificates to disk.
|
32
|
+
def sync
|
33
|
+
@certificates.each do |certificate|
|
34
|
+
certificate_path = File.join(@path, "#{certificate.serial_number}.crt")
|
35
|
+
unless File.exist?(certificate_path)
|
36
|
+
certificate.write_to(certificate_path)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Reads all certificates from disk.
|
42
|
+
def load
|
43
|
+
(Dir.entries(@path) - %w(. .. ca.crt)).each do |entry|
|
44
|
+
certificate_path = File.join(@path, entry)
|
45
|
+
self << CertificateDepot::Certificate.from_file(certificate_path)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
include Enumerable
|
50
|
+
|
51
|
+
def each(&block)
|
52
|
+
@certificates.each(&block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
class CertificateDepot
|
2
|
+
# A worker runs in a separate process created by the server. It hangs around
|
3
|
+
# and polls the server socket for incoming connections. Once it finds one it
|
4
|
+
# tried to process it.
|
5
|
+
#
|
6
|
+
# The worker has a lifeline to the server. The lifeline is a pipe used to
|
7
|
+
# signal the server. When the worker goes down the server can detect the
|
8
|
+
# severed lifeline. If the worker needs the server to stop sleeping it can
|
9
|
+
# write to the lifeline.
|
10
|
+
class Worker
|
11
|
+
attr_accessor :server, :lifeline
|
12
|
+
|
13
|
+
# Creates a new worker instance. The first argument is a server instance.
|
14
|
+
def initialize(server)
|
15
|
+
@server = server
|
16
|
+
@signals = []
|
17
|
+
end
|
18
|
+
|
19
|
+
# Generates a new client certificate and writes it to the socket
|
20
|
+
def generate(socket, distinguished_name)
|
21
|
+
attributes = {
|
22
|
+
:type => :client,
|
23
|
+
:subject => distinguished_name
|
24
|
+
}
|
25
|
+
keypair, certificate = server.depot.generate_keypair_and_certificate(attributes)
|
26
|
+
socket.write keypair.private_key.to_s
|
27
|
+
socket.write certificate.certificate.to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
# Writes help to the socket about a topic or a list of commands
|
31
|
+
def help(socket, command)
|
32
|
+
if command
|
33
|
+
socket.write(self.class.help(command.downcase))
|
34
|
+
else
|
35
|
+
socket.write("generate help shutdown\n")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Runs a command and writes the result to the request socket.
|
40
|
+
def run_command(socket, *args)
|
41
|
+
args = args.dup
|
42
|
+
command = args.shift
|
43
|
+
|
44
|
+
case command
|
45
|
+
when :generate
|
46
|
+
generate(socket, args[0])
|
47
|
+
when :help
|
48
|
+
help(socket, args[0])
|
49
|
+
when :shutdown
|
50
|
+
exit 1
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Processes an incoming request. Parse the command, run the command, and
|
55
|
+
# close the socket.
|
56
|
+
def process_incoming_socket(socket, address)
|
57
|
+
input = socket.gets
|
58
|
+
server.log.debug("Got input: #{input.strip}")
|
59
|
+
run_command(socket, *self.class.parse_command(input))
|
60
|
+
socket.close
|
61
|
+
end
|
62
|
+
|
63
|
+
# Starts the mainloop for the worker. The mainloop sleeps until one of the
|
64
|
+
# following three things happens: server gets a new request, activity on
|
65
|
+
# the lifeline to the server, or 2 seconds go by.
|
66
|
+
def run
|
67
|
+
trap_signals
|
68
|
+
loop do
|
69
|
+
break if signals_want_shutdown
|
70
|
+
begin
|
71
|
+
# IO.select returns either a triplet of lists with IO objects that
|
72
|
+
# need attention or nil on timeout of 2 seconds.
|
73
|
+
if needy = IO.select([server.socket, lifeline], nil, [server.socket, lifeline], 2)
|
74
|
+
server.log.debug("Detected activity on: #{needy.inspect}")
|
75
|
+
# If the lifeline is active the server went down and we need to go
|
76
|
+
# down as well.
|
77
|
+
break if needy.flatten.any? { |io| !io.respond_to?(:accept_nonblock) }
|
78
|
+
# Otherwise we handle incoming requests
|
79
|
+
needy.flatten.each do |io|
|
80
|
+
if io.respond_to?(:accept_nonblock)
|
81
|
+
begin
|
82
|
+
process_incoming_socket(*io.accept_nonblock)
|
83
|
+
rescue Errno::EAGAIN, Errno::ECONNABORTED
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
rescue EOFError, Errno::EAGAIN, Errno::EINTR, Errno::EBADF, IOError
|
89
|
+
end
|
90
|
+
end
|
91
|
+
cleanup
|
92
|
+
end
|
93
|
+
|
94
|
+
# Cleanup all worker resources
|
95
|
+
def cleanup
|
96
|
+
server.log.info("Shutting down")
|
97
|
+
begin
|
98
|
+
lifeline.close
|
99
|
+
rescue Errno::EPIPE
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Installs signal traps to listen for incoming signals to the process.
|
104
|
+
def trap_signals
|
105
|
+
trap(:QUIT) { @signals << :QUIT }
|
106
|
+
trap(:EXIT) { @signals << :EXIT }
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns true when the signals received by the process demand a shutdown
|
110
|
+
def signals_want_shutdown
|
111
|
+
!@signals.empty?
|
112
|
+
end
|
113
|
+
|
114
|
+
# Parses a command issues by a client.
|
115
|
+
def self.parse_command(command)
|
116
|
+
parts = command.split(' ')
|
117
|
+
parts[0] = parts[0].intern if parts[0]
|
118
|
+
|
119
|
+
case parts[0]
|
120
|
+
when :generate
|
121
|
+
parts[1] = OpenSSL::X509::Name.parse(parts[1].to_s) if parts[1]
|
122
|
+
when :revoke
|
123
|
+
parts[1] = parts[1].to_i if parts[1]
|
124
|
+
end
|
125
|
+
|
126
|
+
parts
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns help text for a certain command
|
130
|
+
def self.help(command)
|
131
|
+
case command
|
132
|
+
when 'generate'
|
133
|
+
"GENERATE
|
134
|
+
generate <distinguished name>
|
135
|
+
RETURNS
|
136
|
+
A private key and certificate in PEM format.
|
137
|
+
EXAMPLE
|
138
|
+
generate /UID=12/CN=Bob Owner,emailAddress=bob@example.com
|
139
|
+
"
|
140
|
+
when 'help'
|
141
|
+
"HELP
|
142
|
+
help <command>
|
143
|
+
RETURNS
|
144
|
+
A description of the command.
|
145
|
+
EXAMPLE
|
146
|
+
help generate
|
147
|
+
"
|
148
|
+
when 'shutdown'
|
149
|
+
"SHUTDOWN
|
150
|
+
shutdown
|
151
|
+
RETURNS
|
152
|
+
Kills the current worker handling the request.
|
153
|
+
EXAMPLE
|
154
|
+
shutdown
|
155
|
+
"
|
156
|
+
else
|
157
|
+
"Unknown command #{command}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: certificate-depot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
version: 0.3.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Manfred Stienstra
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-07-27 00:00:00 +02:00
|
18
|
+
default_executable: depot
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: mocha
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :development
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: test-spec
|
34
|
+
prerelease: false
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 0
|
41
|
+
version: "0"
|
42
|
+
type: :development
|
43
|
+
version_requirements: *id002
|
44
|
+
description: Certificate depot is a mini Certification Authority for TLS client certificates.
|
45
|
+
email: manfred@fngtps.com
|
46
|
+
executables:
|
47
|
+
- depot
|
48
|
+
extensions: []
|
49
|
+
|
50
|
+
extra_rdoc_files:
|
51
|
+
- TODO
|
52
|
+
files:
|
53
|
+
- bin/depot
|
54
|
+
- lib/certificate_depot.rb
|
55
|
+
- lib/certificate_depot/certificate.rb
|
56
|
+
- lib/certificate_depot/keypair.rb
|
57
|
+
- lib/certificate_depot/log.rb
|
58
|
+
- lib/certificate_depot/runner.rb
|
59
|
+
- lib/certificate_depot/server.rb
|
60
|
+
- lib/certificate_depot/store.rb
|
61
|
+
- lib/certificate_depot/worker.rb
|
62
|
+
- TODO
|
63
|
+
has_rdoc: true
|
64
|
+
homepage:
|
65
|
+
licenses: []
|
66
|
+
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options:
|
69
|
+
- --charset=UTF-8
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.3.6
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Certificate depot is a mini Certification Authority for TLS client certificates.
|
93
|
+
test_files: []
|
94
|
+
|