echspec 0.0.1

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