cryptcheck/lib/cryptcheck/tls/server.rb

308 lines
8.6 KiB
Ruby
Raw Normal View History

2015-01-21 23:11:30 +00:00
require 'socket'
require 'openssl'
require 'httparty'
module CryptCheck
module Tls
class Server
2015-08-12 23:51:14 +00:00
TCP_TIMEOUT = 10
SSL_TIMEOUT = 2*TCP_TIMEOUT
EXISTING_METHODS = %i(TLSv1_2 TLSv1_1 TLSv1 SSLv3 SSLv2)
2015-01-21 23:11:30 +00:00
SUPPORTED_METHODS = ::OpenSSL::SSL::SSLContext::METHODS
class TLSException < ::Exception
end
class TLSNotAvailableException < TLSException
end
2015-08-16 11:12:08 +00:00
class MethodNotAvailable < TLSException
end
2015-01-21 23:11:30 +00:00
class CipherNotAvailable < TLSException
end
2016-05-03 17:57:34 +00:00
class Timeout < Exception
2015-01-21 23:11:30 +00:00
end
2016-05-03 17:57:34 +00:00
class TLSTimeout < Timeout
2015-01-21 23:11:30 +00:00
end
2016-05-03 17:57:34 +00:00
class ConnectionError < Exception
2015-01-21 23:11:30 +00:00
end
attr_reader :family, :ip, :port, :hostname, :prefered_ciphers, :cert, :cert_valid, :cert_trusted, :dh
2015-01-21 23:11:30 +00:00
2016-05-03 17:57:34 +00:00
def initialize(hostname, family, ip, port)
@hostname, @family, @ip, @port = hostname, family, ip, port
@dh = []
Logger.info { name.colorize :blue }
2015-01-21 23:11:30 +00:00
extract_cert
2015-08-19 16:04:13 +00:00
Logger.info { '' }
Logger.info { "Key : #{Tls.key_to_s self.key}" }
2015-01-21 23:11:30 +00:00
fetch_prefered_ciphers
check_supported_cipher
uniq_dh
2015-01-21 23:11:30 +00:00
end
def key
@cert.public_key
2015-01-21 23:11:30 +00:00
end
def cipher_size
supported_ciphers.collect { |c| c.size }.min
end
def supported_protocols
@supported_ciphers.keys
end
def supported_ciphers
@supported_ciphers.values.flatten 1
end
def supported_ciphers_by_protocol(protocol)
@supported_ciphers[protocol]
2015-01-21 23:11:30 +00:00
end
EXISTING_METHODS.each do |method|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{method.to_s.downcase}?
!prefered_ciphers[:#{method}].nil?
end
RUBY_EVAL
end
{
2015-08-12 23:51:14 +00:00
md2: %w(md2WithRSAEncryption),
md5: %w(md5WithRSAEncryption md5WithRSA),
2015-01-21 23:11:30 +00:00
sha1: %w(sha1WithRSAEncryption sha1WithRSA dsaWithSHA1 dsaWithSHA1_2 ecdsa_with_SHA1)
}.each do |name, signature|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
2015-08-12 23:51:14 +00:00
def #{name}_sig?
#{signature}.include? @cert.signature_algorithm
end
2015-01-21 23:11:30 +00:00
RUBY_EVAL
end
Cipher::TYPES.each do |type, _|
2015-01-21 23:11:30 +00:00
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
2015-08-12 23:51:14 +00:00
def #{type}?
supported_ciphers.any? { |c| c.#{type}? }
2015-08-12 23:51:14 +00:00
end
2015-01-21 23:11:30 +00:00
RUBY_EVAL
end
2015-08-19 16:04:13 +00:00
def key_size
@cert.public_key.rsa_equivalent_size
end
2015-01-21 23:11:30 +00:00
def ssl?
sslv2? or sslv3?
end
def tls?
tlsv1? or tlsv1_1? or tlsv1_2?
end
def tls_only?
tls? and !ssl?
end
def pfs?
supported_ciphers.any? { |c| c.pfs? }
2015-01-21 23:11:30 +00:00
end
def pfs_only?
supported_ciphers.all? { |c| c.pfs? }
2015-01-21 23:11:30 +00:00
end
private
def name
name = "#{@hostname || @ip}:#@port"
name += " [#@ip]" if @hostname
name
end
def connect(&block)
socket = ::Socket.new @family, sock_type
sockaddr = ::Socket.sockaddr_in @port, @ip
2016-05-03 17:57:34 +00:00
Logger.trace { "Connecting to #{@ip}:#{@port}" }
2015-01-21 23:11:30 +00:00
begin
status = socket.connect_nonblock sockaddr
2016-05-03 17:57:34 +00:00
Logger.trace { "Connecting to #{@ip}:#{@port} status : #{status}" }
2015-01-21 23:11:30 +00:00
raise ConnectionError, status unless status == 0
2016-05-03 17:57:34 +00:00
Logger.trace { "Connected to #{@ip}:#{@port}" }
2015-01-21 23:11:30 +00:00
block_given? ? block.call(socket) : nil
rescue ::IO::WaitReadable
2016-05-03 17:57:34 +00:00
Logger.trace { "Waiting for read to #{@ip}:#{@port}" }
raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select [socket], nil, nil, TCP_TIMEOUT
2015-01-21 23:11:30 +00:00
retry
rescue ::IO::WaitWritable
2016-05-03 17:57:34 +00:00
Logger.trace { "Waiting for write to #{@ip}:#{@port}" }
raise Timeout, "Timeout when connect to #{@ip}:#{@port} (max #{TCP_TIMEOUT.humanize})" unless IO.select nil, [socket], nil, TCP_TIMEOUT
2015-01-21 23:11:30 +00:00
retry
ensure
socket.close
end
end
def ssl_connect(socket, context, method, &block)
2015-08-12 23:51:14 +00:00
ssl_socket = ::OpenSSL::SSL::SSLSocket.new socket, context
ssl_socket.hostname = @hostname if @hostname and method != :SSLv2
Logger.trace { "SSL connecting to #{name}" }
2015-01-21 23:11:30 +00:00
begin
ssl_socket.connect_nonblock
Logger.trace { "SSL connected to #{name}" }
2015-01-21 23:11:30 +00:00
return block_given? ? block.call(ssl_socket) : nil
rescue ::IO::WaitReadable
Logger.trace { "Waiting for SSL read to #{name}" }
2016-05-03 17:57:34 +00:00
raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select [ssl_socket], nil, nil, SSL_TIMEOUT
2015-01-21 23:11:30 +00:00
retry
rescue ::IO::WaitWritable
Logger.trace { "Waiting for SSL write to #{name}" }
2016-05-03 17:57:34 +00:00
raise TLSTimeout, "Timeout when TLS connect to #{@ip}:#{@port} (max #{SSL_TIMEOUT.humanize})" unless IO.select nil, [ssl_socket], nil, SSL_TIMEOUT
2015-01-21 23:11:30 +00:00
retry
2015-08-16 11:12:08 +00:00
rescue ::OpenSSL::SSL::SSLError => e
2016-05-03 17:57:34 +00:00
case e
2015-08-16 11:12:08 +00:00
when /state=SSLv2 read server hello A$/,
/state=SSLv3 read server hello A: wrong version number$/
raise MethodNotAvailable, e
when /state=error: no ciphers available$/,
/state=SSLv3 read server hello A: sslv3 alert handshake failure$/
raise CipherNotAvailable, e
end
2015-08-12 23:51:14 +00:00
rescue => e
2016-05-03 17:57:34 +00:00
case e
2015-08-16 11:12:08 +00:00
when /^Connection reset by peer$/
raise MethodNotAvailable, e
end
2015-01-21 23:11:30 +00:00
ensure
ssl_socket.close
end
end
def ssl_client(method, ciphers = nil, &block)
2015-08-12 23:51:14 +00:00
ssl_context = ::OpenSSL::SSL::SSLContext.new method
2015-01-21 23:11:30 +00:00
ssl_context.ciphers = ciphers if ciphers
2015-08-12 23:51:14 +00:00
Logger.trace { "Try #{method} connection with #{ciphers}" }
connect do |socket|
ssl_connect socket, ssl_context, method do |ssl_socket|
return block_given? ? block.call(ssl_socket) : nil
2015-01-21 23:11:30 +00:00
end
end
Logger.debug { "No SSL available on #{name}" }
2015-01-21 23:11:30 +00:00
raise CipherNotAvailable
end
def extract_cert
2015-02-12 00:48:30 +00:00
EXISTING_METHODS.each do |method|
2015-01-21 23:11:30 +00:00
next unless SUPPORTED_METHODS.include? method
begin
@cert, @chain = ssl_client(method) { |s| [s.peer_cert, s.peer_cert_chain] }
2015-08-12 23:51:14 +00:00
Logger.debug { "Certificate #{@cert.subject}" }
2015-01-21 23:11:30 +00:00
break
2015-08-16 11:12:08 +00:00
rescue TLSException
2015-01-21 23:11:30 +00:00
end
end
raise TLSNotAvailableException unless @cert
@cert_valid = ::OpenSSL::SSL.verify_certificate_identity @cert, (@hostname || @ip)
2015-01-21 23:11:30 +00:00
@cert_trusted = verify_trust @chain, @cert
end
def prefered_cipher(method)
cipher = ssl_client(method, 'ALL:COMPLEMENTOFALL') { |s| Cipher.new method, s.cipher, s.tmp_key }
Logger.info { "Prefered cipher for #{Tls.colorize method} : #{cipher.colorize}" }
2015-01-21 23:11:30 +00:00
cipher
2015-08-16 11:12:08 +00:00
rescue TLSException => e
Logger.debug { "Method #{Tls.colorize method} not supported : #{e}" }
2015-01-21 23:11:30 +00:00
nil
end
def fetch_prefered_ciphers
@prefered_ciphers = {}
2015-02-12 00:48:30 +00:00
EXISTING_METHODS.each do |method|
2015-01-21 23:11:30 +00:00
next unless SUPPORTED_METHODS.include? method
@prefered_ciphers[method] = prefered_cipher method
end
2015-08-16 11:12:08 +00:00
raise TLSNotAvailableException unless @prefered_ciphers.any? { |_, c| !c.nil? }
2015-01-21 23:11:30 +00:00
end
def available_ciphers(method)
2015-08-12 23:51:14 +00:00
context = ::OpenSSL::SSL::SSLContext.new method
context.ciphers = 'ALL:COMPLEMENTOFALL'
context.ciphers
2015-01-21 23:11:30 +00:00
end
def supported_cipher?(method, cipher)
2015-08-19 16:04:13 +00:00
dh = ssl_client method, [cipher] { |s| s.tmp_key }
@dh << dh if dh
cipher = Cipher.new method, cipher, dh
2016-05-03 17:57:34 +00:00
dh = dh ? " (#{'DH'.colorize :green} : #{Tls.key_to_s dh})" : ''
Logger.info { "#{Tls.colorize method} / #{cipher.colorize} : Supported#{dh}" }
cipher
2015-01-21 23:11:30 +00:00
rescue TLSException => e
2015-08-23 11:10:28 +00:00
cipher = Cipher.new method, cipher
Logger.debug { "#{Tls.colorize method} / #{cipher.colorize} : Not supported (#{e})" }
nil
2015-01-21 23:11:30 +00:00
end
def check_supported_cipher
2015-08-12 23:51:14 +00:00
Logger.info { '' }
2015-01-21 23:11:30 +00:00
@supported_ciphers = {}
2015-02-12 00:48:30 +00:00
EXISTING_METHODS.each do |method|
2015-01-21 23:11:30 +00:00
next unless SUPPORTED_METHODS.include? method and @prefered_ciphers[method]
supported_ciphers = available_ciphers(method).collect { |c| supported_cipher? method, c }.reject { |c| c.nil? }
Logger.info { '' } unless supported_ciphers.empty?
@supported_ciphers[method] = supported_ciphers
2015-01-21 23:11:30 +00:00
end
end
def verify_trust(chain, cert)
2015-08-12 23:51:14 +00:00
store = ::OpenSSL::X509::Store.new
2015-01-21 23:11:30 +00:00
store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
2015-07-11 19:36:54 +00:00
store.set_default_paths
2016-01-09 15:19:13 +00:00
%w(/etc/ssl/certs).each do |directory|
::Dir.glob(::File.join directory, '*.pem').each do |file|
2015-07-11 19:36:54 +00:00
cert = ::OpenSSL::X509::Certificate.new ::File.read file
begin
store.add_cert cert
rescue ::OpenSSL::X509::StoreError
2015-01-21 23:11:30 +00:00
end
end
end
chain.each do |cert|
begin
store.add_cert cert
rescue ::OpenSSL::X509::StoreError
end
end
2015-07-11 19:36:54 +00:00
trusted = store.verify cert
p store.error_string unless trusted
trusted
2015-01-21 23:11:30 +00:00
end
def uniq_dh
dh, find = [], []
@dh.each do |k|
f = [k.type, k.size]
unless find.include? f
dh << k
find << f
end
end
@dh = dh
end
2015-01-21 23:11:30 +00:00
end
2015-02-25 19:42:51 +00:00
class TcpServer < Server
private
def sock_type
::Socket::SOCK_STREAM
end
end
class UdpServer < Server
private
def sock_type
::Socket::SOCK_DGRAM
end
end
2015-01-21 23:11:30 +00:00
end
end