tlspretense 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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