r509 0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (162) hide show
  1. data/README.md +447 -0
  2. data/Rakefile +38 -0
  3. data/bin/r509 +96 -0
  4. data/bin/r509-parse +35 -0
  5. data/doc/R509.html +154 -0
  6. data/doc/R509/Cert.html +3954 -0
  7. data/doc/R509/Cert/Extensions.html +360 -0
  8. data/doc/R509/Cert/Extensions/AuthorityInfoAccess.html +391 -0
  9. data/doc/R509/Cert/Extensions/AuthorityKeyIdentifier.html +148 -0
  10. data/doc/R509/Cert/Extensions/BasicConstraints.html +482 -0
  11. data/doc/R509/Cert/Extensions/CrlDistributionPoints.html +316 -0
  12. data/doc/R509/Cert/Extensions/ExtendedKeyUsage.html +780 -0
  13. data/doc/R509/Cert/Extensions/KeyUsage.html +1230 -0
  14. data/doc/R509/Cert/Extensions/SubjectAlternativeName.html +467 -0
  15. data/doc/R509/Cert/Extensions/SubjectKeyIdentifier.html +216 -0
  16. data/doc/R509/CertificateAuthority.html +126 -0
  17. data/doc/R509/CertificateAuthority/Signer.html +855 -0
  18. data/doc/R509/Config.html +127 -0
  19. data/doc/R509/Config/CaConfig.html +2144 -0
  20. data/doc/R509/Config/CaConfigPool.html +599 -0
  21. data/doc/R509/Config/CaProfile.html +656 -0
  22. data/doc/R509/Config/SubjectItemPolicy.html +578 -0
  23. data/doc/R509/Crl.html +126 -0
  24. data/doc/R509/Crl/Administrator.html +2077 -0
  25. data/doc/R509/Crl/Parser.html +1224 -0
  26. data/doc/R509/Csr.html +2248 -0
  27. data/doc/R509/IOHelpers.html +564 -0
  28. data/doc/R509/MessageDigest.html +396 -0
  29. data/doc/R509/NameSanitizer.html +319 -0
  30. data/doc/R509/Ocsp.html +128 -0
  31. data/doc/R509/Ocsp/Request.html +126 -0
  32. data/doc/R509/Ocsp/Request/Nonce.html +160 -0
  33. data/doc/R509/Ocsp/Response.html +837 -0
  34. data/doc/R509/OidMapper.html +393 -0
  35. data/doc/R509/PrivateKey.html +1647 -0
  36. data/doc/R509/R509Error.html +134 -0
  37. data/doc/R509/Spki.html +1424 -0
  38. data/doc/R509/Subject.html +836 -0
  39. data/doc/R509/Validity.html +160 -0
  40. data/doc/R509/Validity/Checker.html +320 -0
  41. data/doc/R509/Validity/DefaultChecker.html +283 -0
  42. data/doc/R509/Validity/DefaultWriter.html +330 -0
  43. data/doc/R509/Validity/Status.html +561 -0
  44. data/doc/R509/Validity/Writer.html +394 -0
  45. data/doc/_index.html +501 -0
  46. data/doc/class_list.html +53 -0
  47. data/doc/css/common.css +1 -0
  48. data/doc/css/full_list.css +57 -0
  49. data/doc/css/style.css +328 -0
  50. data/doc/file.README.html +534 -0
  51. data/doc/file.r509.html +149 -0
  52. data/doc/file_list.html +58 -0
  53. data/doc/frames.html +28 -0
  54. data/doc/index.html +534 -0
  55. data/doc/js/app.js +208 -0
  56. data/doc/js/full_list.js +173 -0
  57. data/doc/js/jquery.js +4 -0
  58. data/doc/methods_list.html +1932 -0
  59. data/doc/top-level-namespace.html +112 -0
  60. data/lib/r509.rb +22 -0
  61. data/lib/r509/cert.rb +414 -0
  62. data/lib/r509/cert/extensions.rb +309 -0
  63. data/lib/r509/certificateauthority.rb +290 -0
  64. data/lib/r509/config.rb +407 -0
  65. data/lib/r509/crl.rb +379 -0
  66. data/lib/r509/csr.rb +324 -0
  67. data/lib/r509/exceptions.rb +5 -0
  68. data/lib/r509/io_helpers.rb +52 -0
  69. data/lib/r509/messagedigest.rb +49 -0
  70. data/lib/r509/ocsp.rb +85 -0
  71. data/lib/r509/oidmapper.rb +32 -0
  72. data/lib/r509/privatekey.rb +185 -0
  73. data/lib/r509/spki.rb +112 -0
  74. data/lib/r509/subject.rb +133 -0
  75. data/lib/r509/validity.rb +92 -0
  76. data/lib/r509/version.rb +4 -0
  77. data/r509.yaml +73 -0
  78. data/spec/cert/extensions_spec.rb +632 -0
  79. data/spec/cert_spec.rb +321 -0
  80. data/spec/certificate_authority_spec.rb +260 -0
  81. data/spec/config_spec.rb +349 -0
  82. data/spec/crl_spec.rb +215 -0
  83. data/spec/csr_spec.rb +302 -0
  84. data/spec/fixtures.rb +233 -0
  85. data/spec/fixtures/cert1.der +0 -0
  86. data/spec/fixtures/cert1.pem +24 -0
  87. data/spec/fixtures/cert1_public_key_modulus.txt +1 -0
  88. data/spec/fixtures/cert3.p12 +0 -0
  89. data/spec/fixtures/cert3.pem +28 -0
  90. data/spec/fixtures/cert3_key.pem +27 -0
  91. data/spec/fixtures/cert3_key_des3.pem +30 -0
  92. data/spec/fixtures/cert4.pem +14 -0
  93. data/spec/fixtures/cert5.pem +30 -0
  94. data/spec/fixtures/cert6.pem +26 -0
  95. data/spec/fixtures/cert_expired.pem +26 -0
  96. data/spec/fixtures/cert_not_yet_valid.pem +26 -0
  97. data/spec/fixtures/cert_san.pem +27 -0
  98. data/spec/fixtures/cert_san2.pem +22 -0
  99. data/spec/fixtures/config_pool_test_minimal.yaml +15 -0
  100. data/spec/fixtures/config_test.yaml +41 -0
  101. data/spec/fixtures/config_test_engine_key.yaml +7 -0
  102. data/spec/fixtures/config_test_engine_no_key_name.yaml +6 -0
  103. data/spec/fixtures/config_test_minimal.yaml +7 -0
  104. data/spec/fixtures/config_test_password.yaml +7 -0
  105. data/spec/fixtures/config_test_various.yaml +100 -0
  106. data/spec/fixtures/crl_list_file.txt +1 -0
  107. data/spec/fixtures/crl_with_reason.pem +17 -0
  108. data/spec/fixtures/csr1.der +0 -0
  109. data/spec/fixtures/csr1.pem +17 -0
  110. data/spec/fixtures/csr1_key.der +0 -0
  111. data/spec/fixtures/csr1_key.pem +27 -0
  112. data/spec/fixtures/csr1_key_encrypted_des3.pem +30 -0
  113. data/spec/fixtures/csr1_newlines.pem +32 -0
  114. data/spec/fixtures/csr1_no_begin_end.pem +15 -0
  115. data/spec/fixtures/csr1_public_key_modulus.txt +1 -0
  116. data/spec/fixtures/csr2.pem +15 -0
  117. data/spec/fixtures/csr2_key.pem +27 -0
  118. data/spec/fixtures/csr3.pem +16 -0
  119. data/spec/fixtures/csr4.pem +25 -0
  120. data/spec/fixtures/csr_dsa.pem +15 -0
  121. data/spec/fixtures/csr_invalid_signature.pem +13 -0
  122. data/spec/fixtures/dsa_key.pem +20 -0
  123. data/spec/fixtures/key4.pem +27 -0
  124. data/spec/fixtures/key4_encrypted_des3.pem +30 -0
  125. data/spec/fixtures/missing_key_identifier_ca.cer +21 -0
  126. data/spec/fixtures/missing_key_identifier_ca.key +27 -0
  127. data/spec/fixtures/ocsptest.r509.local.pem +27 -0
  128. data/spec/fixtures/ocsptest.r509.local_ocsp_request.der +0 -0
  129. data/spec/fixtures/ocsptest2.r509.local.pem +27 -0
  130. data/spec/fixtures/second_ca.cer +26 -0
  131. data/spec/fixtures/second_ca.key +27 -0
  132. data/spec/fixtures/spkac.der +0 -0
  133. data/spec/fixtures/spkac.txt +1 -0
  134. data/spec/fixtures/spkac_dsa.txt +1 -0
  135. data/spec/fixtures/stca.pem +22 -0
  136. data/spec/fixtures/stca_ocsp_request.der +0 -0
  137. data/spec/fixtures/stca_ocsp_response.der +0 -0
  138. data/spec/fixtures/test1.csr +17 -0
  139. data/spec/fixtures/test_ca.cer +22 -0
  140. data/spec/fixtures/test_ca.key +28 -0
  141. data/spec/fixtures/test_ca.p12 +0 -0
  142. data/spec/fixtures/test_ca_des3.key +30 -0
  143. data/spec/fixtures/test_ca_ocsp.cer +26 -0
  144. data/spec/fixtures/test_ca_ocsp.key +27 -0
  145. data/spec/fixtures/test_ca_ocsp.p12 +0 -0
  146. data/spec/fixtures/test_ca_ocsp_chain.txt +48 -0
  147. data/spec/fixtures/test_ca_ocsp_response.der +0 -0
  148. data/spec/fixtures/test_ca_subroot.cer +26 -0
  149. data/spec/fixtures/test_ca_subroot.key +27 -0
  150. data/spec/fixtures/test_ca_subroot_ocsp.cer +25 -0
  151. data/spec/fixtures/test_ca_subroot_ocsp.key +27 -0
  152. data/spec/fixtures/test_ca_subroot_ocsp_response.der +0 -0
  153. data/spec/fixtures/unknown_oid.csr +17 -0
  154. data/spec/message_digest_spec.rb +89 -0
  155. data/spec/ocsp_spec.rb +111 -0
  156. data/spec/oid_mapper_spec.rb +31 -0
  157. data/spec/privatekey_spec.rb +198 -0
  158. data/spec/spec_helper.rb +14 -0
  159. data/spec/spki_spec.rb +157 -0
  160. data/spec/subject_spec.rb +203 -0
  161. data/spec/validity_spec.rb +98 -0
  162. metadata +257 -0
@@ -0,0 +1,407 @@
1
+ require 'yaml'
2
+ require 'openssl'
3
+ require 'r509/exceptions'
4
+ require 'r509/io_helpers'
5
+ require 'r509/subject'
6
+ require 'r509/privatekey'
7
+ require 'fileutils'
8
+ require 'pathname'
9
+
10
+ module R509
11
+ # Module to contain all configuration related classes (e.g. CaConfig, CaProfile, SubjectItemPolicy)
12
+ module Config
13
+ # Provides access to configuration profiles
14
+ class CaProfile
15
+ attr_reader :basic_constraints, :key_usage, :extended_key_usage,
16
+ :certificate_policies, :subject_item_policy
17
+
18
+ # @option [String] :basic_constraints
19
+ # @option [Array] :key_usage
20
+ # @option [Array] :extended_key_usage
21
+ # @option [Array] :certificate_policies
22
+ # @option [R509::Config::SubjectItemPolicy] :subject_item_policy optional
23
+ def initialize(opts = {})
24
+ @basic_constraints = opts[:basic_constraints]
25
+ @key_usage = opts[:key_usage]
26
+ @extended_key_usage = opts[:extended_key_usage]
27
+ @certificate_policies = opts[:certificate_policies]
28
+ if opts.has_key?(:subject_item_policy) and not opts[:subject_item_policy].kind_of?(R509::Config::SubjectItemPolicy)
29
+ end
30
+ @subject_item_policy = opts[:subject_item_policy] || nil
31
+ end
32
+ end
33
+
34
+ # returns information about the subject item policy for a profile
35
+ class SubjectItemPolicy
36
+ attr_reader :required, :optional
37
+
38
+ # @param [Hash] hash of required/optional subject items. These must be in OpenSSL shortname format.
39
+ # @example sample hash
40
+ # {"CN" => "required",
41
+ # "O" => "required",
42
+ # "OU" => "optional",
43
+ # "ST" => "required",
44
+ # "C" => "required",
45
+ # "L" => "required",
46
+ # "emailAddress" => "optional"}
47
+ def initialize(hash={})
48
+ if not hash.kind_of?(Hash)
49
+ raise ArgumentError, "Must supply a hash in form 'shortname'=>'required/optional'"
50
+ end
51
+ @required = []
52
+ @optional = []
53
+ if not hash.empty?
54
+ hash.each_pair do |key,value|
55
+ if value == "required"
56
+ @required.push(key)
57
+ elsif value == "optional"
58
+ @optional.push(key)
59
+ else
60
+ raise ArgumentError, "Unknown subject item policy value. Allowed values are required and optional"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # @param [R509::Subject] subject
67
+ # @return [R509::Subject] validated version of the subject or error
68
+ def validate_subject(subject)
69
+ # convert the subject components into an array of component names that match
70
+ # those that are on the required list
71
+ supplied = subject.to_a.each do |item|
72
+ @required.include?(item[0])
73
+ end.map do |item|
74
+ item[0]
75
+ end
76
+ # so we can make sure they gave us everything that's required
77
+ diff = @required - supplied
78
+ if not diff.empty?
79
+ raise R509::R509Error, "This profile requires you supply "+@required.join(", ")
80
+ end
81
+
82
+ # the validated subject contains only those subject components that are either
83
+ # required or optional
84
+ R509::Subject.new(subject.to_a.select do |item|
85
+ @required.include?(item[0]) or @optional.include?(item[0])
86
+ end)
87
+ end
88
+ end
89
+
90
+ # pool of configs, so we can support multiple CAs from a single config file
91
+ class CaConfigPool
92
+ # @option configs [Hash<String, R509::Config::CaConfig>] the configs to add to the pool
93
+ def initialize(configs)
94
+ @configs = configs
95
+ end
96
+
97
+ # get all the config names
98
+ def names
99
+ @configs.keys
100
+ end
101
+
102
+ # retrieve a particular config by its name
103
+ def [](name)
104
+ @configs[name]
105
+ end
106
+
107
+ # @return a list of all the configs in this pool
108
+ def all
109
+ @configs.values
110
+ end
111
+
112
+ # Loads the named configuration config from a yaml string.
113
+ # @param [String] name The name of the config within the file. Note
114
+ # that a single yaml file can contain more than one configuration.
115
+ # @param [String] yaml_data The filename to load yaml config data from.
116
+ def self.from_yaml(name, yaml_data, opts = {})
117
+ conf = YAML.load(yaml_data)
118
+ configs = {}
119
+ conf[name].each_pair do |ca_name, data|
120
+ configs[ca_name] = R509::Config::CaConfig.load_from_hash(data, opts)
121
+ end
122
+ R509::Config::CaConfigPool.new(configs)
123
+ end
124
+ end
125
+
126
+ # Stores a configuration for our CA.
127
+ class CaConfig
128
+ include R509::IOHelpers
129
+ extend R509::IOHelpers
130
+ attr_accessor :ca_cert, :crl_validity_hours, :message_digest,
131
+ :cdp_location, :crl_start_skew_seconds, :ocsp_location, :ocsp_chain,
132
+ :ocsp_start_skew_seconds, :ocsp_validity_hours, :crl_number_file, :crl_list_file
133
+
134
+ # @option opts [R509::Cert] :ca_cert Cert+Key pair
135
+ # @option opts [Integer] :crl_validity_hours (168) The number of hours that
136
+ # a CRL will be valid. Defaults to 7 days.
137
+ # @option opts [Hash<String, R509::Config::CaProfile>] :profiles
138
+ # @option opts [String] :message_digest (SHA1) The hashing algorithm to use.
139
+ # @option opts [String] :cdp_location
140
+ # @option opts [String] :ocsp_location
141
+ # @option opts [String] :crl_number_file The file that we will save
142
+ # the CRL numbers to. defaults to a StringIO object if not provided
143
+ # @option opts [String] :crl_list_file The file that we will save
144
+ # the CRL list data to. defaults to a StringIO object if not provided
145
+ # @option opts [R509::Cert] :ocsp_cert An optional cert+key pair
146
+ # OCSP signing delegate
147
+ # @option opts [Array<OpenSSL::X509::Certificate>] :ocsp_chain An optional array
148
+ # that constitutes the chain to attach to an OCSP response
149
+ #
150
+ def initialize(opts = {} )
151
+ if not opts.has_key?(:ca_cert) then
152
+ raise ArgumentError, 'Config object requires that you pass :ca_cert'
153
+ end
154
+
155
+ @ca_cert = opts[:ca_cert]
156
+
157
+ if not @ca_cert.kind_of?(R509::Cert) then
158
+ raise ArgumentError, ':ca_cert must be of type R509::Cert'
159
+ end
160
+
161
+ #ocsp data
162
+ if opts.has_key?(:ocsp_cert) and not opts[:ocsp_cert].kind_of?(R509::Cert) and not opts[:ocsp_cert].nil?
163
+ raise ArgumentError, ':ocsp_cert, if provided, must be of type R509::Cert'
164
+ end
165
+ if opts.has_key?(:ocsp_cert) and not opts[:ocsp_cert].nil? and not opts[:ocsp_cert].has_private_key?
166
+ raise ArgumentError, ':ocsp_cert must contain a private key, not just a certificate'
167
+ end
168
+ @ocsp_cert = opts[:ocsp_cert] unless opts[:ocsp_cert].nil?
169
+ @ocsp_location = opts[:ocsp_location]
170
+ @ocsp_chain = opts[:ocsp_chain] if opts[:ocsp_chain].kind_of?(Array)
171
+ @ocsp_validity_hours = opts[:ocsp_validity_hours] || 168
172
+ @ocsp_start_skew_seconds = opts[:ocsp_start_skew_seconds] || 3600
173
+
174
+ @crl_validity_hours = opts[:crl_validity_hours] || 168
175
+ @crl_start_skew_seconds = opts[:crl_start_skew_seconds] || 3600
176
+ @crl_number_file = opts[:crl_number_file] || nil
177
+ @crl_list_file = opts[:crl_list_file] || nil
178
+ @cdp_location = opts[:cdp_location]
179
+ @message_digest = opts[:message_digest] || "SHA1"
180
+
181
+
182
+
183
+ @profiles = {}
184
+ if opts[:profiles]
185
+ opts[:profiles].each_pair do |name, prof|
186
+ set_profile(name, prof)
187
+ end
188
+ end
189
+
190
+ end
191
+
192
+ # @return [R509::Cert] either a custom OCSP cert or the ca_cert
193
+ def ocsp_cert
194
+ if @ocsp_cert.nil? then @ca_cert else @ocsp_cert end
195
+ end
196
+
197
+ # @param [String] name The name of the profile
198
+ # @param [R509::Config::CaProfile] prof The profile configuration
199
+ def set_profile(name, prof)
200
+ unless prof.is_a?(R509::Config::CaProfile)
201
+ raise TypeError, "profile is supposed to be a R509::Config::CaProfile"
202
+ end
203
+ @profiles[name] = prof
204
+ end
205
+
206
+ # @param [String] prof
207
+ # @return [R509::Config::CaProfile] The config profile.
208
+ def profile(prof)
209
+ if !@profiles.has_key?(prof)
210
+ raise R509::R509Error, "unknown profile '#{prof}'"
211
+ end
212
+ @profiles[prof]
213
+ end
214
+
215
+ # @return [Integer] The number of profiles
216
+ def num_profiles
217
+ @profiles.count
218
+ end
219
+
220
+
221
+ ######### Class Methods ##########
222
+
223
+ # Load the configuration from a data hash. The same type that might be
224
+ # used when loading from a YAML file.
225
+ # @param [Hash] conf A hash containing all the configuration options
226
+ # @option opts [String] :ca_root_path The root path for the CA. Defaults to
227
+ # the current working directory.
228
+ def self.load_from_hash(conf, opts = {})
229
+ if conf.nil?
230
+ raise ArgumentError, "conf not found"
231
+ end
232
+ unless conf.kind_of?(Hash)
233
+ raise ArgumentError, "conf must be a Hash"
234
+ end
235
+
236
+ ca_root_path = Pathname.new(opts[:ca_root_path] || FileUtils.getwd)
237
+
238
+ unless File.directory?(ca_root_path)
239
+ raise R509Error, "ca_root_path is not a directory: #{ca_root_path}"
240
+ end
241
+
242
+ ca_cert_hash = conf['ca_cert']
243
+
244
+ if ca_cert_hash.has_key?('engine')
245
+ ca_cert = self.load_with_engine(ca_cert_hash,ca_root_path)
246
+ end
247
+
248
+ if ca_cert.nil? and ca_cert_hash.has_key?('pkcs12')
249
+ ca_cert = self.load_with_pkcs12(ca_cert_hash,ca_root_path)
250
+ end
251
+
252
+ if ca_cert.nil? and ca_cert_hash.has_key?('cert')
253
+ ca_cert = self.load_with_key(ca_cert_hash,ca_root_path)
254
+ end
255
+
256
+ if conf.has_key?("ocsp_cert")
257
+ if conf["ocsp_cert"].has_key?('engine')
258
+ ocsp_cert = self.load_with_engine(conf["ocsp_cert"],ca_root_path)
259
+ end
260
+
261
+ if ocsp_cert.nil? and conf["ocsp_cert"].has_key?('pkcs12')
262
+ ocsp_cert = self.load_with_pkcs12(conf["ocsp_cert"],ca_root_path)
263
+ end
264
+
265
+ if ocsp_cert.nil? and conf["ocsp_cert"].has_key?('cert')
266
+ ocsp_cert = self.load_with_key(conf["ocsp_cert"],ca_root_path)
267
+ end
268
+ end
269
+
270
+ ocsp_chain = []
271
+ if conf.has_key?("ocsp_chain")
272
+ ocsp_chain_data = read_data(ca_root_path+conf["ocsp_chain"])
273
+ cert_regex = /-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----/m
274
+ ocsp_chain_data.scan(cert_regex) do |cert|
275
+ ocsp_chain.push(OpenSSL::X509::Certificate.new(cert))
276
+ end
277
+ end
278
+
279
+ opts = {
280
+ :ca_cert => ca_cert,
281
+ :ocsp_cert => ocsp_cert,
282
+ :ocsp_chain => ocsp_chain,
283
+ :crl_validity_hours => conf['crl_validity_hours'],
284
+ :ocsp_validity_hours => conf['ocsp_validity_hours'],
285
+ :ocsp_start_skew_seconds => conf['ocsp_start_skew_seconds'],
286
+ :ocsp_location => conf['ocsp_location'],
287
+ :cdp_location => conf['cdp_location'],
288
+ :message_digest => conf['message_digest'],
289
+ }
290
+
291
+ if conf.has_key?("crl_list")
292
+ opts[:crl_list_file] = (ca_root_path + conf['crl_list']).to_s
293
+ end
294
+
295
+ if conf.has_key?("crl_number")
296
+ opts[:crl_number_file] = (ca_root_path + conf['crl_number']).to_s
297
+ end
298
+
299
+
300
+ profs = {}
301
+ conf['profiles'].keys.each do |profile|
302
+ data = conf['profiles'][profile]
303
+ if not data["subject_item_policy"].nil?
304
+ subject_item_policy = R509::Config::SubjectItemPolicy.new(data["subject_item_policy"])
305
+ end
306
+ profs[profile] = R509::Config::CaProfile.new(:key_usage => data["key_usage"],
307
+ :extended_key_usage => data["extended_key_usage"],
308
+ :basic_constraints => data["basic_constraints"],
309
+ :certificate_policies => data["certificate_policies"],
310
+ :subject_item_policy => subject_item_policy)
311
+ end unless conf['profiles'].nil?
312
+ opts[:profiles] = profs
313
+
314
+ # Create the instance.
315
+ self.new(opts)
316
+ end
317
+
318
+ # Loads the named configuration config from a yaml file.
319
+ # @param [String] conf_name The name of the config within the file. Note
320
+ # that a single yaml file can contain more than one configuration.
321
+ # @param [String] yaml_file The filename to load yaml config data from.
322
+ def self.load_yaml(conf_name, yaml_file, opts = {})
323
+ conf = YAML.load_file(yaml_file)
324
+ self.load_from_hash(conf[conf_name], opts)
325
+ end
326
+
327
+ # Loads the named configuration config from a yaml string.
328
+ # @param [String] conf_name The name of the config within the file. Note
329
+ # that a single yaml file can contain more than one configuration.
330
+ # @param [String] yaml_data The filename to load yaml config data from.
331
+ def self.from_yaml(conf_name, yaml_data, opts = {})
332
+ conf = YAML.load(yaml_data)
333
+ self.load_from_hash(conf[conf_name], opts)
334
+ end
335
+
336
+ private
337
+
338
+ def self.load_with_engine(ca_cert_hash,ca_root_path)
339
+ if ca_cert_hash.has_key?('key')
340
+ raise R509Error, "You can't specify both key and engine"
341
+ end
342
+ if ca_cert_hash.has_key?('pkcs12')
343
+ raise R509Error, "You can't specify both engine and pkcs12"
344
+ end
345
+ if not ca_cert_hash.has_key?('key_name')
346
+ raise R509Error, "You must supply a key_name with an engine"
347
+ end
348
+
349
+ if ca_cert_hash['engine'].respond_to?(:load_private_key)
350
+ #this path is only for testing...ugh
351
+ engine = ca_cert_hash['engine']
352
+ else
353
+ #this path can't be tested by unit tests. bah!
354
+ engine = OpenSSL::Engine.by_id(ca_cert_hash['engine'])
355
+ end
356
+ ca_key = R509::PrivateKey.new(
357
+ :engine => engine,
358
+ :key_name => ca_cert_hash['key_name']
359
+ )
360
+ ca_cert_file = ca_root_path + ca_cert_hash['cert']
361
+ ca_cert = R509::Cert.new(
362
+ :cert => read_data(ca_cert_file),
363
+ :key => ca_key
364
+ )
365
+ ca_cert
366
+ end
367
+
368
+ def self.load_with_pkcs12(ca_cert_hash,ca_root_path)
369
+ if ca_cert_hash.has_key?('cert')
370
+ raise R509Error, "You can't specify both pkcs12 and cert"
371
+ end
372
+ if ca_cert_hash.has_key?('key')
373
+ raise R509Error, "You can't specify both pkcs12 and key"
374
+ end
375
+
376
+ pkcs12_file = ca_root_path + ca_cert_hash['pkcs12']
377
+ ca_cert = R509::Cert.new(
378
+ :pkcs12 => read_data(pkcs12_file),
379
+ :password => ca_cert_hash['password']
380
+ )
381
+ ca_cert
382
+ end
383
+
384
+ def self.load_with_key(ca_cert_hash,ca_root_path)
385
+ ca_cert_file = ca_root_path + ca_cert_hash['cert']
386
+
387
+ if ca_cert_hash.has_key?('key')
388
+ ca_key_file = ca_root_path + ca_cert_hash['key']
389
+ ca_key = R509::PrivateKey.new(
390
+ :key => read_data(ca_key_file),
391
+ :password => ca_cert_hash['password']
392
+ )
393
+ ca_cert = R509::Cert.new(
394
+ :cert => read_data(ca_cert_file),
395
+ :key => ca_key
396
+ )
397
+ else
398
+ # in certain cases (OCSP responders for example) we may want
399
+ # to load a ca_cert with no private key
400
+ ca_cert = R509::Cert.new(:cert => read_data(ca_cert_file))
401
+ end
402
+ ca_cert
403
+ end
404
+
405
+ end
406
+ end
407
+ end
data/lib/r509/crl.rb ADDED
@@ -0,0 +1,379 @@
1
+ require 'openssl'
2
+ require 'r509/config'
3
+ require 'r509/exceptions'
4
+ require 'r509/io_helpers'
5
+
6
+ module R509
7
+ # contains CRL related classes (generator and a pre-existing list loader)
8
+ module Crl
9
+
10
+ class Parser
11
+ attr_reader :crl
12
+
13
+ # @param [String,OpenSSL::X509::CRL] crl
14
+ def initialize(crl)
15
+ @crl = OpenSSL::X509::CRL.new(crl)
16
+ end
17
+
18
+ # Helper method to quickly load a CRL from the filesystem
19
+ #
20
+ # @param [String] filename Path to file you want to load
21
+ # @return [R509::Crl::Parser] CRL object
22
+ def self.load_from_file( filename )
23
+ return R509::Crl::Parser.new( IOHelpers.read_data(filename) )
24
+ end
25
+
26
+ # @return [OpenSSL::X509::Name]
27
+ def issuer
28
+ @crl.issuer
29
+ end
30
+
31
+ # @return [String] The common name (CN) component of the issuer
32
+ def issuer_cn
33
+ return nil if self.issuer.nil?
34
+
35
+ self.issuer.to_a.each do |part, value, length|
36
+ return value if part.upcase == 'CN'
37
+ end
38
+
39
+ # return nil if we didn't find a CN part
40
+ return nil
41
+ end
42
+
43
+ # @return [Time]
44
+ def last_update
45
+ @crl.last_update
46
+ end
47
+
48
+ # @return [Time]
49
+ def next_update
50
+ @crl.next_update
51
+ end
52
+
53
+ # @return [String]
54
+ def signature_algorithm
55
+ @crl.signature_algorithm
56
+ end
57
+
58
+ # Pass a public key to verify that the CRL is signed by a specific certificate (call cert.public_key on that object)
59
+ #
60
+ # @param [OpenSSL::PKey::PKey] public_key
61
+ # @return [Boolean]
62
+ def verify(public_key)
63
+ @crl.verify(public_key)
64
+ end
65
+
66
+ # @param [Integer] serial number
67
+ # @return [Boolean]
68
+ def revoked?(serial)
69
+ if @crl.revoked.find { |revoked| revoked.serial == serial }
70
+ true
71
+ else
72
+ false
73
+ end
74
+ end
75
+
76
+ # @return [Hash] hash of serial => { :time, :reason } hashes
77
+ def revoked
78
+ revoked_list = {}
79
+ @crl.revoked.each do |revoked|
80
+ reason = get_reason(revoked)
81
+ revoked_list[revoked.serial.to_i] = { :time => revoked.time, :reason => reason }
82
+ end
83
+
84
+ revoked_list
85
+ end
86
+
87
+ # @param [Integer] serial number
88
+ # @return [Hash] hash with :time and :reason
89
+ def revoked_cert(serial)
90
+ revoked = @crl.revoked.find { |revoked| revoked.serial == serial }
91
+ if revoked
92
+ reason = get_reason(revoked)
93
+ { :time => revoked.time, :reason => reason }
94
+ else
95
+ nil
96
+ end
97
+ end
98
+
99
+ private
100
+ def get_reason(revocation_object)
101
+ reason = nil
102
+ revocation_object.extensions.each do |extension|
103
+ if extension.oid == "CRLReason"
104
+ reason = extension.value
105
+ end
106
+ end
107
+
108
+ reason
109
+ end
110
+ end
111
+
112
+ # Used to manage revocations and generate CRLs
113
+ class Administrator
114
+ include R509::IOHelpers
115
+
116
+ attr_reader :crl_number,:crl_list_file,:crl_number_file, :validity_hours
117
+
118
+ # @param [R509::Config::CaConfig] config
119
+ def initialize(config)
120
+ @config = config
121
+
122
+ unless @config.kind_of?(R509::Config::CaConfig)
123
+ raise R509Error, "config must be a kind of R509::Config::CaConfig"
124
+ end
125
+
126
+ @validity_hours = @config.crl_validity_hours
127
+ @start_skew_seconds = @config.crl_start_skew_seconds
128
+ @crl = nil
129
+
130
+ @crl_number_file = @config.crl_number_file
131
+ if not @crl_number_file.nil?
132
+ @crl_number = read_data(@crl_number_file).to_i
133
+ else
134
+ @crl_number = 0
135
+ end
136
+
137
+
138
+ @crl_list_file = @config.crl_list_file
139
+ load_crl_list(@crl_list_file)
140
+ end
141
+
142
+ # Indicates whether the serial number has been revoked, or not.
143
+ #
144
+ # @param [Integer] serial The serial number we want to check
145
+ # @return [Boolean] True if the serial number was revoked. False, otherwise.
146
+ def revoked?(serial)
147
+ @revoked_certs.has_key?(serial)
148
+ end
149
+
150
+ # @return [Array] serial, reason, revoke_time tuple
151
+ def revoked_cert(serial)
152
+ @revoked_certs[serial]
153
+ end
154
+
155
+ # Returns the CRL in PEM format
156
+ #
157
+ # @return [String] the CRL in PEM format
158
+ def to_pem
159
+ @crl.to_pem
160
+ end
161
+
162
+ alias :to_s :to_pem
163
+
164
+ # Returns the CRL in DER format
165
+ #
166
+ # @return [String] the CRL in DER format
167
+ def to_der
168
+ @crl.to_der
169
+ end
170
+
171
+ # @return [R509::Crl::Parser]
172
+ def to_crl
173
+ return nil if @crl.nil?
174
+ return R509::Crl::Parser.new(@crl)
175
+ end
176
+
177
+ # Writes the CRL into the PEM format
178
+ #
179
+ # @param [String, #write] filename_or_io Either a string of the path for
180
+ # the file that you'd like to write, or an IO-like object.
181
+ def write_pem(filename_or_io)
182
+ write_data(filename_or_io, @crl.to_pem)
183
+ end
184
+
185
+ # Writes the CRL into the PEM format
186
+ #
187
+ # @param [String, #write] filename_or_io Either a string of the path for
188
+ # the file that you'd like to write, or an IO-like object.
189
+ def write_der(filename_or_io)
190
+ write_data(filename_or_io, @crl.to_der)
191
+ end
192
+
193
+ # Returns the signing time of the CRL
194
+ #
195
+ # @return [Time] when the CRL was signed
196
+ def last_update
197
+ @crl.last_update
198
+ end
199
+
200
+ # Returns the next update time for the CRL
201
+ #
202
+ # @return [Time] when it will be updated next
203
+ def next_update
204
+ @crl.next_update
205
+ end
206
+
207
+ # Adds a certificate to the revocation list. After calling you must call generate_crl to sign a new CRL
208
+ #
209
+ # @param serial [Integer] serial number of the certificate to revoke
210
+ # @param reason [Integer] reason for revocation
211
+ # @param revoke_time [Integer]
212
+ # @param generate_and_save [Boolean] whether we want to generate the CRL and save its file (default=true)
213
+ #
214
+ # reason codes defined by rfc 5280
215
+ #
216
+ # CRLReason ::= ENUMERATED {
217
+ # unspecified (0),
218
+ # keyCompromise (1),
219
+ # cACompromise (2),
220
+ # affiliationChanged (3),
221
+ # superseded (4),
222
+ # cessationOfOperation (5),
223
+ # certificateHold (6),
224
+ # removeFromCRL (8),
225
+ # privilegeWithdrawn (9),
226
+ # aACompromise (10) }
227
+ def revoke_cert(serial,reason=nil, revoke_time=Time.now.to_i, generate_and_save=true)
228
+ if not reason.to_i.between?(0,10)
229
+ reason = 0
230
+ end
231
+ serial = serial.to_i
232
+ reason = reason.to_i
233
+ revoke_time = revoke_time.to_i
234
+ if revoked?(serial)
235
+ raise R509::R509Error, "Cannot revoke a previously revoked certificate"
236
+ end
237
+ @revoked_certs[serial] = {:reason => reason, :revoke_time => revoke_time}
238
+ if generate_and_save
239
+ generate_crl()
240
+ save_crl_list()
241
+ end
242
+ nil
243
+ end
244
+
245
+ # Remove serial from revocation list. After unrevoking you must call generate_crl to sign a new CRL
246
+ #
247
+ # @param serial [Integer] serial number of the certificate to remove from revocation
248
+ def unrevoke_cert(serial)
249
+ @revoked_certs.delete(serial)
250
+ generate_crl()
251
+ save_crl_list()
252
+ nil
253
+ end
254
+
255
+ # Remove serial from revocation list
256
+ #
257
+ # @return [String] PEM encoded signed CRL
258
+ def generate_crl
259
+ crl = OpenSSL::X509::CRL.new
260
+ crl.version = 1
261
+ now = Time.at Time.now.to_i
262
+ crl.last_update = now-@start_skew_seconds
263
+ crl.next_update = now+@validity_hours*3600
264
+ crl.issuer = @config.ca_cert.subject
265
+
266
+ self.revoked_certs.each do |serial, reason, revoke_time|
267
+ revoked = OpenSSL::X509::Revoked.new
268
+ revoked.serial = OpenSSL::BN.new serial.to_s
269
+ revoked.time = Time.at(revoke_time)
270
+ if !reason.nil?
271
+ enum = OpenSSL::ASN1::Enumerated(reason) #see reason codes below
272
+ ext = OpenSSL::X509::Extension.new("CRLReason", enum)
273
+ revoked.add_extension(ext)
274
+ end
275
+ #now add it to the crl
276
+ crl.add_revoked(revoked)
277
+ end
278
+
279
+ ef = OpenSSL::X509::ExtensionFactory.new
280
+ ef.issuer_certificate = @config.ca_cert.cert
281
+ ef.crl = crl
282
+ #grab crl number from file, increment, write back
283
+ crl_number = increment_crl_number
284
+ crlnum = OpenSSL::ASN1::Integer(crl_number)
285
+ crl.add_extension(OpenSSL::X509::Extension.new("crlNumber", crlnum))
286
+ extensions = []
287
+ extensions << ["authorityKeyIdentifier", "keyid:always,issuer:always", false]
288
+ extensions.each{|oid, value, critical|
289
+ crl.add_extension(ef.create_extension(oid, value, critical))
290
+ }
291
+ crl.sign(@config.ca_cert.key.key, OpenSSL::Digest::SHA1.new)
292
+ @crl = crl
293
+ @crl.to_pem
294
+ end
295
+
296
+ # @return [Array<Array>] Returns an array of serial, reason, revoke_time
297
+ # tuples.
298
+ def revoked_certs
299
+ ret = []
300
+ @revoked_certs.keys.sort.each do |serial|
301
+ ret << [serial, @revoked_certs[serial][:reason], @revoked_certs[serial][:revoke_time]]
302
+ end
303
+ ret
304
+ end
305
+
306
+ # Saves the CRL list to a filename or IO. If the class was initialized
307
+ # with :crl_list_file, then the filename specified by that will be used
308
+ # by default.
309
+ # @param [String, #write, nil] filename_or_io If provided, the generated
310
+ # crl will be written to either the file (if a string), or IO. If nil,
311
+ # then the @crl_list_file will be used. If that is nil, then an error
312
+ # will be raised.
313
+ def save_crl_list(filename_or_io = @crl_list_file)
314
+ return nil if filename_or_io.nil?
315
+
316
+ data = []
317
+ self.revoked_certs.each do |serial, reason, revoke_time|
318
+ data << [serial, revoke_time, reason].join(',')
319
+ end
320
+ write_data(filename_or_io, data.join("\n"))
321
+ nil
322
+ end
323
+
324
+ # Save the CRL number to a filename or IO. If the class was initialized
325
+ # with :crl_number_file, then the filename specified by that will be used
326
+ # by default.
327
+ # @param [String, #write, nil] filename_or_io If provided, the current
328
+ # crl number will be written to either the file (if a string), or IO. If nil,
329
+ # then the @crl_number_file will be used. If that is nil, then an error
330
+ # will be raised.
331
+ def save_crl_number(filename_or_io = @crl_number_file)
332
+ return nil if filename_or_io.nil?
333
+ # No valid filename or IO was specified, so bail.
334
+
335
+ write_data(filename_or_io, self.crl_number.to_s)
336
+ nil
337
+ end
338
+
339
+ private
340
+
341
+ # Increments the crl_number.
342
+ # @return [Integer] the new CRL number
343
+ #
344
+ def increment_crl_number
345
+ @crl_number += 1
346
+ save_crl_number()
347
+ @crl_number
348
+ end
349
+
350
+ # Loads the certificate revocation list from file.
351
+ # @param [String, #read, nil] filename_or_io The
352
+ # crl will be read from either the file (if a string), or IO.
353
+ def load_crl_list(filename_or_io)
354
+ @revoked_certs = {}
355
+
356
+ if filename_or_io.nil?
357
+ generate_crl
358
+ return nil
359
+ end
360
+
361
+ data = read_data(filename_or_io)
362
+
363
+ data.each_line do |line|
364
+ line.chomp!
365
+ serial, revoke_time, reason = line.split(',', 3)
366
+ serial = serial.to_i
367
+ reason = (reason == '') ? nil : reason.to_i
368
+ revoke_time = (revoke_time == '') ? nil : revoke_time.to_i
369
+ self.revoke_cert(serial, reason, revoke_time, false)
370
+ end
371
+ generate_crl
372
+ save_crl_list
373
+ nil
374
+ end
375
+
376
+ end
377
+ end
378
+ end
379
+