r509 0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +447 -0
- data/Rakefile +38 -0
- data/bin/r509 +96 -0
- data/bin/r509-parse +35 -0
- data/doc/R509.html +154 -0
- data/doc/R509/Cert.html +3954 -0
- data/doc/R509/Cert/Extensions.html +360 -0
- data/doc/R509/Cert/Extensions/AuthorityInfoAccess.html +391 -0
- data/doc/R509/Cert/Extensions/AuthorityKeyIdentifier.html +148 -0
- data/doc/R509/Cert/Extensions/BasicConstraints.html +482 -0
- data/doc/R509/Cert/Extensions/CrlDistributionPoints.html +316 -0
- data/doc/R509/Cert/Extensions/ExtendedKeyUsage.html +780 -0
- data/doc/R509/Cert/Extensions/KeyUsage.html +1230 -0
- data/doc/R509/Cert/Extensions/SubjectAlternativeName.html +467 -0
- data/doc/R509/Cert/Extensions/SubjectKeyIdentifier.html +216 -0
- data/doc/R509/CertificateAuthority.html +126 -0
- data/doc/R509/CertificateAuthority/Signer.html +855 -0
- data/doc/R509/Config.html +127 -0
- data/doc/R509/Config/CaConfig.html +2144 -0
- data/doc/R509/Config/CaConfigPool.html +599 -0
- data/doc/R509/Config/CaProfile.html +656 -0
- data/doc/R509/Config/SubjectItemPolicy.html +578 -0
- data/doc/R509/Crl.html +126 -0
- data/doc/R509/Crl/Administrator.html +2077 -0
- data/doc/R509/Crl/Parser.html +1224 -0
- data/doc/R509/Csr.html +2248 -0
- data/doc/R509/IOHelpers.html +564 -0
- data/doc/R509/MessageDigest.html +396 -0
- data/doc/R509/NameSanitizer.html +319 -0
- data/doc/R509/Ocsp.html +128 -0
- data/doc/R509/Ocsp/Request.html +126 -0
- data/doc/R509/Ocsp/Request/Nonce.html +160 -0
- data/doc/R509/Ocsp/Response.html +837 -0
- data/doc/R509/OidMapper.html +393 -0
- data/doc/R509/PrivateKey.html +1647 -0
- data/doc/R509/R509Error.html +134 -0
- data/doc/R509/Spki.html +1424 -0
- data/doc/R509/Subject.html +836 -0
- data/doc/R509/Validity.html +160 -0
- data/doc/R509/Validity/Checker.html +320 -0
- data/doc/R509/Validity/DefaultChecker.html +283 -0
- data/doc/R509/Validity/DefaultWriter.html +330 -0
- data/doc/R509/Validity/Status.html +561 -0
- data/doc/R509/Validity/Writer.html +394 -0
- data/doc/_index.html +501 -0
- data/doc/class_list.html +53 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +328 -0
- data/doc/file.README.html +534 -0
- data/doc/file.r509.html +149 -0
- data/doc/file_list.html +58 -0
- data/doc/frames.html +28 -0
- data/doc/index.html +534 -0
- data/doc/js/app.js +208 -0
- data/doc/js/full_list.js +173 -0
- data/doc/js/jquery.js +4 -0
- data/doc/methods_list.html +1932 -0
- data/doc/top-level-namespace.html +112 -0
- data/lib/r509.rb +22 -0
- data/lib/r509/cert.rb +414 -0
- data/lib/r509/cert/extensions.rb +309 -0
- data/lib/r509/certificateauthority.rb +290 -0
- data/lib/r509/config.rb +407 -0
- data/lib/r509/crl.rb +379 -0
- data/lib/r509/csr.rb +324 -0
- data/lib/r509/exceptions.rb +5 -0
- data/lib/r509/io_helpers.rb +52 -0
- data/lib/r509/messagedigest.rb +49 -0
- data/lib/r509/ocsp.rb +85 -0
- data/lib/r509/oidmapper.rb +32 -0
- data/lib/r509/privatekey.rb +185 -0
- data/lib/r509/spki.rb +112 -0
- data/lib/r509/subject.rb +133 -0
- data/lib/r509/validity.rb +92 -0
- data/lib/r509/version.rb +4 -0
- data/r509.yaml +73 -0
- data/spec/cert/extensions_spec.rb +632 -0
- data/spec/cert_spec.rb +321 -0
- data/spec/certificate_authority_spec.rb +260 -0
- data/spec/config_spec.rb +349 -0
- data/spec/crl_spec.rb +215 -0
- data/spec/csr_spec.rb +302 -0
- data/spec/fixtures.rb +233 -0
- data/spec/fixtures/cert1.der +0 -0
- data/spec/fixtures/cert1.pem +24 -0
- data/spec/fixtures/cert1_public_key_modulus.txt +1 -0
- data/spec/fixtures/cert3.p12 +0 -0
- data/spec/fixtures/cert3.pem +28 -0
- data/spec/fixtures/cert3_key.pem +27 -0
- data/spec/fixtures/cert3_key_des3.pem +30 -0
- data/spec/fixtures/cert4.pem +14 -0
- data/spec/fixtures/cert5.pem +30 -0
- data/spec/fixtures/cert6.pem +26 -0
- data/spec/fixtures/cert_expired.pem +26 -0
- data/spec/fixtures/cert_not_yet_valid.pem +26 -0
- data/spec/fixtures/cert_san.pem +27 -0
- data/spec/fixtures/cert_san2.pem +22 -0
- data/spec/fixtures/config_pool_test_minimal.yaml +15 -0
- data/spec/fixtures/config_test.yaml +41 -0
- data/spec/fixtures/config_test_engine_key.yaml +7 -0
- data/spec/fixtures/config_test_engine_no_key_name.yaml +6 -0
- data/spec/fixtures/config_test_minimal.yaml +7 -0
- data/spec/fixtures/config_test_password.yaml +7 -0
- data/spec/fixtures/config_test_various.yaml +100 -0
- data/spec/fixtures/crl_list_file.txt +1 -0
- data/spec/fixtures/crl_with_reason.pem +17 -0
- data/spec/fixtures/csr1.der +0 -0
- data/spec/fixtures/csr1.pem +17 -0
- data/spec/fixtures/csr1_key.der +0 -0
- data/spec/fixtures/csr1_key.pem +27 -0
- data/spec/fixtures/csr1_key_encrypted_des3.pem +30 -0
- data/spec/fixtures/csr1_newlines.pem +32 -0
- data/spec/fixtures/csr1_no_begin_end.pem +15 -0
- data/spec/fixtures/csr1_public_key_modulus.txt +1 -0
- data/spec/fixtures/csr2.pem +15 -0
- data/spec/fixtures/csr2_key.pem +27 -0
- data/spec/fixtures/csr3.pem +16 -0
- data/spec/fixtures/csr4.pem +25 -0
- data/spec/fixtures/csr_dsa.pem +15 -0
- data/spec/fixtures/csr_invalid_signature.pem +13 -0
- data/spec/fixtures/dsa_key.pem +20 -0
- data/spec/fixtures/key4.pem +27 -0
- data/spec/fixtures/key4_encrypted_des3.pem +30 -0
- data/spec/fixtures/missing_key_identifier_ca.cer +21 -0
- data/spec/fixtures/missing_key_identifier_ca.key +27 -0
- data/spec/fixtures/ocsptest.r509.local.pem +27 -0
- data/spec/fixtures/ocsptest.r509.local_ocsp_request.der +0 -0
- data/spec/fixtures/ocsptest2.r509.local.pem +27 -0
- data/spec/fixtures/second_ca.cer +26 -0
- data/spec/fixtures/second_ca.key +27 -0
- data/spec/fixtures/spkac.der +0 -0
- data/spec/fixtures/spkac.txt +1 -0
- data/spec/fixtures/spkac_dsa.txt +1 -0
- data/spec/fixtures/stca.pem +22 -0
- data/spec/fixtures/stca_ocsp_request.der +0 -0
- data/spec/fixtures/stca_ocsp_response.der +0 -0
- data/spec/fixtures/test1.csr +17 -0
- data/spec/fixtures/test_ca.cer +22 -0
- data/spec/fixtures/test_ca.key +28 -0
- data/spec/fixtures/test_ca.p12 +0 -0
- data/spec/fixtures/test_ca_des3.key +30 -0
- data/spec/fixtures/test_ca_ocsp.cer +26 -0
- data/spec/fixtures/test_ca_ocsp.key +27 -0
- data/spec/fixtures/test_ca_ocsp.p12 +0 -0
- data/spec/fixtures/test_ca_ocsp_chain.txt +48 -0
- data/spec/fixtures/test_ca_ocsp_response.der +0 -0
- data/spec/fixtures/test_ca_subroot.cer +26 -0
- data/spec/fixtures/test_ca_subroot.key +27 -0
- data/spec/fixtures/test_ca_subroot_ocsp.cer +25 -0
- data/spec/fixtures/test_ca_subroot_ocsp.key +27 -0
- data/spec/fixtures/test_ca_subroot_ocsp_response.der +0 -0
- data/spec/fixtures/unknown_oid.csr +17 -0
- data/spec/message_digest_spec.rb +89 -0
- data/spec/ocsp_spec.rb +111 -0
- data/spec/oid_mapper_spec.rb +31 -0
- data/spec/privatekey_spec.rb +198 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/spki_spec.rb +157 -0
- data/spec/subject_spec.rb +203 -0
- data/spec/validity_spec.rb +98 -0
- metadata +257 -0
data/lib/r509/config.rb
ADDED
@@ -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
|
+
|