r509-ocsp-responder 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/README.md +77 -0
  2. data/Rakefile +38 -0
  3. data/doc/R509.html +115 -0
  4. data/doc/R509/Ocsp.html +130 -0
  5. data/doc/R509/Ocsp/Helper.html +126 -0
  6. data/doc/R509/Ocsp/Helper/RequestChecker.html +739 -0
  7. data/doc/R509/Ocsp/Helper/ResponseSigner.html +583 -0
  8. data/doc/R509/Ocsp/Responder.html +129 -0
  9. data/doc/R509/Ocsp/Responder/OcspConfig.html +289 -0
  10. data/doc/R509/Ocsp/Responder/Server.html +128 -0
  11. data/doc/R509/Ocsp/Responder/StatusError.html +134 -0
  12. data/doc/R509/Ocsp/Signer.html +584 -0
  13. data/doc/_index.html +197 -0
  14. data/doc/class_list.html +53 -0
  15. data/doc/css/common.css +1 -0
  16. data/doc/css/full_list.css +57 -0
  17. data/doc/css/style.css +328 -0
  18. data/doc/file.README.html +156 -0
  19. data/doc/file_list.html +55 -0
  20. data/doc/frames.html +28 -0
  21. data/doc/index.html +156 -0
  22. data/doc/js/app.js +214 -0
  23. data/doc/js/full_list.js +173 -0
  24. data/doc/js/jquery.js +4 -0
  25. data/doc/method_list.html +164 -0
  26. data/doc/top-level-namespace.html +112 -0
  27. data/lib/r509/ocsp/responder/ocsp-config.rb +35 -0
  28. data/lib/r509/ocsp/responder/server.rb +169 -0
  29. data/lib/r509/ocsp/responder/version.rb +7 -0
  30. data/lib/r509/ocsp/signer.rb +244 -0
  31. data/spec/fixtures.rb +196 -0
  32. data/spec/fixtures/cert1.pem +24 -0
  33. data/spec/fixtures/config_test_various.yaml +46 -0
  34. data/spec/fixtures/ocsptest.r509.local.pem +27 -0
  35. data/spec/fixtures/second_ca.cer +26 -0
  36. data/spec/fixtures/second_ca.key +27 -0
  37. data/spec/fixtures/stca.pem +22 -0
  38. data/spec/fixtures/stca_ocsp_request.der +0 -0
  39. data/spec/fixtures/stca_ocsp_response.der +0 -0
  40. data/spec/fixtures/test_ca.cer +22 -0
  41. data/spec/fixtures/test_ca.key +28 -0
  42. data/spec/fixtures/test_ca_ocsp.cer +26 -0
  43. data/spec/fixtures/test_ca_ocsp.key +27 -0
  44. data/spec/fixtures/test_ca_ocsp_chain.txt +48 -0
  45. data/spec/fixtures/test_ca_request.der +0 -0
  46. data/spec/fixtures/test_ca_response.der +0 -0
  47. data/spec/fixtures/test_ca_subroot.cer +25 -0
  48. data/spec/fixtures/test_ca_subroot.key +27 -0
  49. data/spec/fixtures/test_ca_subroot_ocsp.cer +25 -0
  50. data/spec/fixtures/test_ca_subroot_ocsp.key +27 -0
  51. data/spec/fixtures/test_config.yaml +17 -0
  52. data/spec/server_spec.rb +400 -0
  53. data/spec/signer_spec.rb +275 -0
  54. data/spec/spec_helper.rb +18 -0
  55. metadata +259 -0
@@ -0,0 +1,35 @@
1
+ module R509::Ocsp::Responder
2
+ class OcspConfig
3
+ def self.load_config
4
+ config_data = File.read("config.yaml")
5
+
6
+ Dependo::Registry[:config_pool] = R509::Config::CaConfigPool.from_yaml("certificate_authorities", config_data)
7
+
8
+ Dependo::Registry[:copy_nonce] = YAML.load(config_data)["copy_nonce"] || false
9
+
10
+ Dependo::Registry[:cache_headers] = YAML.load(config_data)["cache_headers"] || false
11
+
12
+ Dependo::Registry[:max_cache_age] = YAML.load(config_data)["max_cache_age"]
13
+
14
+ Dependo::Registry[:ocsp_signer] = R509::Ocsp::Signer.new(
15
+ :configs => Dependo::Registry[:config_pool],
16
+ :validity_checker => R509::Validity::Redis::Checker.new(Dependo::Registry[:redis]),
17
+ :copy_nonce => Dependo::Registry[:copy_nonce]
18
+ )
19
+ end
20
+
21
+ def self.print_config
22
+ Dependo::Registry[:log].warn "Config loaded"
23
+ Dependo::Registry[:log].warn "Copy Nonce: "+Dependo::Registry[:copy_nonce].to_s
24
+ Dependo::Registry[:log].warn "Cache Headers: "+Dependo::Registry[:cache_headers].to_s
25
+ Dependo::Registry[:log].warn "Max Cache Age: "+Dependo::Registry[:max_cache_age].to_s
26
+ Dependo::Registry[:config_pool].all.each do |config|
27
+ Dependo::Registry[:log].warn "Config: "
28
+ Dependo::Registry[:log].warn "CA Cert:"+config.ca_cert.subject.to_s
29
+ Dependo::Registry[:log].warn "OCSP Cert (may be the same as above):"+config.ocsp_cert.subject.to_s
30
+ Dependo::Registry[:log].warn "OCSP Validity Hours: "+config.ocsp_validity_hours.to_s
31
+ Dependo::Registry[:log].warn "\n"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,169 @@
1
+ require 'rubygems' if RUBY_VERSION < "1.9"
2
+ require 'sinatra/base'
3
+ require 'r509'
4
+ require 'r509/ocsp/signer'
5
+ require 'r509/validity/redis'
6
+ require 'base64'
7
+ require 'dependo'
8
+ require 'logger'
9
+ require 'time'
10
+ require File.dirname(__FILE__)+'/ocsp-config.rb'
11
+
12
+ # Capture USR2 calls so we can reload and print the config
13
+ # I'd rather use HUP, but daemons like thin already capture that
14
+ # so we can't use it.
15
+ Signal.trap("USR2") do
16
+ R509::Ocsp::Responder::OcspConfig.load_config
17
+ R509::Ocsp::Responder::OcspConfig.print_config
18
+ end
19
+
20
+
21
+ module R509::Ocsp::Responder
22
+ #error for status checking
23
+ class StatusError < StandardError
24
+ end
25
+
26
+ class Server < Sinatra::Base
27
+ include Dependo::Mixin
28
+
29
+ configure do
30
+ mime_type :ocsp, 'application/ocsp-response'
31
+ disable :protection #disable Rack::Protection (for speed)
32
+ disable :logging
33
+ set :environment, :production
34
+ end
35
+
36
+ error do
37
+ log.error env["sinatra.error"].inspect
38
+ log.error env["sinatra.error"].backtrace.join("\n")
39
+ "Something is amiss with our OCSP responder. You should ... wait?"
40
+ end
41
+
42
+ error OpenSSL::OCSP::OCSPError do
43
+ "Invalid request"
44
+ end
45
+
46
+ error R509::Ocsp::Responder::StatusError do
47
+ "Down"
48
+ end
49
+
50
+ get '/favicon.ico' do
51
+ log.debug "go away. no children."
52
+ "go away. no children"
53
+ end
54
+
55
+ get '/status/?' do
56
+ begin
57
+ if Dependo::Registry[:ocsp_signer].validity_checker.is_available?
58
+ "OK"
59
+ else
60
+ raise R509::Ocsp::Responder::StatusError
61
+ end
62
+ rescue
63
+ raise R509::Ocsp::Responder::StatusError
64
+ end
65
+ end
66
+
67
+ get '/*' do
68
+ raw_request = params[:splat].join("/")
69
+ #remove any leading slashes (looking at you MS Crypto API)
70
+ raw_request.sub!(/^\/+/,"")
71
+ log.info { "GET Request: "+raw_request }
72
+ der = Base64.decode64(raw_request)
73
+ request_response = handle_ocsp_request(der, "GET")
74
+ build_headers(request_response)
75
+ request_response[:response].to_der
76
+ end
77
+
78
+ post '/' do
79
+ if request.media_type == 'application/ocsp-request'
80
+ der = request.env["rack.input"].read
81
+ log.info { "POST Request: "+Base64.encode64(der).gsub!(/\n/,"") }
82
+ request_response = handle_ocsp_request(der, "POST")
83
+ request_response[:response].to_der
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def handle_ocsp_request(der, method)
90
+ begin
91
+ request_response = ocsp_signer.handle_request(der)
92
+
93
+ log_ocsp_response(request_response[:response],method)
94
+
95
+ content_type :ocsp
96
+ request_response
97
+ rescue StandardError => e
98
+ log.error "unexpected error #{e}"
99
+ raise e
100
+ end
101
+ end
102
+
103
+ def log_ocsp_response(ocsp_response, method="?")
104
+ if response.nil?
105
+ log.error "Something went horribly wrong"
106
+ return
107
+ end
108
+
109
+ case ocsp_response.status
110
+ when OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
111
+ serial_data = ocsp_response.basic.status.map do |status|
112
+ friendly_status = case status[1]
113
+ when 0
114
+ "VALID"
115
+ when 1
116
+ "REVOKED"
117
+ when 2
118
+ "UNKNOWN"
119
+ end
120
+ if ocsp_response.basic.status[0][0].respond_to?(:issuer_key_hash)
121
+ config_used = ocsp_signer.request_checker.configs_hash[ocsp_response.basic.status[0][0].issuer_key_hash]
122
+ else
123
+ config_used = ocsp_signer.request_checker.configs.find do |config|
124
+ #we need to create an OCSP::CertificateId object that has the right
125
+ #issuer so we can pass it to #cmp_issuer. This is annoying because
126
+ #CertificateId wants a cert and its issuer, but we don't want to
127
+ #force users to provide an end entity cert just to make this comparison
128
+ #work. So, we create a fake new cert and pass it in.
129
+ ee_cert = OpenSSL::X509::Certificate.new
130
+ ee_cert.issuer = config.ca_cert.cert.subject
131
+ issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert)
132
+ ocsp_response.basic.status[0][0].cmp_issuer(issuer_certid)
133
+ end
134
+ end
135
+ stats.record(config_used.ca_cert.subject.to_s, status[0].serial.to_s, friendly_status) if Dependo::Registry.has_key?(:stats)
136
+ status[0].serial.to_s+" Status: #{friendly_status}"
137
+ end
138
+ log.info { "#{method} Request For Serial(s): #{serial_data.join(",")} UserAgent: #{env["HTTP_USER_AGENT"]}" }
139
+ when OpenSSL::OCSP::RESPONSE_STATUS_UNAUTHORIZED
140
+ log.info { "#{method} Request For Unauthorized CA. UserAgent: #{env["HTTP_USER_AGENT"]}" }
141
+ when OpenSSL::OCSP::RESPONSE_STATUS_MALFORMEDREQUEST
142
+ log.info { "#{method} Malformed Request. UserAgent: #{env["HTTP_USER_AGENT"]}" }
143
+ end
144
+ end
145
+
146
+ def build_headers(request_response)
147
+ ocsp_response = request_response[:response]
148
+ ocsp_request = request_response[:request]
149
+
150
+ # cache_headers is injected via config.ru
151
+ # we only cache if it's a RESPONSE_STATUS_SUCCESSFUL response and there's no nonce.
152
+ if cache_headers and not ocsp_response.basic.nil? and ocsp_response.check_nonce(ocsp_request) == R509::Ocsp::Request::Nonce::BOTH_ABSENT
153
+ calculated_max_age = ocsp_response.basic.status[0][5] - Time.now
154
+ #same with max_cache_age
155
+ if not max_cache_age or ( max_cache_age > calculated_max_age )
156
+ max_age = calculated_max_age
157
+ else
158
+ max_age = max_cache_age
159
+ end
160
+
161
+ response["Last-Modified"] = Time.now.httpdate
162
+ response["ETag"] = OpenSSL::Digest::SHA1.new(ocsp_response.to_der).to_s
163
+ response["Expires"] = ocsp_response.basic.status[0][5].httpdate
164
+ response["Cache-Control"] = "max-age=#{max_age.to_i}, public, no-transform, must-revalidate"
165
+ end
166
+ end
167
+
168
+ end
169
+ end
@@ -0,0 +1,7 @@
1
+ module R509
2
+ module Ocsp
3
+ module Responder
4
+ VERSION="0.3.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,244 @@
1
+ require 'openssl'
2
+ require 'r509/exceptions'
3
+ require 'r509/config'
4
+ require 'dependo'
5
+
6
+ # OCSP related classes (signing, response, request)
7
+ module R509::Ocsp
8
+ # A class for signing OCSP responses
9
+ class Signer
10
+ attr_reader :validity_checker,:request_checker
11
+
12
+ # @option options [Boolean] :copy_nonce copy nonce from request to response?
13
+ # @option options [R509::Config::CaConfigPool] :configs CaConfigPool object
14
+ # possible OCSP issuance roots that we want to issue OCSP responses for
15
+ def initialize(options)
16
+ if options.has_key?(:validity_checker)
17
+ @validity_checker = options[:validity_checker]
18
+ else
19
+ @validity_checker = R509::Validity::DefaultChecker.new
20
+ end
21
+ @request_checker = Helper::RequestChecker.new(options[:configs], @validity_checker)
22
+ @response_signer = Helper::ResponseSigner.new(options)
23
+ end
24
+
25
+
26
+ # @param request [String,OpenSSL::OCSP::Request] OCSP request (string or parsed object)
27
+ # @return [Hash]
28
+ # * :request [OpenSSL::OCSP::Request] parsed request object
29
+ # * :response [OpenSSL::OCSP::Response] full response object
30
+ def handle_request(request)
31
+ begin
32
+ parsed_request = OpenSSL::OCSP::Request.new request
33
+ rescue
34
+ return {:response => @response_signer.create_response(OpenSSL::OCSP::RESPONSE_STATUS_MALFORMEDREQUEST), :request => nil}
35
+ end
36
+
37
+ statuses = @request_checker.check_statuses(parsed_request)
38
+ if not @request_checker.validate_statuses(statuses)
39
+ return {:response => @response_signer.create_response(OpenSSL::OCSP::RESPONSE_STATUS_UNAUTHORIZED), :request => nil}
40
+ end
41
+
42
+ basic_response = @response_signer.create_basic_response(parsed_request,statuses)
43
+
44
+ {:response => @response_signer.create_response(
45
+ OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL,
46
+ basic_response
47
+ ), :request => parsed_request}
48
+ end
49
+
50
+ end
51
+ end
52
+
53
+ #Helper module for OCSP handling
54
+ module R509::Ocsp::Helper
55
+ # checks requests for validity against a set of configs
56
+ class RequestChecker
57
+ include Dependo::Mixin
58
+ attr_reader :configs,:configs_hash
59
+
60
+ # @param [R509::Config::CaConfigPool] configs CaConfigPool object
61
+ # @param [R509::Validity::Checker] validity_checker an implementation of the R509::Validity::Checker class
62
+ def initialize(configs, validity_checker)
63
+ unless configs.kind_of?(R509::Config::CaConfigPool)
64
+ raise R509::R509Error, "Must pass R509::Config::CaConfigPool object"
65
+ end
66
+ if configs.all.empty?
67
+ raise R509::R509Error, "Must be at least one R509::Config object"
68
+ end
69
+ @configs = configs.all
70
+ test_cid = OpenSSL::OCSP::CertificateId.new(OpenSSL::X509::Certificate.new,OpenSSL::X509::Certificate.new)
71
+ if test_cid.respond_to?(:issuer_key_hash)
72
+ @configs_hash = {}
73
+ @configs.each do |config|
74
+ ee_cert = OpenSSL::X509::Certificate.new
75
+ ee_cert.issuer = config.ca_cert.cert.subject
76
+ # per RFC 5019
77
+ # Clients MUST use SHA1 as the hashing algorithm for the
78
+ # CertID.issuerNameHash and the CertID.issuerKeyHash values.
79
+ # so we can safely assume that our inbound hashes will be SHA1
80
+ issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert,OpenSSL::Digest::SHA1.new)
81
+ @configs_hash[issuer_certid.issuer_key_hash] = config
82
+ end
83
+ end
84
+ @validity_checker = validity_checker
85
+ if @validity_checker.nil?
86
+ raise R509::R509Error, "Must supply a R509::Validity::Checker"
87
+ end
88
+ if not @validity_checker.respond_to?(:check)
89
+ raise R509::R509Error, "The validity checker must have a check method"
90
+ end
91
+ end
92
+
93
+ # Loads and checks a raw OCSP request
94
+ #
95
+ # @param request [OpenSSL::OCSP::Request] OpenSSL OCSP Request object
96
+ # @return [Hash] hash from the check_status method
97
+ def check_statuses(request)
98
+ request.certid.map { |certid|
99
+ if certid.respond_to?(:issuer_key_hash)
100
+ validated_config = @configs_hash[certid.issuer_key_hash]
101
+ else
102
+ validated_config = @configs.find do |config|
103
+ #we need to create an OCSP::CertificateId object that has the right
104
+ #issuer so we can pass it to #cmp_issuer. This is annoying because
105
+ #CertificateId wants a cert and its issuer, but we don't want to
106
+ #force users to provide an end entity cert just to make this comparison
107
+ #work. So, we create a fake new cert and pass it in.
108
+ ee_cert = OpenSSL::X509::Certificate.new
109
+ ee_cert.issuer = config.ca_cert.cert.subject
110
+ issuer_certid = OpenSSL::OCSP::CertificateId.new(ee_cert,config.ca_cert.cert)
111
+ certid.cmp_issuer(issuer_certid)
112
+ end
113
+ end
114
+
115
+ log.info "#{validated_config.ca_cert.subject.to_s} found for issuer" if validated_config
116
+ check_status(certid, validated_config)
117
+ }
118
+ end
119
+
120
+ # Determines whether the statuses constitute a request that is compliant.
121
+ # No config means we don't know the CA, different configs means there are
122
+ # requests from two different CAs in there. Both are invalid.
123
+ #
124
+ # @param statuses [Array<Hash>] array of hashes from check_statuses
125
+ # @return [Boolean]
126
+ def validate_statuses(statuses)
127
+ validity = true
128
+ config = nil
129
+
130
+ statuses.each do |status|
131
+ if status[:config].nil?
132
+ validity = false
133
+ end
134
+ if config.nil?
135
+ config = status[:config]
136
+ end
137
+ if config != status[:config]
138
+ validity = false
139
+ end
140
+ end
141
+
142
+ validity
143
+ end
144
+
145
+ private
146
+
147
+ # Checks the status of a certificate with the corresponding CA
148
+ # @param certid [OpenSSL::OCSP::CertificateId] The certId object from check_statuses
149
+ # @param validated_config [R509::Config]
150
+ def check_status(certid, validated_config)
151
+ if(validated_config == nil) then
152
+ return {
153
+ :certid => certid,
154
+ :config => nil
155
+ }
156
+ else
157
+ validity_status = @validity_checker.check(validated_config.ca_cert.subject.to_s,certid.serial)
158
+ return {
159
+ :certid => certid,
160
+ :status => validity_status.ocsp_status,
161
+ :revocation_reason => validity_status.revocation_reason,
162
+ :revocation_time => validity_status.revocation_time,
163
+ :config => validated_config
164
+ }
165
+ end
166
+ end
167
+ end
168
+
169
+ #signs OCSP responses
170
+ class ResponseSigner
171
+ # @option options [Boolean] :copy_nonce
172
+ def initialize(options)
173
+ if options.has_key?(:copy_nonce)
174
+ @copy_nonce = options[:copy_nonce]
175
+ else
176
+ @copy_nonce = false
177
+ end
178
+ end
179
+
180
+ # It is UNWISE to call this method directly because it assumes that the request is
181
+ # validated. You probably want to take a look at R509::Ocsp::Signer#handle_request
182
+ #
183
+ # @param request [OpenSSL::OCSP::Request]
184
+ # @param statuses [Hash] hash from R509::Ocsp::Helper::RequestChecker#check_statuses
185
+ # @return [OpenSSL::OCSP::BasicResponse]
186
+ def create_basic_response(request,statuses)
187
+ basic_response = OpenSSL::OCSP::BasicResponse.new
188
+
189
+ basic_response.copy_nonce(request) if @copy_nonce
190
+
191
+ statuses.each do |status|
192
+ #revocation time is retarded and is relative to now, so
193
+ #let's figure out what that is.
194
+ if status[:status] == OpenSSL::OCSP::V_CERTSTATUS_REVOKED
195
+ revocation_time = status[:revocation_time].to_i - Time.now.to_i
196
+ end
197
+ basic_response.add_status(status[:certid],
198
+ status[:status],
199
+ status[:revocation_reason],
200
+ revocation_time,
201
+ -1*status[:config].ocsp_start_skew_seconds,
202
+ status[:config].ocsp_validity_hours*3600,
203
+ [] #array of OpenSSL::X509::Extensions
204
+ )
205
+ end
206
+
207
+ #this method assumes the request data is validated by validate_request so all configs will be the same and
208
+ #we can choose to use the first one safely
209
+ config = statuses[0][:config]
210
+
211
+ #confusing, but R509::Cert contains R509::PrivateKey under #key. PrivateKey#key gives the OpenSSL object
212
+ #turns out BasicResponse#sign can take up to 4 params
213
+ #cert, key, array of OpenSSL::X509::Certificates, flags (not sure what the enumeration of those are)
214
+ basic_response.sign(config.ocsp_cert.cert,config.ocsp_cert.key.key,config.ocsp_chain)
215
+ end
216
+
217
+ # Builds final response.
218
+ #
219
+ # @param response_status [OpenSSL::OCSP::RESPONSE_STATUS_*] the primary response status
220
+ # @param basic_response [OpenSSL::OCSP::BasicResponse] an optional basic response object
221
+ # generated by create_basic_response
222
+ # @return [OpenSSL::OCSP::OCSPResponse]
223
+ def create_response(response_status,basic_response=nil)
224
+
225
+ # first arg is the response status code, comes from this list
226
+ # these can also be enumerated via OpenSSL::OCSP::RESPONSE_STATUS_*
227
+ #OCSPResponseStatus ::= ENUMERATED {
228
+ # successful (0), --Response has valid confirmations
229
+ # malformedRequest (1), --Illegal confirmation request
230
+ # internalError (2), --Internal error in issuer
231
+ # tryLater (3), --Try again later
232
+ # --(4) is not used
233
+ # sigRequired (5), --Must sign the request
234
+ # unauthorized (6) --Request unauthorized
235
+ #}
236
+ #
237
+ R509::Ocsp::Response.new(
238
+ OpenSSL::OCSP::Response.create(
239
+ response_status, basic_response
240
+ )
241
+ )
242
+ end
243
+ end
244
+ end