certificate-depot 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+ * Allow people to set more subject information for the CA certificate
2
+ * Validate input
3
+ - Email address when creating a certificate
4
+ - Common name for server certificate
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # When it's invoked directly and not from Rubygems
4
+ if $0 == __FILE__
5
+ $:.unshift(File.expand_path('../../lib', __FILE__))
6
+ end
7
+
8
+ require 'certificate_depot'
9
+ CertificateDepot.run(ARGV)
@@ -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
+