tlspretense 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/.document +6 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +41 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.rdoc +231 -0
  8. data/Rakefile +44 -0
  9. data/bin/makeder.sh +6 -0
  10. data/bin/tlspretense +7 -0
  11. data/bin/view.sh +3 -0
  12. data/doc/general_setup.rdoc +288 -0
  13. data/doc/linux_setup.rdoc +64 -0
  14. data/lib/certmaker.rb +61 -0
  15. data/lib/certmaker/certificate_factory.rb +106 -0
  16. data/lib/certmaker/certificate_suite_generator.rb +120 -0
  17. data/lib/certmaker/ext_core/hash_indifferent_fetch.rb +12 -0
  18. data/lib/certmaker/runner.rb +27 -0
  19. data/lib/certmaker/tasks.rb +20 -0
  20. data/lib/packetthief.rb +167 -0
  21. data/lib/packetthief/handlers.rb +14 -0
  22. data/lib/packetthief/handlers/abstract_ssl_handler.rb +249 -0
  23. data/lib/packetthief/handlers/proxy_redirector.rb +26 -0
  24. data/lib/packetthief/handlers/ssl_client.rb +87 -0
  25. data/lib/packetthief/handlers/ssl_server.rb +174 -0
  26. data/lib/packetthief/handlers/ssl_smart_proxy.rb +143 -0
  27. data/lib/packetthief/handlers/ssl_transparent_proxy.rb +225 -0
  28. data/lib/packetthief/handlers/transparent_proxy.rb +183 -0
  29. data/lib/packetthief/impl.rb +11 -0
  30. data/lib/packetthief/impl/ipfw.rb +140 -0
  31. data/lib/packetthief/impl/manual.rb +54 -0
  32. data/lib/packetthief/impl/netfilter.rb +109 -0
  33. data/lib/packetthief/impl/pf_divert.rb +168 -0
  34. data/lib/packetthief/impl/pf_rdr.rb +192 -0
  35. data/lib/packetthief/logging.rb +49 -0
  36. data/lib/packetthief/redirect_rule.rb +29 -0
  37. data/lib/packetthief/util.rb +36 -0
  38. data/lib/ssl_test.rb +21 -0
  39. data/lib/ssl_test/app_context.rb +17 -0
  40. data/lib/ssl_test/certificate_manager.rb +33 -0
  41. data/lib/ssl_test/config.rb +79 -0
  42. data/lib/ssl_test/ext_core/io_raw_input.rb +31 -0
  43. data/lib/ssl_test/input_handler.rb +35 -0
  44. data/lib/ssl_test/runner.rb +110 -0
  45. data/lib/ssl_test/runner_options.rb +68 -0
  46. data/lib/ssl_test/ssl_test_case.rb +46 -0
  47. data/lib/ssl_test/ssl_test_report.rb +24 -0
  48. data/lib/ssl_test/ssl_test_result.rb +30 -0
  49. data/lib/ssl_test/test_listener.rb +140 -0
  50. data/lib/ssl_test/test_manager.rb +116 -0
  51. data/lib/tlspretense.rb +13 -0
  52. data/lib/tlspretense/app.rb +52 -0
  53. data/lib/tlspretense/init_runner.rb +115 -0
  54. data/lib/tlspretense/skel/ca/goodcacert.pem +19 -0
  55. data/lib/tlspretense/skel/ca/goodcakey.pem +27 -0
  56. data/lib/tlspretense/skel/config.yml +523 -0
  57. data/lib/tlspretense/version.rb +3 -0
  58. data/packetthief_examples/em_ssl_test.rb +73 -0
  59. data/packetthief_examples/redirector.rb +29 -0
  60. data/packetthief_examples/setup_iptables.sh +24 -0
  61. data/packetthief_examples/ssl_client_simple.rb +27 -0
  62. data/packetthief_examples/ssl_server_simple.rb +44 -0
  63. data/packetthief_examples/ssl_smart_proxy.rb +115 -0
  64. data/packetthief_examples/ssl_transparent_proxy.rb +97 -0
  65. data/packetthief_examples/transparent_proxy.rb +56 -0
  66. data/spec/packetthief/impl/ipfw_spec.rb +98 -0
  67. data/spec/packetthief/impl/manual_spec.rb +65 -0
  68. data/spec/packetthief/impl/netfilter_spec.rb +66 -0
  69. data/spec/packetthief/impl/pf_divert_spec.rb +82 -0
  70. data/spec/packetthief/impl/pf_rdr_spec.rb +133 -0
  71. data/spec/packetthief/logging_spec.rb +78 -0
  72. data/spec/packetthief_spec.rb +47 -0
  73. data/spec/spec_helper.rb +53 -0
  74. data/spec/ssl_test/certificate_manager_spec.rb +222 -0
  75. data/spec/ssl_test/config_spec.rb +76 -0
  76. data/spec/ssl_test/runner_spec.rb +360 -0
  77. data/spec/ssl_test/ssl_test_case_spec.rb +113 -0
  78. data/spec/ssl_test/test_listener_spec.rb +199 -0
  79. data/spec/ssl_test/test_manager_spec.rb +324 -0
  80. data/tlspretense.gemspec +35 -0
  81. metadata +262 -0
@@ -0,0 +1,31 @@
1
+ require 'termios'
2
+
3
+ # Extends IO to enable "raw" input on TTYs.
4
+ class IO
5
+ # Enables raw character input for a TTY. It uses the ruby-termios gem to
6
+ # disable ICANON and ECHO functionality. This means that characters will
7
+ # become immediately available to IO#gets, IO#getchar, etc. after the user
8
+ # presses a button, and that the characters will not be implicitly echoed to
9
+ # the screen.
10
+ #
11
+ # If you call this on $stdin, you probably should ensure that you call
12
+ # #disable_raw_chars in order to restore the previous termios state after
13
+ # your program exits. Otherwise, you may screw up the user's terminal, and
14
+ # they will have to call `reset`.
15
+ def enable_raw_chars
16
+ raise IOError, "#{self} is not a TTY." unless self.tty?
17
+ @_oldtermios = Termios.tcgetattr(self)
18
+ newt = @_oldtermios.dup
19
+ newt.c_lflag &= ~Termios::ICANON
20
+ newt.c_lflag &= ~Termios::ECHO
21
+ Termios.tcsetattr(self, Termios::TCSANOW, newt)
22
+ end
23
+
24
+ # Reverts the termios state to what it was before calling #enable_raw_chars.
25
+ def disable_raw_chars
26
+ raise IOError, "#{self} is not a TTY." unless self.tty?
27
+ Termios.tcsetattr(self, Termios::TCSANOW, @_oldtermios)
28
+ end
29
+
30
+
31
+ end
@@ -0,0 +1,35 @@
1
+
2
+
3
+ module SSLTest
4
+ # EM handler to handle keyboard input while a test is running.
5
+ module InputHandler
6
+
7
+ def initialize(stdin=$stdin)
8
+ @stdin = stdin
9
+ @actions = {}
10
+
11
+ # Set the term to accept keystrokes immediately.
12
+ @stdin.enable_raw_chars
13
+ end
14
+
15
+ def unbind
16
+ # Clean up by resotring the old termios
17
+ @stdin.disable_raw_chars
18
+ end
19
+
20
+ # Receives one character at a time.
21
+ def receive_data(data)
22
+ raise "data was longer than 1 char: #{data.inspect}" if data.length != 1
23
+ if @actions.has_key? data
24
+ @actions[data].call
25
+ end
26
+ end
27
+
28
+ def on(char, blk=nil, &block)
29
+ puts "Warning: setting a keyboard handler for a keystroke that is longer than one char: #{char.inspect}" if char.length != 1
30
+ raise ArgumentError, "No block passed in" if blk == nil and block == nil
31
+ @actions[char] = ( blk ? blk : block)
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,110 @@
1
+ module SSLTest
2
+ # Handles a list of arguments, and uses the arguments to run a sequence of tests.
3
+ class Runner
4
+ include PacketThief::Logging
5
+
6
+ attr_reader :results
7
+
8
+ def initialize(args, stdin, stdout)
9
+ options = RunnerOptions.parse(args)
10
+ @test_list = options.args
11
+ @stdin = stdin
12
+ @stdout = stdout
13
+
14
+ @config = Config.new options.options
15
+ cert_manager = CertificateManager.new(@config.certs)
16
+ @logger = Logger.new(@config.logfile)
17
+ @logger.level = @config.loglevel
18
+ @logger.datetime_format = "%Y-%m-%d %H:%M:%S %Z"
19
+ @logger.formatter = proc do |severity, datetime, progname, msg|
20
+ "#{datetime}:#{severity}: #{msg}\n"
21
+ end
22
+ @app_context = AppContext.new(@config, cert_manager, @logger)
23
+
24
+ @report = SSLTestReport.new
25
+ init_packetthief
26
+ end
27
+
28
+ def run
29
+ case @config.action
30
+ when :list
31
+ @stdout.puts "These are the test I will perform and their descriptions:"
32
+ @stdout.puts ''
33
+ SSLTestCase.factory(@app_context, @config.tests, @test_list).each do |test|
34
+ display_test test
35
+ end
36
+ when :runtests
37
+ @stdout.puts "Press spacebar to skip a test, or 'q' to stop testing."
38
+ loginfo "Hostname being tested (assuming certs are up to date): #{@config.hosttotest}"
39
+
40
+ @tests = SSLTestCase.factory(@app_context, @config.tests, @test_list)
41
+ loginfo "Running #{@tests.length} tests"
42
+
43
+ start_packetthief
44
+ run_tests(@tests)
45
+ stop_packetthief
46
+
47
+ @report.print_results(@stdout)
48
+ else
49
+ raise "Unknown action: #{opts[:action]}"
50
+ end
51
+ end
52
+
53
+ def run_tests(testlist)
54
+ test_manager = TestManager.new(@app_context, testlist, @report, @logger)
55
+ EM.run do
56
+ # @listener handles the initial server socket, not the accepted connections.
57
+ # h in the code block is for each accepted connection.
58
+ @listener = TestListener.start('', @config.listener_port, test_manager)
59
+ @listener.logger = @logger
60
+ @keyboard = EM.open_keyboard InputHandler do |h|
61
+ h.on(' ') { test_manager.test_completed(test_manager.current_test, :skipped) }
62
+ h.on('q') { test_manager.stop_testing }
63
+ h.on("\n") { test_manager.unpause }
64
+ end
65
+ EM.add_periodic_timer(5) { logdebug "EM connection count: #{EM.connection_count}" }
66
+ end
67
+ end
68
+
69
+ # Initialize custom PacketThief modes of operation. Eg, we use manual when PT
70
+ # does not manage the firewall rules and when it should just return a
71
+ # preconfigured destination, and external for when PT does not manage the
72
+ # firewall rules but still needs to know how to discover the destination.
73
+ def init_packetthief
74
+ PacketThief.logger = @logger
75
+ if @config.packetthief.has_key? 'implementation'
76
+ impl = @config.packetthief['implementation']
77
+ case impl
78
+ when /manual\(/i
79
+ PacketThief.implementation = :manual
80
+ host = /manual\((.*)\)/i.match(impl)[1]
81
+ PacketThief.set_dest(host, @config.packetthief.fetch('dest_port',443))
82
+ when /external\(/i
83
+ real_impl = /external\((.*)\)/i.match(impl)[1]
84
+ PacketThief.implementation = real_impl.strip.downcase.to_sym
85
+ else
86
+ PacketThief.implementation = impl
87
+ end
88
+ end
89
+ end
90
+
91
+ def start_packetthief
92
+ ptconf = @config.packetthief
93
+ unless ptconf.has_key? 'implementation' and ptconf['implementation'].match(/external/i)
94
+ PacketThief.redirect(:to_ports => @config.listener_port).where(ptconf).run
95
+ end
96
+ at_exit { PacketThief.revert }
97
+ end
98
+
99
+ def stop_packetthief
100
+ PacketThief.revert
101
+ end
102
+
103
+ def display_test(test)
104
+ @stdout.printf "%s: %s\n", test.id, test.description
105
+ @stdout.printf " %s\n", test.certchainalias.inspect
106
+ @stdout.puts ''
107
+ end
108
+
109
+ end
110
+ end
@@ -0,0 +1,68 @@
1
+ module SSLTest
2
+ class RunnerOptions
3
+
4
+ DEFAULT_OPTS = {
5
+ :pause => false,
6
+ :config => Config::DEFAULT,
7
+ :action => :runtests,
8
+ :loglevel => 'INFO',
9
+ :logfile => '-'
10
+ }
11
+
12
+ # Parsed command line options.
13
+ attr_reader :options
14
+
15
+ # Any command line arguments that are not options.
16
+ attr_reader :args
17
+
18
+ def self.parse(args)
19
+ opts = new(args)
20
+ opts.parse
21
+ opts
22
+ end
23
+
24
+ def initialize(args)
25
+ @orig_args = args
26
+ end
27
+
28
+ def parse
29
+ @options = DEFAULT_OPTS.dup
30
+
31
+ opts = OptionParser.new do |opts|
32
+ opts.banner = "Usage: #{$0} [options] [tests to run]"
33
+
34
+ opts.on("-p","--[no-]pause", "Pause between tests") do
35
+ @options[:pause] = true
36
+ end
37
+
38
+ opts.on("-c", "--config path/to/config.yml",
39
+ "Specify a custom config.yml file",
40
+ " (Default: #{DEFAULT_OPTS[:config]})") do |confname|
41
+ @options[:config] = confname
42
+ end
43
+
44
+ opts.on("-l","--list", "List all tests (or those specified on the command line") do
45
+ @options[:action] = :list
46
+ end
47
+
48
+ opts.on("--log-level=loglevel", "Set the log level. It can be one of:",
49
+ " DEBUG, INFO, WARN, ERROR, FATAL", " (Default: INFO, or whatever config.yml sets)") do |level|
50
+ @options[:loglevel] = level
51
+ end
52
+
53
+ opts.on("--log-file=somefile.log", "Specify the file to write logs to."," (Default: - (STDOUT))") do |file|
54
+ @options[:logfile] = file
55
+ end
56
+ opts.on_tail('-h', '--help', "Print this help message.") do
57
+ puts opts
58
+ exit
59
+ end
60
+
61
+ end
62
+
63
+ @args = @orig_args.dup
64
+ opts.parse!(@args)
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,46 @@
1
+ module SSLTest
2
+ # Represents a single test case.
3
+ class SSLTestCase
4
+ include PacketThief::Logging
5
+
6
+ attr_reader :id
7
+ attr_reader :description
8
+ attr_reader :certchainalias
9
+ attr_reader :expected_result
10
+
11
+ attr_reader :certchain
12
+ attr_reader :keychain
13
+ attr_reader :hosttotest
14
+
15
+ def self.factory(appctx, test_data, tests_to_create)
16
+ if tests_to_create == [] or tests_to_create == nil
17
+ final_test_data = test_data
18
+ else
19
+ final_test_data = tests_to_create.map { |name| test_data.select { |test| test['alias'] == name }[0] }
20
+ end
21
+ final_test_data.map { |data| SSLTestCase.new(appctx, data) }
22
+ end
23
+
24
+ def initialize(appctx, testdesc)
25
+ @appctx = appctx
26
+ @raw = testdesc.dup
27
+ @id = @raw['alias']
28
+ @description = @raw['name']
29
+ @certchainalias = @raw['certchain']
30
+ @expected_result = @raw['expected_result']
31
+ end
32
+
33
+ def certchain
34
+ @certchain ||= @appctx.cert_manager.get_chain(@certchainalias)
35
+ end
36
+
37
+ def keychain
38
+ @keychain ||= @appctx.cert_manager.get_keychain(@certchainalias)
39
+ end
40
+
41
+ def hosttotest
42
+ @hosttotest ||= @appctx.config.hosttotest
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,24 @@
1
+ module SSLTest
2
+ # Represents an entire report. SSLTestCases add results to it, which it can
3
+ # later format.
4
+ class SSLTestReport
5
+
6
+ def initialize
7
+ @results = []
8
+ end
9
+
10
+ def add_result(result)
11
+ @results << result
12
+ end
13
+
14
+ def print_results(out)
15
+ out.puts "Alias Description P/F Expected Actual Start Time Stop Time "
16
+ out.puts "---------------- ---------------- ---- -------- -------- ---------- ----------"
17
+ @results.each do |r|
18
+ out.printf "%-16.16<id>s %-16.16<description>s %-4.4<passed>s %-8.8<expected_result>s %-8.8<actual_result>s %<start_time>s %<stop_time>s\n", r.to_h
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ module SSLTest
2
+ # SSLTestResults are created by running SSLTestCases. They are then added to
3
+ # an SSLTestReport so that they can be included in that report.
4
+ class SSLTestResult
5
+
6
+ attr_reader :id
7
+ attr_accessor :description
8
+ attr_accessor :expected_result, :actual_result
9
+ attr_accessor :start_time, :stop_time
10
+
11
+ def passed? ; @passed ; end
12
+
13
+ def initialize(testcase_id, passed=false)
14
+ @id = testcase_id
15
+ @passed = passed
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ :id => id,
21
+ :passed => passed? ? "Pass" : "Fail",
22
+ :description => description,
23
+ :expected_result => expected_result,
24
+ :actual_result => actual_result,
25
+ :start_time => start_time,
26
+ :stop_time => stop_time,
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,140 @@
1
+ module SSLTest
2
+
3
+ # TestListener is the real workhorse used by SSLTestCases. It builds on the
4
+ # SSLSmartProxy from PacketThief in order to intercept and forward SSL
5
+ # connections. It uses SSLSmartProxy because SSLSmartProxy provides a default
6
+ # behavior where it grabs the remote certificate from the destination and
7
+ # re-signs it before presenting it to the client.
8
+ #
9
+ # TestListener expands on this by presenting the configured test chain
10
+ # instead of the re-signed remote certificate when the destination
11
+ # corresponds to the hostname the test suite is testing off of.
12
+ class TestListener < PacketThief::Handlers::SSLSmartProxy
13
+
14
+ # For all hosts that do not match _hosttotest_, we currently use the
15
+ # _cacert_ and re-sign the original cert provided by the actual host. This
16
+ # will cause issues with certificate revocation.
17
+ #
18
+ # * _cacert_ [OpenSSL::X509::Certificate] A CA that the client should
19
+ # trust.
20
+ # * _cakey_ [OpenSSL::PKey::PKey] The CA's key, needed for resigning. It
21
+ # will also be the key used by the resigned certificates.
22
+ # * _hosttotest_ [String] The hostname we want to apply the test chain to.
23
+ # * _chaintotest_ [Array<OpenSSL::X509Certificate>] A chain of certs to
24
+ # present when the client attempts to connect to hostname.
25
+ # * _keytotest_ [OpenSSL::PKey::PKey] The key corresponding to the leaf
26
+ # node in _chaintotest_.
27
+ def initialize(tcpsocket, test_manager, logger=nil)
28
+ @test_manager = test_manager
29
+
30
+ if @test_manager.paused?
31
+ @paused = true
32
+ else
33
+ @paused = false
34
+ @test = @test_manager.current_test
35
+ @hosttotest = @test.hosttotest
36
+ chain = @test.certchain.dup
37
+ @hostcert = chain.shift
38
+ @hostkey = @test.keychain[0]
39
+ @extrachain = chain
40
+ end
41
+ # Use the goodca for hosts we don't care to test against.
42
+ super(tcpsocket, @test_manager.goodcacert, @test_manager.goodcakey, logger)
43
+
44
+ @test_status = :running
45
+ @testing_host = false
46
+ end
47
+
48
+ # Checks whether the initial original destination certificate (without SNI
49
+ # hostname) matches the test hostname. We do this with post_init to have
50
+ # the check happen after the parent class already added a re-signed
51
+ # certificate to +@ctx+.
52
+ def post_init
53
+ check_for_hosttotest(@ctx)
54
+ end
55
+
56
+ # Checks whether the original destination certificate after we handle the
57
+ # SNI hostname matches the test hostname. Super already replaced the
58
+ # context with a certificate based on the remote host's certificate.
59
+ def servername_cb(sslsock, hostname)
60
+ check_for_hosttotest(super(sslsock, hostname))
61
+ end
62
+
63
+ # Replaces the certificates used in the SSLContext with the test
64
+ # certificates if the destination matches the hostname we wish to test
65
+ # against. Otherwise, it leaves the context alone.
66
+ #
67
+ # Additionally, if it matches, it sets @testing_host to true to check
68
+ # whether the test succeeds or not.
69
+ def check_for_hosttotest(actx)
70
+ if @paused
71
+ logdebug "Testing is paused, not checking whether this is the host to test", :certcubject => actx.cert.subject
72
+ elsif TestListener.cert_matches_host(actx.cert, @hosttotest)
73
+ logdebug "Destination matches host-to-test", :hosttotest => @hosttotest, :certsubject => actx.cert.subject, :testname => @test.id
74
+ actx.cert = @hostcert
75
+ actx.key = @hostkey
76
+ actx.extra_chain_cert = @extrachain
77
+ @testing_host = true
78
+ else
79
+ logdebug "Destination does not match host-to-test", :hosttotest => @hosttotest, :certsubject => actx.cert.subject
80
+ end
81
+ actx
82
+ end
83
+
84
+ # Return true if _cert_'s CNAME or subjectAltName matches hostname,
85
+ # otherwise return false.
86
+ def self.cert_matches_host(cert, hostname)
87
+ OpenSSL::SSL.verify_certificate_identity(cert, hostname)
88
+ end
89
+
90
+ # If the client completes connecting, we might consider that trusting our
91
+ # certificate chain. However, at least Java's SSL client classes don't
92
+ # reject until after completing the handshake.
93
+ def tls_successful_handshake
94
+ super
95
+ logdebug "successful handshake"
96
+ if @testing_host
97
+ @test_status = :connected
98
+ if @test_manager.testing_method == 'tlshandshake'
99
+ @test_manager.test_completed(@test, @test_status)
100
+ @testing_host = false
101
+ end
102
+ end
103
+ end
104
+
105
+ # If the handshake failed, then the client rejected our cert chain.
106
+ def tls_failed_handshake(e)
107
+ super
108
+ logdebug "failed handshake"
109
+ if @testing_host
110
+ @test_status = :rejected
111
+ @test_manager.test_completed(@test, @test_status)
112
+ @testing_host = false
113
+ end
114
+ end
115
+
116
+ # Report our result.
117
+ def unbind
118
+ super
119
+ logdebug "unbind"
120
+ if @testing_host
121
+ @test_manager.test_completed(@test, @test_status)
122
+ @testing_host = false
123
+ end
124
+ end
125
+
126
+ # client_recv means that the client sent data over the TLS connection,
127
+ # meaning they definately trusted our certificate chain.
128
+ def client_recv(data)
129
+ if @testing_host
130
+ @test_status = :sentdata
131
+ if @test_manager.testing_method == 'senddata'
132
+ @test_manager.test_completed(@test, @test_status)
133
+ @testing_host = false
134
+ end
135
+ end
136
+ super(data)
137
+ end
138
+
139
+ end
140
+ end