echspec 0.0.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.
@@ -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