r509 0.8

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.
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
+