Permalink
Fetching contributors…
Cannot retrieve contributors at this time
368 lines (332 sloc) 14.5 KB
# encoding: utf-8
require 'logstash/inputs/base'
require 'logstash/namespace'
require 'socket' # for Socket.gethostname
require 'openssl'
require 'httpi'
require 'json'
require 'time'
# periodically check OCSP Responder and capture the response output as an event.
#
# Notes:a
#
# * The '@source' of this event will be the command run.
# * The '@message' of this event will be the entire stdout of the command
# as one event.
#
class LogStash::Inputs::OCSP < LogStash::Inputs::Base
config_name 'ocsp'
milestone 2
default :codec, 'json'
# certificate. File with PEM-encoded ee-certificate to check
config :certificate, validate: :string, required: true
# issuer. File with PEM-encioded issuer of ee-cert
config :ca, validate: :string, required: true
# ocsp_uri. For example, "http://ocsp.example.org/ocspr"
# when ommited, ocsp uri from certificate's AIA is used
config :ocsp_uri, validate: :string, default: nil
# Interval to run the request. Value is in seconds.
config :interval, validate: :number, required: true
# conn_timeout. Timeout for HTTP Connection establishment
config :conn_timeout, validate: :number, default: 2
# read_timeout. Timeout for HTTP Response from server
config :read_timeout, validate: :number, default: 2
# expected. Expected result for OCSP check
# valid values:
# 0 => OCSP returned good for certificate
# 1 => OCSP Response Status malformedRequest
# 2 => OCSP Response Status internalError
# 3 => OCSP Response Status tryLater
# 5 => OCSP Response Status sigRequired
# 6 => OCSP Response Status unauthorized
# 11 => OCSP Response contains other serialNumber than Request
# 12 => OCSP Response is not yet valid or lastUpdate has passed
# 21 => OCSP returned revoked for certificate
# 22 => OCSP returned unknown for certificate
# 99 => Generic Network error (Timeout, DNS failure, etc.)
# >300 => HTTP error returned by Request
config :expected, validate: :number, default: 0
# simple_check. Accept any valid OCSP response. Only fail when
# OCSP service does not return anything or HTTP Code is not 200
config :simple_check, validate: :boolean, default: true
public
def register
@cert = OpenSSL::X509::Certificate.new(File.read(@certificate))
@issuer = OpenSSL::X509::Certificate.new(File.read(@ca))
HTTPI.logger = @logger
if @ocsp_uri.nil?
@cert.extensions.each do |ext|
if ext.oid == 'authorityInfoAccess'
re = %r{.*OCSP.*?http://([^"].*?)"}
match = ext.value.match re
if match.length > 1
@ocsp_uri = 'http://' + match[1]
else
@logger.error("No OCSP server uri given and couldn't find one in certificate")
exit 1
end
end
next
end
end
cert_serial = OpenSSL::ASN1::Integer.new(@cert.serial.to_i)
issuer_name_hash = OpenSSL::ASN1::OctetString.new(OpenSSL::Digest::SHA1.digest(@cert.issuer.to_der))
# issuer_key = OpenSSL::ASN1.decode(@issuer.public_key.to_der).value[1].value
issuer_key = @issuer.public_key.to_der
# puts issuer_key
issuer_key_hash = OpenSSL::ASN1::OctetString.new(OpenSSL::Digest::SHA1.digest(issuer_key))
hash_algorithm = OpenSSL::ASN1::ObjectId.new('1.3.14.3.2.26')
hash_algorithm_seq = OpenSSL::ASN1::Sequence.new([hash_algorithm, OpenSSL::ASN1::Null.new(nil)])
@logger.info('Certificate to check',
serial: @cert.serial.to_i,
issuer_key_hash: OpenSSL::Digest::SHA1.hexdigest(issuer_key),
issuer_name_hash: OpenSSL::Digest::SHA1.hexdigest(@cert.issuer.to_der))
@cert_id = OpenSSL::ASN1::Sequence.new([
hash_algorithm_seq,
issuer_name_hash,
issuer_key_hash,
cert_serial
])
@logger.info('Registering OCSP Input', type: @type,
ocsp_uri: @ocsp_uri, cert: @certificate, interval: @interval)
end # def register
public
def run(queue)
hostname = Socket.gethostname
loop do
message = {
http_code: nil,
ocsp_code: nil,
ocsp_result: nil,
ocsp_uri: nil,
ocsp_issuer_name_hash: nil,
ocsp_issuer_key_hash: nil,
ocsp_serial: nil,
ocsp_status_code: nil,
ocsp_status_result: nil,
ocsp_produced_at: nil,
ocsp_time_from: nil,
ocsp_time_to: nil,
ocsp_time_revoked: nil,
ocsp_revocation_text: nil,
ocsp_revocation_reason: nil,
text: nil,
return_code: nil,
alarm: nil
}
message[:ocsp_uri] = @ocsp_uri
message[:expected] = @expected
log_id = Random.rand(99999)
request = OpenSSL::ASN1::Sequence.new([@cert_id])
req_list = OpenSSL::ASN1::Sequence.new([request])
tbsrequest = OpenSSL::ASN1::Sequence.new([req_list])
ocsp_request = OpenSSL::ASN1::Sequence.new([tbsrequest])
http_request = HTTPI::Request.new
http_request.url = message[:ocsp_uri]
http_request.headers = {
'Content-Type' => 'application/ocsp-request'
}
http_request.open_timeout = @conn_timeout
http_request.read_timeout = @read_timeout
http_request.body = ocsp_request.to_der
start = Time.now
@logger.info? && @logger.info('Running ocsp request',
log_id: log_id,
ocsp_uri: @ocsp_uri,
certificate: @certificate,
ca: @ca
)
begin
http_response = HTTPI.post(http_request)
message[:http_code] = http_response.code
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError => e
message[:text] = "NET error (#{e.class}): #{e.to_s}"
message[:http_code] = 99
end
if message[:http_code] >= 100 && http_response.error?
message[:text] = "Not a successfull HTTP status: #{message[:http_code]}"
message[:return_code] = message[:http_code]
elsif message[:http_code] >= 100
ocsp_response = OpenSSL::ASN1.decode(http_response.body)
message[:ocsp_code] = ocsp_response.value[0].value # status
case message[:ocsp_code]
when 0
message[:ocsp_result] = 'successful'
when 1
message[:ocsp_result] = 'malformedRequest'
when 2
message[:ocsp_result] = 'internalError'
when 3
message[:ocsp_result] = 'tryLater'
when 5
message[:ocsp_result] = 'sigRequired'
when 6
message[:ocsp_result] = 'unauthorized'
else
message[:ocsp_result] = 'invalidCode'
end
if message[:ocsp_code] != 0
message[:text] = "Not a successful status: #{message[:ocsp_code]}: #{message[:ocsp_result]}"
message[:return_code] = message[:ocsp_code]
else
response_bytes = ocsp_response.value[1]
response_bytes.tag != 0 ? @logger.warn("ResponseBytes ASN1 tag is not 0 but #{response_bytes.tag}", log_id: log_id) :
response_type = response_bytes.value[0].value[0]
response = response_bytes.value[0].value[1]
response_type.oid != '1.3.6.1.5.5.7.48.1.1' ? @logger.warn("Basic OCSP Response OID not found! OID is '#{response_type.oid}' but should be '1.3.6.1.5.5.7.48.1.1'", log_id: log_id) :
ocsp_basic_response = OpenSSL::ASN1.decode(response.value)
tbs_responsedata = ocsp_basic_response.value[0]
# sigAlg = ocsp_basic_response.value[1]
# signature = ocsp_basic_response.value[2]
case tbs_responsedata.value[0].tag
when 0
# first element is version
# response_version = tbs_responsedata.value[0].value
# responder_id = tbs_responsedata.value[1].value
response_time = tbs_responsedata.value[2].value
responses = tbs_responsedata.value[3].value
when 1..2
# first element is responder id
# response_version = 0
# responder_id = tbs_responsedata.value[0].value
response_time = tbs_responsedata.value[1].value
responses = tbs_responsedata.value[2].value
else
@logger.warn('Invalid tbs_responsedata', tbs_responsedata: tbs_responsedata, tag: tbs_responsedata.value[0].tag, log_id: log_id)
end
message[:ocsp_produced_at] = response_time
message[:ocsp_serial] = responses[0].value[0].value[3].value # basic.status[0][0].serial
message[:ocsp_issuer_name_hash] = OpenSSL::Digest::SHA1.hexdigest(responses[0].value[0].value[1].value)
message[:ocsp_issuer_key_hash] = OpenSSL::Digest::SHA1.hexdigest(responses[0].value[0].value[2].value)
if message[:ocsp_serial] != @cert.serial
message[:text] = "Not the same serial OCSP: '#{message[:ocsp_serial]}', Cert: '#{@cert.serial}'"
message[:return_code] = 11
end
current_time = Time.now
message[:ocsp_time_from] = responses[0].value[2].value
if responses[0].value.length >= 4 && responses[0].value[3].tag == 0
# this is a nextupdate
message[:ocsp_time_to] = responses[0].value[3].value[0].value
elsif responses[0].value.length >= 4 && responses[0].value[3].tag == 1
# this is a singleextension - I just ignore it for now
message[:ocsp_time_to] = current_time + 120
else
# i allow the response to be 120 seconds old
message[:ocsp_time_to] = current_time + 120
end
message[:"@timestamp"] = current_time.iso8601
if message[:ocsp_time_from] > current_time || message[:ocsp_time_to] < current_time
message[:text] = 'The response is not within its validity window'
message[:return_code] = 12
end
status = responses[0].value[1]
status_info = responses[0].value[1].value
if status.tag == 0
# good
message[:ocsp_status_code] = 0
elsif status.tag == 1
# revoked
message[:ocsp_status_code] = 1
else
# unknown
message[:ocsp_status_code] = 2
end
case message[:ocsp_status_code]
when 0
message[:ocsp_status_result] = 'good'
when 1
message[:ocsp_status_result] = 'revoked'
message[:ocsp_time_revoked] = status_info[0].value
if status_info.count == 2
# there could be a revocation_reason
status_info[1].tag == 0 ? message[:ocsp_revocation_reason] = status_info[1].value : message[:ocsp_revocation_reason] = -1
else
# no revocation_reason
message[:ocsp_revocation_reason] = -1
end
case message[:ocsp_revocation_reason]
when -1
message[:ocsp_revocation_text] = 'notProvidedInResponse'
when 0
message[:ocsp_revocation_text] = 'unspecified'
when 1
message[:ocsp_revocation_text] = 'keyCompromise'
when 2
message[:ocsp_revocation_text] = 'CACompromise'
when 3
message[:ocsp_revocation_text] = 'affiliationChanged'
when 4
message[:ocsp_revocation_text] = 'superseeded'
when 5
message[:ocsp_revocation_text] = 'cessationOfOperation'
when 6
message[:ocsp_revocation_text] = 'certificateHold'
when 8
message[:ocsp_revocation_text] = 'removeFromCRL'
when 9
message[:ocsp_revocation_text] = 'privilegeWithdrawn'
when 10
message[:ocsp_revocation_text] = 'AACompromise'
else
message[:ocsp_revocation_text] = 'invalidCode'
end
when 2
message[:ocsp_status_result] = 'unknown'
else
message[:ocsp_status_result] = 'invalidCode'
end
if message[:ocsp_status_code] != 0 # 0 is good, 1 is revoked, 2 is unknown.
message[:text] = "Not a good status: #{message[:ocsp_status_code]}: #{message[:ocsp_status_result]}"
message[:return_code] = message[:ocsp_status_code] + 20
end
if message[:return_code].nil? && message[:text].nil?
message[:return_code] = 0
message[:text] = 'OK'
end
end
else
message[:return_code] = message[:http_code]
message.delete(:http_code)
end
if message[:return_code] == @expected
@logger.info? && @logger.info('Got expected result:', ocsp_uri: @ocsp_uri, expected: @expected, return_code: message[:return_code], log_id: log_id)
message[:alarm] = "closed"
message[:simple_check] = @simple_check
elsif @simple_check && message[:return_code] < 99
@logger.info? && @logger.info('Got unexpected but valid result:', ocsp_uri: @ocsp_uri, expected: @expected, return_code: message[:return_code], log_id: log_id)
message[:alarm] = "closed"
message[:simple_check] = true
else # unexpected return code >= 99
@logger.warn? && @logger.warn('Got wrong result:', ocsp_uri: @ocsp_uri, expected: @expected, return_code: message[:return_code], log_id: log_id)
message[:alarm] = "open"
message[:simple_check] = @simple_check
end
message.delete_if { |_k, v| v.nil? }
duration = Time.now - start
@codec.decode(message.to_json) do |event|
decorate(event)
event['host'] = hostname
event['duration'] = duration
queue << event
end
@logger.info? && @logger.info('OCSP request completed',
ocsp_uri: @ocsp_uri,
duration: duration,
log_id: log_id
)
# Sleep for the remainder of the interval, or 0 if the duration ran
# longer than the interval.
sleeptime = [0, @interval - duration].max
if sleeptime == 0
@logger.warn('Execution ran longer than the interval. Skipping sleep.',
ocsp_uri: @ocsp_uri,
duration: duration,
interval: @interval,
log_id: log_id
)
else
sleep(sleeptime)
end
end # loop
end # def run
end # class LogStash::Inputs::OCSP