certificate-depot 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|