ssl2 1.0.0__py3-none-any.whl

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.
ssl2/__init__.py ADDED
@@ -0,0 +1,106 @@
1
+ from ssl2.asynchronous import AsyncTransport, StreamTransport, TLSStream
2
+ from ssl2.client_hello import ClientHello, ClientHelloBuilder, ClientHelloParser
3
+ from ssl2.credentials import (
4
+ Certificate,
5
+ Credential,
6
+ PrivateKey,
7
+ SigningChain,
8
+ TrustStore,
9
+ )
10
+ from ssl2.errors import (
11
+ ConfigurationError,
12
+ EarlyDataRejected,
13
+ RaggedEOF,
14
+ TLSError,
15
+ WantReadError,
16
+ WantWriteError,
17
+ )
18
+ from ssl2.fingerprint import (
19
+ JA3,
20
+ JA4,
21
+ AkamaiFingerprint,
22
+ Http2Priority,
23
+ Http2Setting,
24
+ JA4Transport,
25
+ JA4Version,
26
+ PseudoHeader,
27
+ )
28
+ from ssl2.interface import (
29
+ ClientContext,
30
+ ServerContext,
31
+ SessionCache,
32
+ TLSBuffer,
33
+ TLSClientConfiguration,
34
+ TLSImplementation,
35
+ TLSServerConfiguration,
36
+ TLSSocket,
37
+ Transport,
38
+ )
39
+ from ssl2.parameters import (
40
+ CertificateCompressionAlgorithm,
41
+ CipherSuite,
42
+ CodePoint,
43
+ ECPointFormat,
44
+ ExtensionType,
45
+ Http2SettingIdentifier,
46
+ NamedGroup,
47
+ NextProtocol,
48
+ ProtocolVersion,
49
+ PskKeyExchangeMode,
50
+ SignatureScheme,
51
+ TLSVersion,
52
+ )
53
+ from ssl2.proxy import Proxy, ProxyCredentials, ProxyError, ProxyScheme
54
+
55
+ __all__ = (
56
+ "JA3",
57
+ "JA4",
58
+ "AkamaiFingerprint",
59
+ "AsyncTransport",
60
+ "Certificate",
61
+ "CertificateCompressionAlgorithm",
62
+ "CipherSuite",
63
+ "ClientContext",
64
+ "ClientHello",
65
+ "ClientHelloBuilder",
66
+ "ClientHelloParser",
67
+ "CodePoint",
68
+ "ConfigurationError",
69
+ "Credential",
70
+ "ECPointFormat",
71
+ "EarlyDataRejected",
72
+ "ExtensionType",
73
+ "Http2Priority",
74
+ "Http2Setting",
75
+ "Http2SettingIdentifier",
76
+ "JA4Transport",
77
+ "JA4Version",
78
+ "NamedGroup",
79
+ "NextProtocol",
80
+ "PrivateKey",
81
+ "ProtocolVersion",
82
+ "Proxy",
83
+ "ProxyCredentials",
84
+ "ProxyError",
85
+ "ProxyScheme",
86
+ "PseudoHeader",
87
+ "PskKeyExchangeMode",
88
+ "RaggedEOF",
89
+ "ServerContext",
90
+ "SessionCache",
91
+ "SignatureScheme",
92
+ "SigningChain",
93
+ "StreamTransport",
94
+ "TLSBuffer",
95
+ "TLSClientConfiguration",
96
+ "TLSError",
97
+ "TLSImplementation",
98
+ "TLSServerConfiguration",
99
+ "TLSSocket",
100
+ "TLSStream",
101
+ "TLSVersion",
102
+ "Transport",
103
+ "TrustStore",
104
+ "WantReadError",
105
+ "WantWriteError",
106
+ )
ssl2/asynchronous.py ADDED
@@ -0,0 +1,217 @@
1
+ import asyncio
2
+ import socket
3
+ from collections.abc import Callable, Generator
4
+ from functools import partial
5
+ from typing import Final, Protocol, Self, cast, final
6
+
7
+ from ssl2.errors import RaggedEOF, WantReadError, WantWriteError
8
+ from ssl2.interface import RECEIVE_SIZE, ClientContext, ServerContext, TLSBuffer
9
+ from ssl2.parameters import CipherSuite, NextProtocol, TLSVersion
10
+ from ssl2.proxy import Proxy, ProxyError, ProxyScheme
11
+
12
+ SHUTDOWN_TIMEOUT: Final = 5.0
13
+
14
+
15
+ class AsyncTransport(Protocol):
16
+ async def read(self, limit: int, /) -> bytes: ...
17
+
18
+ async def write(self, data: bytes, /) -> None: ...
19
+
20
+ async def close(self) -> None: ...
21
+
22
+
23
+ @final
24
+ class StreamTransport:
25
+ def __init__(
26
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
27
+ ) -> None:
28
+ self._reader = reader
29
+ self._writer = writer
30
+
31
+ async def read(self, limit: int) -> bytes:
32
+ return await self._reader.read(limit)
33
+
34
+ async def write(self, data: bytes) -> None:
35
+ self._writer.write(data)
36
+ await self._writer.drain()
37
+
38
+ async def close(self) -> None:
39
+ self._writer.close()
40
+
41
+
42
+ @final
43
+ class TLSStream:
44
+ def __init__(
45
+ self,
46
+ context: ClientContext | ServerContext,
47
+ buffer: TLSBuffer,
48
+ transport: AsyncTransport,
49
+ ) -> None:
50
+ self._context = context
51
+ self._buffer = buffer
52
+ self._transport = transport
53
+
54
+ @classmethod
55
+ async def connect(
56
+ cls,
57
+ context: ClientContext,
58
+ address: tuple[str, int],
59
+ *,
60
+ proxy: Proxy | None = None,
61
+ timeout: float | None = None,
62
+ ) -> Self:
63
+ async with asyncio.timeout(timeout):
64
+ transport, leftover = await cls.tunnel(context, address, proxy)
65
+ try:
66
+ return await cls.open(
67
+ context, context.create_buffer(address[0]), transport, leftover
68
+ )
69
+ except BaseException:
70
+ await transport.close()
71
+ raise
72
+
73
+ @classmethod
74
+ async def tunnel(
75
+ cls, context: ClientContext, address: tuple[str, int], proxy: Proxy | None
76
+ ) -> tuple[AsyncTransport, bytes]:
77
+ if proxy is None:
78
+ return StreamTransport(*await asyncio.open_connection(*address)), b""
79
+ transport: AsyncTransport
80
+ if proxy.scheme is ProxyScheme.HTTPS:
81
+ transport = await cls.connect(context, (proxy.host, proxy.port))
82
+ else:
83
+ transport = StreamTransport(
84
+ *await asyncio.open_connection(proxy.host, proxy.port)
85
+ )
86
+ try:
87
+ return transport, await cls._tunnel(proxy, address, transport)
88
+ except BaseException:
89
+ await transport.close()
90
+ raise
91
+
92
+ @classmethod
93
+ async def open(
94
+ cls,
95
+ context: ClientContext | ServerContext,
96
+ buffer: TLSBuffer,
97
+ transport: AsyncTransport,
98
+ leftover: bytes = b"",
99
+ ) -> Self:
100
+ self = cls(context, buffer, transport)
101
+ if leftover:
102
+ buffer.process_incoming(leftover)
103
+ await self._run(buffer.do_handshake)
104
+ return self
105
+
106
+ @property
107
+ def context(self) -> ClientContext | ServerContext:
108
+ return self._context
109
+
110
+ @property
111
+ def buffer(self) -> TLSBuffer:
112
+ return self._buffer
113
+
114
+ async def read(self, limit: int) -> bytes:
115
+ try:
116
+ return await self._run(lambda: self._buffer.read(limit))
117
+ except RaggedEOF:
118
+ return b""
119
+
120
+ async def write(self, data: bytes) -> None:
121
+ view = memoryview(data)
122
+ while view:
123
+ written = await self._run(partial(self._buffer.write, view))
124
+ view = view[written:]
125
+
126
+ async def close(self) -> None:
127
+ try:
128
+ async with asyncio.timeout(SHUTDOWN_TIMEOUT):
129
+ await self._run(self._buffer.shutdown)
130
+ except TimeoutError, RaggedEOF, OSError:
131
+ pass
132
+ await self._transport.close()
133
+
134
+ def cipher(self) -> CipherSuite | int | None:
135
+ return self._buffer.cipher()
136
+
137
+ def negotiated_protocol(self) -> NextProtocol | bytes | None:
138
+ return self._buffer.negotiated_protocol()
139
+
140
+ @property
141
+ def negotiated_tls_version(self) -> TLSVersion | None:
142
+ return self._buffer.negotiated_tls_version
143
+
144
+ def getpeercert(self) -> bytes | None:
145
+ return self._buffer.getpeercert()
146
+
147
+ @classmethod
148
+ async def associate(
149
+ cls, proxy: Proxy, transport: AsyncTransport
150
+ ) -> tuple[str, int]:
151
+ _, (host, port) = await cls._converse(
152
+ proxy.associate(("0.0.0.0", 0)), transport
153
+ )
154
+ return (proxy.host if host in ("0.0.0.0", "::") else host, port)
155
+
156
+ @classmethod
157
+ async def _tunnel(
158
+ cls, proxy: Proxy, destination: tuple[str, int], transport: AsyncTransport
159
+ ) -> bytes:
160
+ host, port = destination
161
+ if (family := proxy.scheme.local_address_family) is not None:
162
+ candidates = await asyncio.get_running_loop().getaddrinfo(
163
+ host, port, family=family, type=socket.SOCK_STREAM
164
+ )
165
+ host = str(candidates[0][4][0])
166
+ leftover, _ = await cls._converse(proxy.converse((host, port)), transport)
167
+ return leftover
168
+
169
+ @staticmethod
170
+ async def _converse[ResultT](
171
+ conversation: Generator[tuple[bytes, int | bytes], bytes, ResultT],
172
+ transport: AsyncTransport,
173
+ ) -> tuple[bytes, ResultT]:
174
+ pending = bytearray()
175
+ step = next(conversation)
176
+ while True:
177
+ request, expected = step
178
+ if request:
179
+ await transport.write(request)
180
+ while not (end := Proxy.reply_length(pending, expected)):
181
+ piece = await transport.read(RECEIVE_SIZE)
182
+ if not piece:
183
+ raise ProxyError(
184
+ "the proxy closed the connection during the tunnel handshake"
185
+ )
186
+ pending += piece
187
+ reply = bytes(pending[:end])
188
+ del pending[:end]
189
+ try:
190
+ step = conversation.send(reply)
191
+ except StopIteration as stop:
192
+ return bytes(pending), cast(ResultT, stop.value)
193
+
194
+ async def _run[ResultT](self, operation: Callable[[], ResultT]) -> ResultT:
195
+ while True:
196
+ try:
197
+ result = operation()
198
+ except WantReadError:
199
+ await self._flush()
200
+ await self._fill()
201
+ continue
202
+ except WantWriteError:
203
+ await self._flush()
204
+ continue
205
+ await self._flush()
206
+ return result
207
+
208
+ async def _flush(self) -> None:
209
+ # one take of everything buffered: draining a memory BIO in small pieces moves the remainder each time
210
+ while pending := self._buffer.outgoing_bytes_buffered():
211
+ await self._transport.write(self._buffer.process_outgoing(pending))
212
+
213
+ async def _fill(self) -> None:
214
+ data = await self._transport.read(RECEIVE_SIZE)
215
+ if not data:
216
+ raise RaggedEOF("the transport closed before close_notify")
217
+ self._buffer.process_incoming(data)
ssl2/client_hello.py ADDED
@@ -0,0 +1,353 @@
1
+ from collections.abc import Iterable
2
+ from dataclasses import dataclass, replace
3
+ from typing import Final, Self, final
4
+
5
+ from ssl2.parameters import (
6
+ CertificateCompressionAlgorithm,
7
+ CipherSuite,
8
+ CodePoint,
9
+ ContentType,
10
+ ECPointFormat,
11
+ ExtensionType,
12
+ HandshakeType,
13
+ NamedGroup,
14
+ NextProtocol,
15
+ ProtocolVersion,
16
+ PskKeyExchangeMode,
17
+ SignatureScheme,
18
+ )
19
+
20
+
21
+ @dataclass(frozen=True, slots=True, kw_only=True)
22
+ class ClientHello:
23
+ legacy_version: ProtocolVersion = ProtocolVersion.TLS_1_2
24
+ supported_versions: tuple[ProtocolVersion, ...] = (ProtocolVersion.TLS_1_2,)
25
+ cipher_suites: tuple[CipherSuite, ...]
26
+ extensions: tuple[ExtensionType, ...]
27
+ supported_groups: tuple[NamedGroup, ...] = ()
28
+ key_share_groups: tuple[NamedGroup, ...] = ()
29
+ ec_point_formats: tuple[ECPointFormat, ...] = ()
30
+ signature_algorithms: tuple[SignatureScheme, ...] = ()
31
+ application_protocols: tuple[bytes, ...] = ()
32
+ application_settings: tuple[bytes, ...] = ()
33
+ certificate_compression_algorithms: tuple[CertificateCompressionAlgorithm, ...] = ()
34
+ psk_key_exchange_modes: tuple[PskKeyExchangeMode, ...] = ()
35
+ delegated_credential_schemes: tuple[SignatureScheme, ...] = ()
36
+ record_size_limit: int | None = None
37
+ grease: bool = False
38
+ permute_extensions: bool = False
39
+
40
+ def __post_init__(self) -> None:
41
+ if len(set(self.extensions)) != len(self.extensions):
42
+ raise ValueError("extensions must not repeat")
43
+ if len(set(self.supported_versions)) != len(self.supported_versions):
44
+ raise ValueError("supported_versions must not repeat")
45
+ payloads: tuple[tuple[object, ExtensionType], ...] = (
46
+ (self.supported_groups, ExtensionType.supported_groups),
47
+ (self.key_share_groups, ExtensionType.key_share),
48
+ (self.ec_point_formats, ExtensionType.ec_point_formats),
49
+ (self.signature_algorithms, ExtensionType.signature_algorithms),
50
+ (
51
+ self.application_protocols,
52
+ ExtensionType.application_layer_protocol_negotiation,
53
+ ),
54
+ (
55
+ self.certificate_compression_algorithms,
56
+ ExtensionType.compress_certificate,
57
+ ),
58
+ (self.psk_key_exchange_modes, ExtensionType.psk_key_exchange_modes),
59
+ (self.delegated_credential_schemes, ExtensionType.delegated_credential),
60
+ (self.record_size_limit, ExtensionType.record_size_limit),
61
+ )
62
+ for value, extension in payloads:
63
+ if bool(value) != self.offers(extension):
64
+ raise ValueError(
65
+ f"{extension.name} needs its payload and its extension together"
66
+ )
67
+ settings = {
68
+ ExtensionType.application_settings,
69
+ ExtensionType.application_settings_old,
70
+ }
71
+ if len(settings & set(self.extensions)) != bool(self.application_settings):
72
+ raise ValueError(
73
+ "application_settings needs exactly one application_settings extension codepoint"
74
+ )
75
+ if not self._is_subsequence(self.key_share_groups, self.supported_groups):
76
+ raise ValueError(
77
+ "key_share_groups must follow the order of supported_groups"
78
+ )
79
+ if not self.offers(
80
+ ExtensionType.supported_versions
81
+ ) and self.supported_versions != (self.legacy_version,):
82
+ raise ValueError(
83
+ "without the supported_versions extension only the legacy version is offered"
84
+ )
85
+
86
+ def offers(self, extension: ExtensionType) -> bool:
87
+ return extension in self.extensions
88
+
89
+ @property
90
+ def highest_version(self) -> ProtocolVersion:
91
+ return max(self.supported_versions)
92
+
93
+ @property
94
+ def lowest_version(self) -> ProtocolVersion:
95
+ return min(self.supported_versions)
96
+
97
+ @staticmethod
98
+ def _is_subsequence(
99
+ part: tuple[NamedGroup, ...], whole: tuple[NamedGroup, ...]
100
+ ) -> bool:
101
+ position = 0
102
+ for group in whole:
103
+ if position < len(part) and part[position] == group:
104
+ position += 1
105
+ return position == len(part)
106
+
107
+
108
+ EMPTY_CLIENT_HELLO: Final = ClientHello(cipher_suites=(), extensions=())
109
+
110
+
111
+ class ClientHelloBuilder:
112
+ def __init__(self) -> None:
113
+ self._fields: dict[str, object] = {}
114
+
115
+ def legacy_version(self, version: ProtocolVersion | int) -> Self:
116
+ return self._set("legacy_version", ProtocolVersion(version))
117
+
118
+ def supported_versions(self, *versions: ProtocolVersion | int) -> Self:
119
+ return self._set(
120
+ "supported_versions",
121
+ tuple(ProtocolVersion(version) for version in versions),
122
+ )
123
+
124
+ def cipher_suites(self, *suites: CipherSuite | int) -> Self:
125
+ return self._set("cipher_suites", tuple(CipherSuite(suite) for suite in suites))
126
+
127
+ def extensions(self, *types: ExtensionType | int) -> Self:
128
+ return self._set("extensions", tuple(ExtensionType(type) for type in types))
129
+
130
+ def supported_groups(self, *groups: NamedGroup | int) -> Self:
131
+ return self._set(
132
+ "supported_groups", tuple(NamedGroup(group) for group in groups)
133
+ )
134
+
135
+ def key_shares(self, *groups: NamedGroup | int) -> Self:
136
+ return self._set(
137
+ "key_share_groups", tuple(NamedGroup(group) for group in groups)
138
+ )
139
+
140
+ def ec_point_formats(self, *formats: ECPointFormat | int) -> Self:
141
+ return self._set(
142
+ "ec_point_formats", tuple(ECPointFormat(format) for format in formats)
143
+ )
144
+
145
+ def signature_algorithms(self, *schemes: SignatureScheme | int) -> Self:
146
+ return self._set(
147
+ "signature_algorithms", tuple(SignatureScheme(scheme) for scheme in schemes)
148
+ )
149
+
150
+ def application_protocols(self, *protocols: NextProtocol | bytes) -> Self:
151
+ return self._set("application_protocols", self._protocol_bytes(protocols))
152
+
153
+ def application_settings(self, *protocols: NextProtocol | bytes) -> Self:
154
+ return self._set("application_settings", self._protocol_bytes(protocols))
155
+
156
+ def certificate_compression(
157
+ self, *algorithms: CertificateCompressionAlgorithm | int
158
+ ) -> Self:
159
+ algorithms_typed = tuple(
160
+ CertificateCompressionAlgorithm(algorithm) for algorithm in algorithms
161
+ )
162
+ return self._set("certificate_compression_algorithms", algorithms_typed)
163
+
164
+ def psk_key_exchange_modes(self, *modes: PskKeyExchangeMode | int) -> Self:
165
+ return self._set(
166
+ "psk_key_exchange_modes", tuple(PskKeyExchangeMode(mode) for mode in modes)
167
+ )
168
+
169
+ def delegated_credentials(self, *schemes: SignatureScheme | int) -> Self:
170
+ return self._set(
171
+ "delegated_credential_schemes",
172
+ tuple(SignatureScheme(scheme) for scheme in schemes),
173
+ )
174
+
175
+ def record_size_limit(self, limit: int | None) -> Self:
176
+ return self._set("record_size_limit", limit)
177
+
178
+ def grease(self, enabled: bool = True) -> Self:
179
+ return self._set("grease", enabled)
180
+
181
+ def permute_extensions(self, enabled: bool = True) -> Self:
182
+ return self._set("permute_extensions", enabled)
183
+
184
+ def build(self) -> ClientHello:
185
+ return replace(EMPTY_CLIENT_HELLO, **self._fields)
186
+
187
+ def _set(self, name: str, value: object) -> Self:
188
+ self._fields[name] = value
189
+ return self
190
+
191
+ @staticmethod
192
+ def _protocol_bytes(protocols: Iterable[NextProtocol | bytes]) -> tuple[bytes, ...]:
193
+ return tuple(
194
+ protocol.value if isinstance(protocol, NextProtocol) else protocol
195
+ for protocol in protocols
196
+ )
197
+
198
+
199
+ @final
200
+ class ByteReader:
201
+ def __init__(self, data: bytes) -> None:
202
+ self._data = data
203
+ self._position = 0
204
+
205
+ @property
206
+ def remaining(self) -> int:
207
+ return len(self._data) - self._position
208
+
209
+ def take(self, length: int) -> bytes:
210
+ if length > self.remaining:
211
+ raise ValueError("truncated ClientHello")
212
+ chunk = self._data[self._position : self._position + length]
213
+ self._position += length
214
+ return chunk
215
+
216
+ def integer(self, width: int) -> int:
217
+ return int.from_bytes(self.take(width), "big")
218
+
219
+ def vector(self, length_width: int) -> bytes:
220
+ return self.take(self.integer(length_width))
221
+
222
+ def integers(self, length_width: int, item_width: int) -> tuple[int, ...]:
223
+ body = self.vector(length_width)
224
+ if len(body) % item_width:
225
+ raise ValueError("malformed vector length")
226
+ return tuple(
227
+ int.from_bytes(body[offset : offset + item_width], "big")
228
+ for offset in range(0, len(body), item_width)
229
+ )
230
+
231
+ def strings(self, length_width: int, item_length_width: int) -> tuple[bytes, ...]:
232
+ inner = ByteReader(self.vector(length_width))
233
+ items: list[bytes] = []
234
+ while inner.remaining:
235
+ items.append(inner.vector(item_length_width))
236
+ return tuple(items)
237
+
238
+
239
+ @final
240
+ class ClientHelloParser:
241
+ def __init__(self) -> None:
242
+ self._fields: dict[str, object] = {}
243
+ self._grease = False
244
+
245
+ @classmethod
246
+ def parse(cls, data: bytes) -> ClientHello:
247
+ if not data:
248
+ raise ValueError("empty ClientHello")
249
+ if data[0] == ContentType.handshake:
250
+ data = cls._unwrap_records(data)
251
+ reader = ByteReader(data)
252
+ if reader.integer(1) != HandshakeType.client_hello:
253
+ raise ValueError("not a ClientHello")
254
+ parser = cls()
255
+ parser._read_body(ByteReader(reader.vector(3)))
256
+ return replace(EMPTY_CLIENT_HELLO, **parser._fields, grease=parser._grease)
257
+
258
+ @staticmethod
259
+ def _unwrap_records(data: bytes) -> bytes:
260
+ reader = ByteReader(data)
261
+ message = bytearray()
262
+ while reader.remaining:
263
+ content_type = reader.integer(1)
264
+ _ = reader.take(2)
265
+ fragment = reader.vector(2)
266
+ if content_type != ContentType.handshake:
267
+ raise ValueError("record is not a handshake record")
268
+ message += fragment
269
+ if len(message) >= 4 and len(message) >= 4 + int.from_bytes(
270
+ message[1:4], "big"
271
+ ):
272
+ break
273
+ return bytes(message)
274
+
275
+ def _read_body(self, body: ByteReader) -> None:
276
+ legacy_version = ProtocolVersion(body.integer(2))
277
+ _ = body.take(32)
278
+ _ = body.vector(1)
279
+ self._fields["legacy_version"] = legacy_version
280
+ self._fields["supported_versions"] = (legacy_version,)
281
+ self._fields["cipher_suites"] = self._typed(CipherSuite, body.integers(2, 2))
282
+ _ = body.vector(1)
283
+ extensions: list[ExtensionType] = []
284
+ if body.remaining:
285
+ block = ByteReader(body.vector(2))
286
+ while block.remaining:
287
+ extension_type = ExtensionType(block.integer(2))
288
+ payload = ByteReader(block.vector(2))
289
+ if extension_type.is_grease:
290
+ self._grease = True
291
+ continue
292
+ extensions.append(extension_type)
293
+ self._read_payload(extension_type, payload)
294
+ self._fields["extensions"] = tuple(extensions)
295
+
296
+ def _read_payload(self, extension_type: ExtensionType, payload: ByteReader) -> None:
297
+ match extension_type:
298
+ case ExtensionType.supported_versions:
299
+ self._fields["supported_versions"] = self._typed(
300
+ ProtocolVersion, payload.integers(1, 2)
301
+ )
302
+ case ExtensionType.supported_groups:
303
+ self._fields["supported_groups"] = self._typed(
304
+ NamedGroup, payload.integers(2, 2)
305
+ )
306
+ case ExtensionType.ec_point_formats:
307
+ self._fields["ec_point_formats"] = self._typed(
308
+ ECPointFormat, payload.integers(1, 1)
309
+ )
310
+ case ExtensionType.signature_algorithms:
311
+ self._fields["signature_algorithms"] = self._typed(
312
+ SignatureScheme, payload.integers(2, 2)
313
+ )
314
+ case ExtensionType.application_layer_protocol_negotiation:
315
+ self._fields["application_protocols"] = payload.strings(2, 1)
316
+ case (
317
+ ExtensionType.application_settings
318
+ | ExtensionType.application_settings_old
319
+ ):
320
+ self._fields["application_settings"] = payload.strings(2, 1)
321
+ case ExtensionType.key_share:
322
+ shares = ByteReader(payload.vector(2))
323
+ groups: list[int] = []
324
+ while shares.remaining:
325
+ groups.append(shares.integer(2))
326
+ _ = shares.vector(2)
327
+ self._fields["key_share_groups"] = self._typed(
328
+ NamedGroup, tuple(groups)
329
+ )
330
+ case ExtensionType.psk_key_exchange_modes:
331
+ self._fields["psk_key_exchange_modes"] = self._typed(
332
+ PskKeyExchangeMode, payload.integers(1, 1)
333
+ )
334
+ case ExtensionType.compress_certificate:
335
+ self._fields["certificate_compression_algorithms"] = self._typed(
336
+ CertificateCompressionAlgorithm, payload.integers(1, 2)
337
+ )
338
+ case ExtensionType.delegated_credential:
339
+ self._fields["delegated_credential_schemes"] = self._typed(
340
+ SignatureScheme, payload.integers(2, 2)
341
+ )
342
+ case ExtensionType.record_size_limit:
343
+ self._fields["record_size_limit"] = payload.integer(2)
344
+ case _:
345
+ pass
346
+
347
+ def _typed[CodePointT: CodePoint](
348
+ self, registry: type[CodePointT], values: tuple[int, ...]
349
+ ) -> tuple[CodePointT, ...]:
350
+ members = tuple(registry(value) for value in values)
351
+ kept = tuple(member for member in members if not member.is_grease)
352
+ self._grease |= len(kept) != len(members)
353
+ return kept