echspec 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|