echspec 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,170 @@
1
+ module EchSpec
2
+ module Spec
3
+ class << self
4
+ using Refinements
5
+
6
+ # @param msg [TTTLS13::Message::Record]
7
+ # @param desc [Symbol]
8
+ #
9
+ # @return [Boolean]
10
+ def expect_alert(msg, desc)
11
+ msg.is_a?(TTTLS13::Message::Alert) &&
12
+ msg.description == TTTLS13::Message::ALERT_DESCRIPTION[desc]
13
+ end
14
+
15
+ ResultDesc = Struct.new(:result, :desc)
16
+
17
+ # @param rds [Array of ResultDesc] result: EchSpec::Ok | Err, desc: String
18
+ # @param verbose [Boolean]
19
+ def print_results(rds, verbose)
20
+ rds.each { |rd| print_summary(rd.result, rd.desc) }
21
+ failures = rds.filter { |rd| rd.result.is_a? Err }
22
+ return if failures.empty?
23
+
24
+ puts
25
+ puts 'Failures:'
26
+ puts
27
+ failures.each
28
+ .with_index { |rd, idx| print_err_details(rd.result, idx, rd.desc, verbose) }
29
+ puts "#{failures.length} failure".red
30
+ end
31
+
32
+ # @param result [EchSpec::Ok | Err]
33
+ # @param desc [String]
34
+ def print_summary(result, desc)
35
+ check = "\u2714"
36
+ cross = "\u0078"
37
+ summary = case result
38
+ in Ok
39
+ "\t#{check} #{desc}".green
40
+ in Err
41
+ "\t#{cross} #{desc}".red
42
+ end
43
+ puts summary
44
+ end
45
+
46
+ # @param err [EchSpec::Err]
47
+ # @param idx [Integer]
48
+ # @param desc [String]
49
+ # @param verbose [Boolean]
50
+ def print_err_details(err, idx, desc, verbose)
51
+ puts "\t#{idx + 1}) #{desc}"
52
+ puts "\t\t#{err.details}"
53
+ warn err.message_stack if verbose && !err.message_stack.nil?
54
+ puts
55
+ end
56
+ end
57
+
58
+ class WithSocket
59
+ def with_socket(hostname, port)
60
+ socket = TCPSocket.new(hostname, port)
61
+ yield(socket)
62
+ rescue Timeout::Error
63
+ Err.new("#{hostname}:#{port} connection timeout", message_stack)
64
+ rescue Errno::ECONNREFUSED
65
+ Err.new("#{hostname}:#{port} connection refused", message_stack)
66
+ rescue Error::BeforeTargetSituationError => e
67
+ Err.new(e.message, message_stack)
68
+ ensure
69
+ socket&.close
70
+ end
71
+
72
+ def initialize
73
+ @stack = Log::MessageStack.new
74
+ end
75
+
76
+ def message_stack
77
+ @stack.marshal
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ Dir["#{File.dirname(__FILE__)}/spec/*.rb"].sort.each { |f| require f }
84
+
85
+ module EchSpec
86
+ module Spec
87
+ class << self
88
+ using Refinements
89
+
90
+ # @param fpath [String | NilClass]
91
+ # @param port [Integer]
92
+ # @param hostname [String]
93
+ # @param force_compliant [Boolean]
94
+ # @param verbose [Boolean]
95
+ def run(fpath, port, hostname, force_compliant, verbose)
96
+ TTTLS13::Logging.logger.level = Logger::WARN
97
+ puts 'TLS Encrypted Client Hello Server'
98
+ ech_config = try_get_ech_config(fpath, hostname, force_compliant)
99
+
100
+ do_run(port, hostname, ech_config, spec_groups, verbose)
101
+ end
102
+
103
+ # @param fpath [String | NilClass]
104
+ # @param port [Integer]
105
+ # @param hostname [String]
106
+ # @param sections [Array of String]
107
+ # @param verbose [Boolean]
108
+ def run_only(fpath, port, hostname, sections, verbose)
109
+ targets = spec_groups.filter { |g| sections.include?(g.section) }
110
+ force_compliant = sections.include?(Spec9.section)
111
+
112
+ TTTLS13::Logging.logger.level = Logger::WARN
113
+ puts 'TLS Encrypted Client Hello Server'
114
+ ech_config = try_get_ech_config(fpath, hostname, force_compliant)
115
+
116
+ do_run(port, hostname, ech_config, targets, verbose)
117
+ end
118
+
119
+ # @param port [Integer]
120
+ # @param hostname [String]
121
+ # @param ech_config [ECHConfig]
122
+ # @param targets [Array of EchSpec::SpecGroup]
123
+ # @param verbose [Boolean]
124
+ def do_run(port, hostname, ech_config, targets, verbose)
125
+ rds = targets.flat_map do |g|
126
+ g.spec_cases.map do |sc|
127
+ r = sc.method.call(hostname, port, ech_config)
128
+ d = "#{sc.description} [#{g.section}]"
129
+ ResultDesc.new(result: r, desc: d)
130
+ end
131
+ end
132
+
133
+ print_results(rds, verbose)
134
+ end
135
+
136
+ # @param fpath [String | NilClass]
137
+ # @param hostname [String]
138
+ # @param force_compliant [Boolean]
139
+ #
140
+ # @return [ECHConfig]
141
+ def try_get_ech_config(fpath, hostname, force_compliant)
142
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-9
143
+ case result = Spec9.try_get_ech_config(fpath, hostname, force_compliant)
144
+ in Ok(obj) if force_compliant
145
+ result.tap { |r| print_summary(r, "#{Spec9.description} [#{Spec9.section}]") }
146
+ obj
147
+ in Ok(obj)
148
+ obj
149
+ in Err(details, _)
150
+ puts "\t#{details}".red
151
+ exit 1
152
+ end
153
+ end
154
+
155
+ def spec_groups
156
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-5
157
+ groups = [Spec5_1_9, Spec5_1_10]
158
+
159
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7
160
+ groups += [Spec7_5, Spec7_1_11, Spec7_1_14_2_1, Spec7_1_1_2, Spec7_1_1_5]
161
+
162
+ groups.map(&:spec_group)
163
+ end
164
+
165
+ def sections
166
+ (spec_groups + [Spec9]).map(&:section)
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,10 @@
1
+ module EchSpec
2
+ class SpecCase
3
+ attr_reader :description, :method
4
+
5
+ def initialize(description, method)
6
+ @description = description
7
+ @method = method
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module EchSpec
2
+ class SpecGroup
3
+ attr_reader :section, :spec_cases
4
+
5
+ def initialize(section, spec_cases)
6
+ @section = section
7
+ @spec_cases = spec_cases
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,167 @@
1
+ module EchSpec
2
+ module TLS13Client
3
+ class Connection < TTTLS13::Connection
4
+ # @param socket [Socket]
5
+ # @param side [:client or :server]
6
+
7
+ # @param cipher [TTTLS13::Cryptograph::$Object]
8
+ #
9
+ # @return [TTTLS13::Message::$Object]
10
+ # @return [String]
11
+ def recv_message(cipher)
12
+ return @message_queue.shift unless @message_queue.empty?
13
+
14
+ messages = nil
15
+ orig_msgs = []
16
+ loop do
17
+ record, orig_msgs = recv_record(cipher)
18
+ messages = record.messages
19
+ break unless messages.empty?
20
+ end
21
+
22
+ @message_queue += messages[1..].zip(orig_msgs[1..])
23
+ message = messages.first
24
+ orig_msg = orig_msgs.first
25
+
26
+ [message, orig_msg]
27
+ end
28
+ end
29
+
30
+ class << self
31
+ # @param hostname [String]
32
+ #
33
+ # @return [TTTLS13::Message::Extensions]
34
+ # @return [Hash of NamedGroup => OpenSSL::PKey::EC.$Object]
35
+ def gen_ch_extensions(hostname)
36
+ exs = TTTLS13::Message::Extensions.new
37
+ # server_name
38
+ exs << TTTLS13::Message::Extension::ServerName.new(hostname)
39
+
40
+ # supported_versions: only TLS 1.3
41
+ exs << TTTLS13::Message::Extension::SupportedVersions.new(
42
+ msg_type: TTTLS13::Message::HandshakeType::CLIENT_HELLO
43
+ )
44
+
45
+ # signature_algorithms
46
+ exs << TTTLS13::Message::Extension::SignatureAlgorithms.new(
47
+ [
48
+ TTTLS13::SignatureScheme::ECDSA_SECP256R1_SHA256,
49
+ TTTLS13::SignatureScheme::ECDSA_SECP384R1_SHA384,
50
+ TTTLS13::SignatureScheme::ECDSA_SECP521R1_SHA512,
51
+ TTTLS13::SignatureScheme::RSA_PSS_RSAE_SHA256,
52
+ TTTLS13::SignatureScheme::RSA_PSS_RSAE_SHA384,
53
+ TTTLS13::SignatureScheme::RSA_PSS_RSAE_SHA512,
54
+ TTTLS13::SignatureScheme::RSA_PKCS1_SHA256,
55
+ TTTLS13::SignatureScheme::RSA_PKCS1_SHA384,
56
+ TTTLS13::SignatureScheme::RSA_PKCS1_SHA512
57
+ ]
58
+ )
59
+
60
+ # supported_groups
61
+ groups = [
62
+ TTTLS13::NamedGroup::SECP256R1,
63
+ TTTLS13::NamedGroup::SECP384R1,
64
+ TTTLS13::NamedGroup::SECP521R1
65
+ ]
66
+ exs << TTTLS13::Message::Extension::SupportedGroups.new(groups)
67
+
68
+ # key_share
69
+ key_share, priv_keys = TTTLS13::Message::Extension::KeyShare.gen_ch_key_share(
70
+ groups
71
+ )
72
+ exs << key_share
73
+
74
+ [exs, priv_keys]
75
+ end
76
+
77
+ # @param ch1 [TTTLS13::Message::ClientHello]
78
+ # @param hrr [TTTLS13::Message::ServerHello]
79
+ #
80
+ # @return [TTTLS13::Message::Extensions]
81
+ def gen_newch_extensions(ch1, hrr)
82
+ exs = TTTLS13::Message::Extensions.new
83
+ # key_share
84
+ if hrr.extensions.include?(TTTLS13::Message::ExtensionType::KEY_SHARE)
85
+ group = hrr.extensions[TTTLS13::Message::ExtensionType::KEY_SHARE]
86
+ .key_share_entry.first.group
87
+ key_share, = TTTLS13::Message::Extension::KeyShare.gen_ch_key_share([group])
88
+ exs << key_share
89
+ end
90
+
91
+ # cookie
92
+ exs << hrr.extensions[TTTLS13::Message::ExtensionType::COOKIE] \
93
+ if hrr.extensions.include?(TTTLS13::Message::ExtensionType::COOKIE)
94
+
95
+ ch1.extensions.merge(exs)
96
+ end
97
+
98
+ # @param conf [ECHConfig::ECHConfigContents::HpkeKeyConfig]
99
+ #
100
+ # @return [Boolean]
101
+ def select_ech_hpke_cipher_suite(conf)
102
+ TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES.find do |cs|
103
+ conf.cipher_suites.include?(cs)
104
+ end
105
+ end
106
+
107
+ # @param socket [TCPSocket]
108
+ # @param hostname [String]
109
+ # @param ech_config [ECHConfig]
110
+ # @param stack [EchSpec::Log::MessageStack]
111
+ #
112
+ # @raise [EchSpec::Error::BeforeTargetSituationError]
113
+ #
114
+ # @return [EchSpec::TLS13Client::Connection]
115
+ # @return [TTTLS13::Message::ClientHello] ClientHelloInner
116
+ # @return [TTTLS13::Message::ClientHello]
117
+ # @return [TTTLS13::Message::ServerHello] HelloRetryRequest
118
+ # @return [TTTLS13::EchState]
119
+ # rubocop: disable Metrics/MethodLength
120
+ def recv_hrr(socket, hostname, ech_config, stack)
121
+ # send 1st ClientHello
122
+ conn = TLS13Client::Connection.new(socket, :client)
123
+ inner_ech = TTTLS13::Message::Extension::ECHClientHello.new_inner
124
+ exs, = TLS13Client.gen_ch_extensions(hostname)
125
+ # for HRR
126
+ key_share = TTTLS13::Message::Extension::KeyShare.new(
127
+ msg_type: TTTLS13::Message::HandshakeType::CLIENT_HELLO,
128
+ key_share_entry: [] # empty client_shares vector
129
+ )
130
+ exs[TTTLS13::Message::ExtensionType::KEY_SHARE] = key_share
131
+ inner = TTTLS13::Message::ClientHello.new(
132
+ cipher_suites: TTTLS13::CipherSuites.new(
133
+ [
134
+ TTTLS13::CipherSuite::TLS_AES_256_GCM_SHA384,
135
+ TTTLS13::CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
136
+ TTTLS13::CipherSuite::TLS_AES_128_GCM_SHA256
137
+ ]
138
+ ),
139
+ extensions: exs.merge(
140
+ TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech
141
+ )
142
+ )
143
+ stack << inner
144
+
145
+ selector = proc { |x| TLS13Client.select_ech_hpke_cipher_suite(x) }
146
+ ch, _inner, ech_state = TTTLS13::Ech.offer_ech(inner, ech_config, selector)
147
+ conn.send_record(
148
+ TTTLS13::Message::Record.new(
149
+ type: TTTLS13::Message::ContentType::HANDSHAKE,
150
+ messages: [ch],
151
+ cipher: TTTLS13::Cryptograph::Passer.new
152
+ )
153
+ )
154
+ stack << ch
155
+
156
+ # receive HelloRetryRequest
157
+ recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
158
+ stack << recv
159
+ raise Error::BeforeTargetSituationError, 'did not send expected handshake message: HelloRetryRequest' \
160
+ unless recv.is_a?(TTTLS13::Message::ServerHello) && recv.hrr?
161
+
162
+ [conn, inner, ch, recv, ech_state]
163
+ end
164
+ # rubocop: enable Metrics/MethodLength
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,21 @@
1
+ module EchSpec
2
+ module Refinements
3
+ refine String do
4
+ def colorize(code)
5
+ "\e[#{code}m#{self}\e[0m"
6
+ end
7
+
8
+ def red
9
+ colorize(31)
10
+ end
11
+
12
+ def green
13
+ colorize(32)
14
+ end
15
+
16
+ def yellow
17
+ colorize(33)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module EchSpec
2
+ VERSION = '0.0.1'.freeze
3
+ end
data/lib/echspec.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'base64'
2
+ require 'optparse'
3
+ require 'resolv'
4
+ require 'timeout'
5
+ require 'tttls1.3'
6
+
7
+ require 'echspec/version'
8
+ require 'echspec/utils'
9
+ require 'echspec/log'
10
+ require 'echspec/error'
11
+ require 'echspec/result'
12
+ require 'echspec/tls13_client'
13
+ require 'echspec/spec_case'
14
+ require 'echspec/spec_group'
15
+ require 'echspec/spec'
16
+ require 'echspec/cli'
data/spec/9_spec.rb ADDED
@@ -0,0 +1,13 @@
1
+ require_relative 'spec_helper'
2
+
3
+ RSpec.describe EchSpec::Spec::Spec9 do
4
+ context 'parse_pem' do
5
+ let(:pem) do
6
+ File.open("#{__dir__}/../fixtures/echconfigs.pem").read
7
+ end
8
+
9
+ it 'could parse' do
10
+ expect(EchSpec::Spec::Spec9.send(:parse_pem, pem)).to be_a EchSpec::Ok
11
+ end
12
+ end
13
+ end
data/spec/log_spec.rb ADDED
@@ -0,0 +1,58 @@
1
+ require_relative 'spec_helper'
2
+
3
+ RSpec.describe EchSpec::Log::MessageStack do
4
+ context 'obj2json' do
5
+ let(:crt) do
6
+ File.open("#{__dir__}/../fixtures/server.crt").read
7
+ end
8
+
9
+ it 'should convert' do
10
+ expect(EchSpec::Log::MessageStack.obj2json(OpenSSL::X509::Certificate.new(crt)))
11
+ .to eq "#{crt.split("\n").join('\n')}\\n"
12
+ end
13
+
14
+ it 'should convert' do
15
+ expect(EchSpec::Log::MessageStack.obj2json(1)).to eq '1'
16
+ end
17
+
18
+ it 'should convert' do
19
+ expect(EchSpec::Log::MessageStack.obj2json(0.1)).to eq '0.1'
20
+ end
21
+
22
+ it 'should convert' do
23
+ expect(EchSpec::Log::MessageStack.obj2json(true)).to eq 'true'
24
+ end
25
+
26
+ it 'should convert' do
27
+ expect(EchSpec::Log::MessageStack.obj2json(false)).to eq 'false'
28
+ end
29
+
30
+ it 'should convert' do
31
+ expect(EchSpec::Log::MessageStack.obj2json('')).to eq '""'
32
+ end
33
+
34
+ it 'should convert' do
35
+ expect(EchSpec::Log::MessageStack.obj2json('string')).to eq '"0x737472696e67"'
36
+ end
37
+
38
+ it 'should convert' do
39
+ expect(EchSpec::Log::MessageStack.obj2json(nil)).to eq 'null'
40
+ end
41
+
42
+ it 'should convert' do
43
+ expect(EchSpec::Log::MessageStack.obj2json([1, true, '', 'string', nil])).to eq '[1,true,"","0x737472696e67",null]'
44
+ end
45
+
46
+ it 'should convert' do
47
+ expect(EchSpec::Log::MessageStack.obj2json(1 => true, '' => 'string', nil => [])).to eq '{1:true,"":"0x737472696e67",null:[]}'
48
+ end
49
+
50
+ it 'should convert' do
51
+ expect(EchSpec::Log::MessageStack.obj2json(C.new('string'))).to eq '{"name":"0x737472696e67"}'
52
+ end
53
+
54
+ it 'should convert' do
55
+ expect(EchSpec::Log::MessageStack.obj2json(D.new)).to eq '"$D"'
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,12 @@
1
+ RSpec.configure(&:disable_monkey_patching!)
2
+
3
+ require 'echspec'
4
+
5
+ class C
6
+ def initialize(name)
7
+ @name = name
8
+ end
9
+ end
10
+
11
+ class D
12
+ end
@@ -0,0 +1,77 @@
1
+ require_relative 'spec_helper'
2
+
3
+ module EchSpec
4
+ module Spec
5
+ class SpecX < WithSocket
6
+ def validate(hostname, port)
7
+ with_socket(hostname, port) do |_socket|
8
+ # not return
9
+ end
10
+ end
11
+ end
12
+
13
+ class SpecY < WithSocket
14
+ def validate(hostname, port)
15
+ with_socket(hostname, port) do |_socket|
16
+ return EchSpec::Ok.new(1)
17
+ end
18
+ end
19
+ end
20
+
21
+ class SpecZ < WithSocket
22
+ def validate(hostname, port)
23
+ with_socket(hostname, port) do |_socket|
24
+ msg = TTTLS13::Message::Alert.new(
25
+ level: TTTLS13::Message::AlertLevel::FATAL,
26
+ description: "\x0a"
27
+ )
28
+ return EchSpec::Err.new('details', [msg])
29
+ end
30
+ end
31
+ end
32
+
33
+ class SpecW < WithSocket
34
+ def validate(hostname, port)
35
+ with_socket(hostname, port) do |_socket|
36
+ raise EchSpec::Error::BeforeTargetSituationError, 'not received ClientHello'
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ RSpec.describe EchSpec::Spec::WithSocket do
44
+ context 'with_socket' do
45
+ before do
46
+ socket = StringIO.new
47
+ allow(TCPSocket).to receive(:new).and_return(socket)
48
+ end
49
+
50
+ it 'should return nil' do
51
+ result = EchSpec::Spec::SpecX.new.validate('localhost', 4433)
52
+ expect(result).to eq nil
53
+ end
54
+
55
+ it 'should return Ok(1)' do
56
+ result = EchSpec::Spec::SpecY.new.validate('localhost', 4433)
57
+ expect(result).to be_a EchSpec::Ok
58
+ expect(result.obj).to eq 1
59
+ end
60
+
61
+ it 'should return Err(details, message_stack)' do
62
+ result = EchSpec::Spec::SpecZ.new.validate('localhost', 4433)
63
+ expect(result).to be_a EchSpec::Err
64
+ expect(result.details).to eq 'details'
65
+ expect(result.message_stack.length).to be 1
66
+ expect(result.message_stack.first.level).to be TTTLS13::Message::AlertLevel::FATAL
67
+ expect(result.message_stack.first.description).to eq "\x0a"
68
+ end
69
+
70
+ it 'should return Err(details, message_stack), raised BeforeTargetSituationError' do
71
+ result = EchSpec::Spec::SpecW.new.validate('localhost', 4433)
72
+ expect(result).to be_a EchSpec::Err
73
+ expect(result.details).to eq 'not received ClientHello'
74
+ expect(result.message_stack).to eq '{}'
75
+ end
76
+ end
77
+ end