certificate-depot 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/TODO 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
+