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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +36 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +272 -0
- data/Rakefile +8 -0
- data/docs/echspec-demo.png +0 -0
- data/echspec.gemspec +26 -0
- data/exe/echspec +7 -0
- data/fixtures/echconfigs.pem +7 -0
- data/fixtures/server.crt +20 -0
- data/fixtures/server.key +27 -0
- data/lib/echspec/cli.rb +97 -0
- data/lib/echspec/error.rb +9 -0
- data/lib/echspec/log.rb +85 -0
- data/lib/echspec/result.rb +26 -0
- data/lib/echspec/spec/5.1-10.rb +222 -0
- data/lib/echspec/spec/5.1-9.rb +108 -0
- data/lib/echspec/spec/7-5.rb +242 -0
- data/lib/echspec/spec/7.1-11.rb +93 -0
- data/lib/echspec/spec/7.1-14.2.1.rb +133 -0
- data/lib/echspec/spec/7.1.1-2.rb +142 -0
- data/lib/echspec/spec/7.1.1-5.rb +83 -0
- data/lib/echspec/spec/9.rb +113 -0
- data/lib/echspec/spec.rb +170 -0
- data/lib/echspec/spec_case.rb +10 -0
- data/lib/echspec/spec_group.rb +10 -0
- data/lib/echspec/tls13_client.rb +167 -0
- data/lib/echspec/utils.rb +21 -0
- data/lib/echspec/version.rb +3 -0
- data/lib/echspec.rb +16 -0
- data/spec/9_spec.rb +13 -0
- data/spec/log_spec.rb +58 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/with_socket_spec.rb +77 -0
- metadata +141 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
module EchSpec
|
2
|
+
module Spec
|
3
|
+
class Spec7_1_11 < WithSocket
|
4
|
+
# Upon determining the ClientHelloInner, the client-facing server
|
5
|
+
# checks that the message includes a well-formed
|
6
|
+
# "encrypted_client_hello" extension of type inner and that it does not
|
7
|
+
# offer TLS 1.2 or below. If either of these checks fails, the client-
|
8
|
+
# facing server MUST abort with an "illegal_parameter" alert.
|
9
|
+
#
|
10
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7.1-11
|
11
|
+
|
12
|
+
# @return [SpecGroup]
|
13
|
+
def self.spec_group
|
14
|
+
SpecGroup.new(
|
15
|
+
'7.1-11',
|
16
|
+
[
|
17
|
+
SpecCase.new(
|
18
|
+
'MUST abort with an "illegal_parameter" alert, if ClientHelloInner offers TLS 1.2 or below.',
|
19
|
+
method(:validate_ech_with_tls12)
|
20
|
+
)
|
21
|
+
]
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param hostname [String]
|
26
|
+
# @param port [Integer]
|
27
|
+
# @param ech_config [ECHConfig]
|
28
|
+
#
|
29
|
+
# @return [EchSpec::Ok | Err]
|
30
|
+
def self.validate_ech_with_tls12(hostname, port, ech_config)
|
31
|
+
Spec7_1_11.new.do_validate_ech_with_tls12(hostname, port, ech_config)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param hostname [String]
|
35
|
+
# @param port [Integer]
|
36
|
+
# @param ech_config [ECHConfig]
|
37
|
+
#
|
38
|
+
# @return [EchSpec::Ok | Err]
|
39
|
+
def do_validate_ech_with_tls12(hostname, port, ech_config)
|
40
|
+
with_socket(hostname, port) do |socket|
|
41
|
+
recv = send_ch_ech_with_tls12(socket, hostname, ech_config)
|
42
|
+
return Err.new('did not send expected alert: illegal_parameter', message_stack) \
|
43
|
+
unless Spec.expect_alert(recv, :illegal_parameter)
|
44
|
+
|
45
|
+
Ok.new(nil)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# rubocop: disable Metrics/MethodLength
|
50
|
+
def send_ch_ech_with_tls12(socket, hostname, ech_config)
|
51
|
+
conn = TLS13Client::Connection.new(socket, :client)
|
52
|
+
inner_ech = TTTLS13::Message::Extension::ECHClientHello.new_inner
|
53
|
+
exs, = TLS13Client.gen_ch_extensions(hostname)
|
54
|
+
# supported_versions: only TLS 1.2
|
55
|
+
versions = TTTLS13::Message::Extension::SupportedVersions.new(
|
56
|
+
msg_type: TTTLS13::Message::HandshakeType::CLIENT_HELLO,
|
57
|
+
versions: [TTTLS13::Message::ProtocolVersion::TLS_1_2]
|
58
|
+
)
|
59
|
+
exs[TTTLS13::Message::ExtensionType::SUPPORTED_VERSIONS] = versions
|
60
|
+
inner = TTTLS13::Message::ClientHello.new(
|
61
|
+
cipher_suites: TTTLS13::CipherSuites.new(
|
62
|
+
[
|
63
|
+
TTTLS13::CipherSuite::TLS_AES_256_GCM_SHA384,
|
64
|
+
TTTLS13::CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
|
65
|
+
TTTLS13::CipherSuite::TLS_AES_128_GCM_SHA256
|
66
|
+
]
|
67
|
+
),
|
68
|
+
extensions: exs.merge(
|
69
|
+
TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech
|
70
|
+
)
|
71
|
+
)
|
72
|
+
|
73
|
+
selector = proc { |x| TLS13Client.select_ech_hpke_cipher_suite(x) }
|
74
|
+
ch, inner, = TTTLS13::Ech.offer_ech(inner, ech_config, selector)
|
75
|
+
conn.send_record(
|
76
|
+
TTTLS13::Message::Record.new(
|
77
|
+
type: TTTLS13::Message::ContentType::HANDSHAKE,
|
78
|
+
messages: [ch],
|
79
|
+
cipher: TTTLS13::Cryptograph::Passer.new
|
80
|
+
)
|
81
|
+
)
|
82
|
+
@stack << inner
|
83
|
+
@stack << ch
|
84
|
+
|
85
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
|
86
|
+
@stack << recv
|
87
|
+
|
88
|
+
recv
|
89
|
+
end
|
90
|
+
# rubocop: enable Metrics/MethodLength
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module EchSpec
|
2
|
+
module Spec
|
3
|
+
class Spec7_1_14_2_1 < WithSocket
|
4
|
+
# Otherwise, if all candidate ECHConfig values fail to decrypt the
|
5
|
+
# extension, the client-facing server MUST ignore the extension and
|
6
|
+
# proceed with the connection using ClientHelloOuter, with the
|
7
|
+
# following modifications:
|
8
|
+
#
|
9
|
+
# * If the server is configured with any ECHConfigs, it MUST include
|
10
|
+
# the "encrypted_client_hello" extension in its EncryptedExtensions
|
11
|
+
# with the "retry_configs" field set to one or more ECHConfig
|
12
|
+
# structures with up-to-date keys. Servers MAY supply multiple
|
13
|
+
# ECHConfig values of different versions. This allows a server to
|
14
|
+
# support multiple versions at once.
|
15
|
+
#
|
16
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7.1-14.2.1
|
17
|
+
|
18
|
+
# @return [EchSpec::SpecGroup]
|
19
|
+
def self.spec_group
|
20
|
+
SpecGroup.new(
|
21
|
+
'7.1-14.2.1',
|
22
|
+
[
|
23
|
+
SpecCase.new(
|
24
|
+
'MUST include the "encrypted_client_hello" extension in its EncryptedExtensions with the "retry_configs" field set to one or more ECHConfig.',
|
25
|
+
method(:validate_ee_retry_configs)
|
26
|
+
)
|
27
|
+
]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param hostname [String]
|
32
|
+
# @param port [Integer]
|
33
|
+
# @param _ [ECHConfig]
|
34
|
+
#
|
35
|
+
# @return [EchSpec::Ok | Err]
|
36
|
+
def self.validate_ee_retry_configs(hostname, port, _)
|
37
|
+
Spec7_1_14_2_1.new.do_validate_ee_retry_configs(hostname, port)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param hostname [String]
|
41
|
+
# @param port [Integer]
|
42
|
+
#
|
43
|
+
# @return [EchSpec::Ok | Err]
|
44
|
+
def do_validate_ee_retry_configs(hostname, port)
|
45
|
+
with_socket(hostname, port) do |socket|
|
46
|
+
recv = send_ch_with_greased_ech(socket, hostname)
|
47
|
+
ex = recv.extensions[TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
|
48
|
+
return Err.new('did not send expected alert: encrypted_client_hello', message_stack) \
|
49
|
+
unless ex.is_a?(TTTLS13::Message::Extension::ECHEncryptedExtensions)
|
50
|
+
return Err.new('ECHConfigs did not have "retry_configs"', message_stack) \
|
51
|
+
if ex.retry_configs.nil? || ex.retry_configs.empty?
|
52
|
+
|
53
|
+
Ok.new(nil)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# rubocop: disable Metrics/AbcSize
|
58
|
+
# rubocop: disable Metrics/MethodLength
|
59
|
+
def send_ch_with_greased_ech(socket, hostname)
|
60
|
+
# send ClientHello
|
61
|
+
conn = TLS13Client::Connection.new(socket, :client)
|
62
|
+
inner_ech = TTTLS13::Message::Extension::ECHClientHello.new_inner
|
63
|
+
exs, priv_keys = TLS13Client.gen_ch_extensions(hostname)
|
64
|
+
inner = TTTLS13::Message::ClientHello.new(
|
65
|
+
cipher_suites: TTTLS13::CipherSuites.new(
|
66
|
+
[
|
67
|
+
TTTLS13::CipherSuite::TLS_AES_256_GCM_SHA384,
|
68
|
+
TTTLS13::CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
|
69
|
+
TTTLS13::CipherSuite::TLS_AES_128_GCM_SHA256
|
70
|
+
]
|
71
|
+
),
|
72
|
+
extensions: exs.merge(
|
73
|
+
TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech
|
74
|
+
)
|
75
|
+
)
|
76
|
+
@stack << inner
|
77
|
+
|
78
|
+
ch = TTTLS13::Ech.new_greased_ch(inner, TTTLS13::Ech.new_grease_ech)
|
79
|
+
conn.send_record(
|
80
|
+
TTTLS13::Message::Record.new(
|
81
|
+
type: TTTLS13::Message::ContentType::HANDSHAKE,
|
82
|
+
messages: [ch],
|
83
|
+
cipher: TTTLS13::Cryptograph::Passer.new
|
84
|
+
)
|
85
|
+
)
|
86
|
+
@stack << ch
|
87
|
+
|
88
|
+
# receive ServerHello
|
89
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
|
90
|
+
@stack << recv
|
91
|
+
raise Error::BeforeTargetSituationError, 'not received ServerHello' \
|
92
|
+
unless recv.is_a?(TTTLS13::Message::ServerHello) && !recv.hrr?
|
93
|
+
|
94
|
+
# receive EncryptedExtensions
|
95
|
+
transcript = TTTLS13::Transcript.new
|
96
|
+
transcript[TTTLS13::CH] = [ch, ch.serialize]
|
97
|
+
sh = recv
|
98
|
+
transcript[TTTLS13::SH] = [sh, sh.serialize]
|
99
|
+
kse = sh.extensions[TTTLS13::Message::ExtensionType::KEY_SHARE]
|
100
|
+
.key_share_entry.first
|
101
|
+
shared_secret = TTTLS13::Endpoint.gen_shared_secret(
|
102
|
+
kse.key_exchange,
|
103
|
+
priv_keys[kse.group],
|
104
|
+
kse.group
|
105
|
+
)
|
106
|
+
key_schedule = TTTLS13::KeySchedule.new(
|
107
|
+
psk: nil,
|
108
|
+
shared_secret:,
|
109
|
+
cipher_suite: sh.cipher_suite,
|
110
|
+
transcript:
|
111
|
+
)
|
112
|
+
hs_rcipher = TTTLS13::Endpoint.gen_cipher(
|
113
|
+
sh.cipher_suite,
|
114
|
+
key_schedule.server_handshake_write_key,
|
115
|
+
key_schedule.server_handshake_write_iv
|
116
|
+
)
|
117
|
+
recv, = conn.recv_message(hs_rcipher)
|
118
|
+
@stack << recv
|
119
|
+
if recv.is_a?(TTTLS13::Message::ChangeCipherSpec)
|
120
|
+
recv, = conn.recv_message(hs_rcipher)
|
121
|
+
@stack << recv
|
122
|
+
end
|
123
|
+
|
124
|
+
raise Error::BeforeTargetSituationError, 'not received EncryptedExtensions' \
|
125
|
+
unless recv.is_a?(TTTLS13::Message::EncryptedExtensions)
|
126
|
+
|
127
|
+
recv
|
128
|
+
end
|
129
|
+
# rubocop: enable Metrics/AbcSize
|
130
|
+
# rubocop: enable Metrics/MethodLength
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module EchSpec
|
2
|
+
module Spec
|
3
|
+
class Spec7_1_1_2 < WithSocket
|
4
|
+
# If the client-facing server accepted ECH, it checks the second
|
5
|
+
# ClientHelloOuter also contains the "encrypted_client_hello"
|
6
|
+
# extension. If not, it MUST abort the handshake with a
|
7
|
+
# "missing_extension" alert. Otherwise, it checks that
|
8
|
+
# ECHClientHello.cipher_suite and ECHClientHello.config_id are
|
9
|
+
# unchanged, and that ECHClientHello.enc is empty. If not, it MUST
|
10
|
+
# abort the handshake with an "illegal_parameter" alert.
|
11
|
+
#
|
12
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7.1.1-2
|
13
|
+
|
14
|
+
# @return [EchSpec::SpecGroup]
|
15
|
+
def self.spec_group
|
16
|
+
SpecGroup.new(
|
17
|
+
'7.1.1-2',
|
18
|
+
[
|
19
|
+
SpecCase.new(
|
20
|
+
'MUST abort with a "missing_extension" alert, if 2nd ClientHelloOuter does not contains the "encrypted_client_hello" extension.',
|
21
|
+
method(:validate_2nd_ch_missing_ech)
|
22
|
+
),
|
23
|
+
SpecCase.new(
|
24
|
+
'MUST abort with an "illegal_parameter" alert, if 2nd ClientHelloOuter "encrypted_client_hello" enc is empty.',
|
25
|
+
method(:validate_2nd_ch_unchanged_ech)
|
26
|
+
)
|
27
|
+
]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param hostname [String]
|
32
|
+
# @param port [Integer]
|
33
|
+
# @param ech_config [ECHConfig]
|
34
|
+
#
|
35
|
+
# @return [EchSpec::Ok | Err]
|
36
|
+
def self.validate_2nd_ch_missing_ech(hostname, port, ech_config)
|
37
|
+
Spec7_1_1_2.new.do_validate_2nd_ch_missing_ech(hostname, port, ech_config)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param hostname [String]
|
41
|
+
# @param port [Integer]
|
42
|
+
# @param ech_config [ECHConfig]
|
43
|
+
#
|
44
|
+
# @return [EchSpec::Ok | Err]
|
45
|
+
def do_validate_2nd_ch_missing_ech(hostname, port, ech_config)
|
46
|
+
with_socket(hostname, port) do |socket|
|
47
|
+
recv = send_2nd_ch_missing_ech(socket, hostname, ech_config)
|
48
|
+
return Err.new('did not send expected alert: missing_extension', message_stack) \
|
49
|
+
unless Spec.expect_alert(recv, :missing_extension)
|
50
|
+
|
51
|
+
Ok.new(nil)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param hostname [String]
|
56
|
+
# @param port [Integer]
|
57
|
+
# @param ech_config [ECHConfig]
|
58
|
+
#
|
59
|
+
# @return [EchSpec::Ok | Err]
|
60
|
+
def self.validate_2nd_ch_unchanged_ech(hostname, port, ech_config)
|
61
|
+
Spec7_1_1_2.new.do_validate_2nd_ch_unchanged_ech(hostname, port, ech_config)
|
62
|
+
end
|
63
|
+
|
64
|
+
# @param hostname [String]
|
65
|
+
# @param port [Integer]
|
66
|
+
# @param ech_config [ECHConfig]
|
67
|
+
#
|
68
|
+
# @return [EchSpec::Ok | Err]
|
69
|
+
def do_validate_2nd_ch_unchanged_ech(hostname, port, ech_config)
|
70
|
+
with_socket(hostname, port) do |socket|
|
71
|
+
recv = send_2nd_ch_unchanged_ech(socket, hostname, ech_config)
|
72
|
+
return Err.new('did not send expected alert: illegal_parameter', message_stack) \
|
73
|
+
unless Spec.expect_alert(recv, :illegal_parameter)
|
74
|
+
|
75
|
+
Ok.new(nil)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def send_2nd_ch_missing_ech(socket, hostname, ech_config)
|
80
|
+
conn, _inner1, ch1, hrr, = TLS13Client.recv_hrr(socket, hostname, ech_config, @stack)
|
81
|
+
# send 2nd ClientHello without ech
|
82
|
+
new_exs = TLS13Client.gen_newch_extensions(ch1, hrr)
|
83
|
+
new_exs.delete(TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO)
|
84
|
+
ch = TTTLS13::Message::ClientHello.new(
|
85
|
+
legacy_version: ch1.legacy_version,
|
86
|
+
random: ch1.random,
|
87
|
+
legacy_session_id: ch1.legacy_session_id,
|
88
|
+
cipher_suites: ch1.cipher_suites,
|
89
|
+
legacy_compression_methods: ch1.legacy_compression_methods,
|
90
|
+
extensions: new_exs
|
91
|
+
)
|
92
|
+
conn.send_record(
|
93
|
+
TTTLS13::Message::Record.new(
|
94
|
+
type: TTTLS13::Message::ContentType::HANDSHAKE,
|
95
|
+
messages: [ch],
|
96
|
+
cipher: TTTLS13::Cryptograph::Passer.new
|
97
|
+
)
|
98
|
+
)
|
99
|
+
@stack << ch
|
100
|
+
|
101
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
|
102
|
+
@stack << recv
|
103
|
+
|
104
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new) \
|
105
|
+
if recv.is_a?(TTTLS13::Message::ChangeCipherSpec)
|
106
|
+
recv
|
107
|
+
end
|
108
|
+
|
109
|
+
def send_2nd_ch_unchanged_ech(socket, hostname, ech_config)
|
110
|
+
conn, inner1, ch1, hrr, = TLS13Client.recv_hrr(socket, hostname, ech_config, @stack)
|
111
|
+
# send 2nd ClientHello with unchanged ech
|
112
|
+
new_exs = TLS13Client.gen_newch_extensions(ch1, hrr)
|
113
|
+
new_exs[TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO] =
|
114
|
+
ch1.extensions[TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
|
115
|
+
ch = TTTLS13::Message::ClientHello.new(
|
116
|
+
legacy_version: ch1.legacy_version,
|
117
|
+
random: ch1.random,
|
118
|
+
legacy_session_id: ch1.legacy_session_id,
|
119
|
+
cipher_suites: ch1.cipher_suites,
|
120
|
+
legacy_compression_methods: ch1.legacy_compression_methods,
|
121
|
+
extensions: new_exs
|
122
|
+
)
|
123
|
+
conn.send_record(
|
124
|
+
TTTLS13::Message::Record.new(
|
125
|
+
type: TTTLS13::Message::ContentType::HANDSHAKE,
|
126
|
+
messages: [ch],
|
127
|
+
cipher: TTTLS13::Cryptograph::Passer.new
|
128
|
+
)
|
129
|
+
)
|
130
|
+
@stack << inner1
|
131
|
+
@stack << ch
|
132
|
+
|
133
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
|
134
|
+
@stack << recv
|
135
|
+
|
136
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new) \
|
137
|
+
if recv.is_a?(TTTLS13::Message::ChangeCipherSpec)
|
138
|
+
recv
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module EchSpec
|
2
|
+
module Spec
|
3
|
+
class Spec7_1_1_5 < WithSocket
|
4
|
+
# ClientHelloOuterAAD is computed as described in Section 5.2, but
|
5
|
+
# using the second ClientHelloOuter. If decryption fails, the client-
|
6
|
+
# facing server MUST abort the handshake with a "decrypt_error" alert.
|
7
|
+
# Otherwise, it reconstructs the second ClientHelloInner from the new
|
8
|
+
# EncodedClientHelloInner as described in Section 5.1, using the
|
9
|
+
# second ClientHelloOuter for any referenced extensions.
|
10
|
+
#
|
11
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7.1.1-5
|
12
|
+
|
13
|
+
# @return [EchSpec::SpecGroup]
|
14
|
+
def self.spec_group
|
15
|
+
SpecGroup.new(
|
16
|
+
'7.1.1-5',
|
17
|
+
[
|
18
|
+
SpecCase.new(
|
19
|
+
'MUST abort with a "decrypt_error" alert, if fails to decrypt 2nd ClientHelloOuter.',
|
20
|
+
method(:validate_undecryptable_2nd_ch_outer)
|
21
|
+
)
|
22
|
+
]
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param hostname [String]
|
27
|
+
# @param port [Integer]
|
28
|
+
# @param ech_config [ECHConfig]
|
29
|
+
#
|
30
|
+
# @return [EchSpec::Ok | Err]
|
31
|
+
def self.validate_undecryptable_2nd_ch_outer(hostname, port, ech_config)
|
32
|
+
Spec7_1_1_5.new.do_validate_undecryptable_2nd_ch_outer(hostname, port, ech_config)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param hostname [String]
|
36
|
+
# @param port [Integer]
|
37
|
+
# @param ech_config [ECHConfig]
|
38
|
+
#
|
39
|
+
# @return [EchSpec::Ok | Err]
|
40
|
+
def do_validate_undecryptable_2nd_ch_outer(hostname, port, ech_config)
|
41
|
+
with_socket(hostname, port) do |socket|
|
42
|
+
recv = send_2nd_ch_with_undecryptable_ech(socket, hostname, ech_config)
|
43
|
+
return Err.new('did not send expected alert: decrypt_error', message_stack) \
|
44
|
+
unless Spec.expect_alert(recv, :decrypt_error)
|
45
|
+
|
46
|
+
Ok.new(nil)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def send_2nd_ch_with_undecryptable_ech(socket, hostname, ech_config)
|
51
|
+
conn, _inner1, ch1, hrr, ech_state = TLS13Client.recv_hrr(socket, hostname, ech_config, @stack)
|
52
|
+
# send 2nd ClientHello with undecryptable ech
|
53
|
+
new_exs = TLS13Client.gen_newch_extensions(ch1, hrr)
|
54
|
+
inner = TTTLS13::Message::ClientHello.new(
|
55
|
+
legacy_version: ch1.legacy_version,
|
56
|
+
random: ch1.random,
|
57
|
+
legacy_session_id: ch1.legacy_session_id,
|
58
|
+
cipher_suites: ch1.cipher_suites,
|
59
|
+
legacy_compression_methods: ch1.legacy_compression_methods,
|
60
|
+
extensions: new_exs
|
61
|
+
)
|
62
|
+
ech_state.ctx.increment_seq # invalidly increment of the sequence number
|
63
|
+
ch, inner = TTTLS13::Ech.offer_new_ech(inner, ech_state)
|
64
|
+
conn.send_record(
|
65
|
+
TTTLS13::Message::Record.new(
|
66
|
+
type: TTTLS13::Message::ContentType::HANDSHAKE,
|
67
|
+
messages: [ch],
|
68
|
+
cipher: TTTLS13::Cryptograph::Passer.new
|
69
|
+
)
|
70
|
+
)
|
71
|
+
@stack << inner
|
72
|
+
@stack << ch
|
73
|
+
|
74
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
|
75
|
+
@stack << recv
|
76
|
+
|
77
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new) \
|
78
|
+
if recv.is_a?(TTTLS13::Message::ChangeCipherSpec)
|
79
|
+
recv
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module EchSpec
|
2
|
+
module Spec
|
3
|
+
class Spec9
|
4
|
+
# In the absence of an application profile standard specifying
|
5
|
+
# otherwise, a compliant ECH application MUST implement the following
|
6
|
+
# HPKE cipher suite:
|
7
|
+
#
|
8
|
+
# * KEM: DHKEM(X25519, HKDF-SHA256) (see Section 7.1 of [HPKE])
|
9
|
+
# * KDF: HKDF-SHA256 (see Section 7.2 of [HPKE])
|
10
|
+
# * AEAD: AES-128-GCM (see Section 7.3 of [HPKE])
|
11
|
+
#
|
12
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-9
|
13
|
+
@section = '9'
|
14
|
+
@description = 'MUST implement the following HPKE cipher suite: KEM: DHKEM(X25519, HKDF-SHA256), KDF: HKDF-SHA256 and AEAD: AES-128-GCM.'
|
15
|
+
class << self
|
16
|
+
attr_reader :description, :section
|
17
|
+
|
18
|
+
# @param fpath [String | NilClass]
|
19
|
+
# @param hostname [String]
|
20
|
+
# @param force_compliant [Boolean]
|
21
|
+
#
|
22
|
+
# @return [EchSpec::Ok<ECHConfig> | Err]
|
23
|
+
def try_get_ech_config(fpath, hostname, force_compliant)
|
24
|
+
result = if fpath.nil?
|
25
|
+
resolve_ech_configs(hostname)
|
26
|
+
else
|
27
|
+
parse_pem(File.open(fpath).read)
|
28
|
+
end
|
29
|
+
|
30
|
+
ech_configs = case result
|
31
|
+
in Ok(obj)
|
32
|
+
obj
|
33
|
+
in Err
|
34
|
+
return result
|
35
|
+
end
|
36
|
+
|
37
|
+
if force_compliant
|
38
|
+
validate_compliant_ech_configs(ech_configs)
|
39
|
+
else
|
40
|
+
Ok.new(ech_configs.first)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param ech_configs [Array of ECHConfig]
|
45
|
+
#
|
46
|
+
# @return [EchSpec::Ok<ECHConfig> | Err]
|
47
|
+
def validate_compliant_ech_configs(ech_configs)
|
48
|
+
ech_config = ech_configs.find do |c|
|
49
|
+
kconfig = c.echconfig_contents.key_config
|
50
|
+
valid_kem_id = kconfig.kem_id.uint16 == 0x0020
|
51
|
+
valid_cipher_suite = kconfig.cipher_suites.any? do |cs|
|
52
|
+
cs.kdf_id.uint16 == 0x0001 && cs.aead_id.uint16 == 0x0001
|
53
|
+
end
|
54
|
+
|
55
|
+
valid_kem_id && valid_cipher_suite
|
56
|
+
end
|
57
|
+
return Ok.new(ech_config) unless ech_config.nil?
|
58
|
+
|
59
|
+
Err.new('ECHConfigs does NOT include HPKE cipher suite: KEM: DHKEM(X25519, HKDF-SHA256), KDF: HKDF-SHA256 and AEAD: AES-128-GCM.', nil)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param hostname [String]
|
63
|
+
#
|
64
|
+
# @return [EchSpec::Ok<Array of ECHConfig> | Err]
|
65
|
+
def resolve_ech_configs(hostname)
|
66
|
+
begin
|
67
|
+
rr = Resolv::DNS.new.getresource(
|
68
|
+
hostname,
|
69
|
+
Resolv::DNS::Resource::IN::HTTPS
|
70
|
+
)
|
71
|
+
rescue Resolv::ResolvError => e
|
72
|
+
return Err.new(e.message, nil)
|
73
|
+
end
|
74
|
+
|
75
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-svcb-ech-01#section-6
|
76
|
+
ech = 5
|
77
|
+
return Err.new("HTTPS resource record for #{hostname} does NOT have ech SvcParams.", nil) if rr.params[ech].nil?
|
78
|
+
|
79
|
+
octet = rr.params[ech].value
|
80
|
+
Err.new('Failed to parse ECHConfig on HTTPS resource record.', nil) \
|
81
|
+
unless octet.length == octet.slice(0, 2).unpack1('n') + 2
|
82
|
+
|
83
|
+
Ok.new(ECHConfig.decode_vectors(octet.slice(2..)))
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param pem [String]
|
87
|
+
#
|
88
|
+
# @return [EchSpec::Ok<Array of ECHConfig> | Err]
|
89
|
+
def parse_pem(pem)
|
90
|
+
s = pem.scan(/-----BEGIN ECHCONFIG-----(.*)-----END ECHCONFIG-----/m)
|
91
|
+
.first
|
92
|
+
.first
|
93
|
+
.gsub("\n", '')
|
94
|
+
b = Base64.decode64(s)
|
95
|
+
ech_configs = ECHConfig.decode_vectors(b.slice(2..))
|
96
|
+
Ok.new(ech_configs)
|
97
|
+
rescue StandardError
|
98
|
+
# https://datatracker.ietf.org/doc/html/draft-farrell-tls-pemesni-08#section-3
|
99
|
+
example = <<~PEM
|
100
|
+
-----BEGIN PRIVATE KEY-----
|
101
|
+
MC4CAQAwBQYDK2VuBCIEICjd4yGRdsoP9gU7YT7My8DHx1Tjme8GYDXrOMCi8v1V
|
102
|
+
-----END PRIVATE KEY-----
|
103
|
+
-----BEGIN ECHCONFIG-----
|
104
|
+
AD7+DQA65wAgACA8wVN2BtscOl3vQheUzHeIkVmKIiydUhDCliA4iyQRCwAEAAEA
|
105
|
+
AQALZXhhbXBsZS5jb20AAA==
|
106
|
+
-----END ECHCONFIG-----
|
107
|
+
PEM
|
108
|
+
Err.new("Failed to parse ECHConfig PEM file, expected ECHConfig PEM like following: \n\n#{example}", nil)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|