ssl2 1.1.0__tar.gz → 2.0.0__tar.gz
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-1.1.0 → ssl2-2.0.0}/PKG-INFO +4 -4
- {ssl2-1.1.0 → ssl2-2.0.0}/README.md +3 -3
- {ssl2-1.1.0 → ssl2-2.0.0}/pyproject.toml +4 -1
- {ssl2-1.1.0 → ssl2-2.0.0}/pyproject.toml.orig +4 -1
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/__init__.py +9 -3
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/asynchronous.py +113 -24
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/client_hello.py +7 -41
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/credentials.py +5 -5
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/fingerprint.py +3 -3
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/interface.py +82 -25
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/parameters.py +3 -3
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/proxy.py +4 -4
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/errors.py +0 -0
- {ssl2-1.1.0 → ssl2-2.0.0}/src/ssl2/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ssl2
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: PEP 748 TLS interface: typed ClientHello model and parser, JA3, JA4 and Akamai fingerprints, proxies, sockets and asyncio streams
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Project-URL: Repository, https://github.com/baobien-dev/ssl2
|
|
@@ -126,7 +126,7 @@ print(JA3.parse(JA3.of(chrome).text) == JA3.of(chrome)) # True
|
|
|
126
126
|
|
|
127
127
|
`JA3.builder()` starts a `ClientHelloBuilder` from the JA3 fields; the payloads JA3 does not carry, such as signature schemes, key shares and ALPN, go in before `build()`.
|
|
128
128
|
|
|
129
|
-
`.trust_anchor_ids("44947.2.1", ...)` lists the trust anchor identifiers of draft-ietf-tls-trust-anchor-ids in dotted form, which the parser reads back from the wire. `
|
|
129
|
+
`.trust_anchor_ids("44947.2.1", ...)` lists the trust anchor identifiers of draft-ietf-tls-trust-anchor-ids in dotted form, which the parser reads back from the wire. `TrustAnchorIdentifier.encode` and `.decode` move between the dotted form and the wire bytes.
|
|
130
130
|
|
|
131
131
|
## Configuration, credentials, trust
|
|
132
132
|
|
|
@@ -175,8 +175,8 @@ async def main() -> None:
|
|
|
175
175
|
asyncio.run(main())
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
`context` is an engine's `ClientContext`. A peer that drops TCP without close_notify reads as `b""`, the same default the `ssl` module has.
|
|
178
|
+
`context` is an engine's `ClientContext`. A peer that drops TCP without close_notify reads as `b""`, the same default the `ssl` module has. On a direct connection, and behind an HTTP or SOCKS proxy, both hand the TCP socket to the engine through `TLSBuffer.attach`, so records move between the kernel and the TLS state without a stop in Python; behind an HTTPS proxy, or over any other `AsyncTransport`, the records go through the buffer's memory BIOs instead. A socket timeout applies to every wait.
|
|
179
179
|
|
|
180
180
|
## Writing an engine
|
|
181
181
|
|
|
182
|
-
An engine implements `ClientContext`, `ServerContext` and `TLSBuffer` from `ssl2.interface` and exposes `TLSImplementation(ClientContext, ServerContext, validate_config)`. `TLSSocket` and `TLSStream` drive any such `TLSBuffer
|
|
182
|
+
An engine implements `ClientContext`, `ServerContext` and `TLSBuffer` from `ssl2.interface` and exposes `TLSImplementation(ClientContext, ServerContext, validate_config)`. `TLSSocket` and `TLSStream` drive any such `TLSBuffer`, through its memory BIOs or, when the buffer implements `attach(fileno)`, by handing it the socket; `getpeerchain()` returns the certificates the peer sent, leaf first. `SessionCache` stores what the engine records.
|
|
@@ -118,7 +118,7 @@ print(JA3.parse(JA3.of(chrome).text) == JA3.of(chrome)) # True
|
|
|
118
118
|
|
|
119
119
|
`JA3.builder()` starts a `ClientHelloBuilder` from the JA3 fields; the payloads JA3 does not carry, such as signature schemes, key shares and ALPN, go in before `build()`.
|
|
120
120
|
|
|
121
|
-
`.trust_anchor_ids("44947.2.1", ...)` lists the trust anchor identifiers of draft-ietf-tls-trust-anchor-ids in dotted form, which the parser reads back from the wire. `
|
|
121
|
+
`.trust_anchor_ids("44947.2.1", ...)` lists the trust anchor identifiers of draft-ietf-tls-trust-anchor-ids in dotted form, which the parser reads back from the wire. `TrustAnchorIdentifier.encode` and `.decode` move between the dotted form and the wire bytes.
|
|
122
122
|
|
|
123
123
|
## Configuration, credentials, trust
|
|
124
124
|
|
|
@@ -167,8 +167,8 @@ async def main() -> None:
|
|
|
167
167
|
asyncio.run(main())
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
-
`context` is an engine's `ClientContext`. A peer that drops TCP without close_notify reads as `b""`, the same default the `ssl` module has.
|
|
170
|
+
`context` is an engine's `ClientContext`. A peer that drops TCP without close_notify reads as `b""`, the same default the `ssl` module has. On a direct connection, and behind an HTTP or SOCKS proxy, both hand the TCP socket to the engine through `TLSBuffer.attach`, so records move between the kernel and the TLS state without a stop in Python; behind an HTTPS proxy, or over any other `AsyncTransport`, the records go through the buffer's memory BIOs instead. A socket timeout applies to every wait.
|
|
171
171
|
|
|
172
172
|
## Writing an engine
|
|
173
173
|
|
|
174
|
-
An engine implements `ClientContext`, `ServerContext` and `TLSBuffer` from `ssl2.interface` and exposes `TLSImplementation(ClientContext, ServerContext, validate_config)`. `TLSSocket` and `TLSStream` drive any such `TLSBuffer
|
|
174
|
+
An engine implements `ClientContext`, `ServerContext` and `TLSBuffer` from `ssl2.interface` and exposes `TLSImplementation(ClientContext, ServerContext, validate_config)`. `TLSSocket` and `TLSStream` drive any such `TLSBuffer`, through its memory BIOs or, when the buffer implements `attach(fileno)`, by handing it the socket; `getpeerchain()` returns the certificates the peer sent, leaf first. `SessionCache` stores what the engine records.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ssl2"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.0"
|
|
4
4
|
description = "PEP 748 TLS interface: typed ClientHello model and parser, JA3, JA4 and Akamai fingerprints, proxies, sockets and asyncio streams"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -9,6 +9,9 @@ dependencies = []
|
|
|
9
9
|
[project.urls]
|
|
10
10
|
Repository = "https://github.com/baobien-dev/ssl2"
|
|
11
11
|
|
|
12
|
+
[tool.basedpyright]
|
|
13
|
+
typeCheckingMode = "strict"
|
|
14
|
+
|
|
12
15
|
[build-system]
|
|
13
16
|
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
14
17
|
build-backend = "uv_build"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ssl2"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.0"
|
|
4
4
|
description = "PEP 748 TLS interface: typed ClientHello model and parser, JA3, JA4 and Akamai fingerprints, proxies, sockets and asyncio streams"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -9,6 +9,9 @@ dependencies = []
|
|
|
9
9
|
[project.urls]
|
|
10
10
|
Repository = "https://github.com/baobien-dev/ssl2"
|
|
11
11
|
|
|
12
|
+
[tool.basedpyright]
|
|
13
|
+
typeCheckingMode = "strict"
|
|
14
|
+
|
|
12
15
|
[build-system]
|
|
13
16
|
requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
14
17
|
build-backend = "uv_build"
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
from ssl2.asynchronous import
|
|
1
|
+
from ssl2.asynchronous import (
|
|
2
|
+
AsyncTransport,
|
|
3
|
+
SocketTransport,
|
|
4
|
+
StreamTransport,
|
|
5
|
+
TLSStream,
|
|
6
|
+
Tunnel,
|
|
7
|
+
)
|
|
2
8
|
from ssl2.client_hello import (
|
|
3
|
-
CHROME_TRUST_ANCHOR_IDS,
|
|
4
9
|
ClientHello,
|
|
5
10
|
ClientHelloBuilder,
|
|
6
11
|
ClientHelloParser,
|
|
@@ -59,7 +64,6 @@ from ssl2.parameters import (
|
|
|
59
64
|
from ssl2.proxy import Proxy, ProxyCredentials, ProxyError, ProxyScheme
|
|
60
65
|
|
|
61
66
|
__all__ = (
|
|
62
|
-
"CHROME_TRUST_ANCHOR_IDS",
|
|
63
67
|
"JA3",
|
|
64
68
|
"JA4",
|
|
65
69
|
"AkamaiFingerprint",
|
|
@@ -97,6 +101,7 @@ __all__ = (
|
|
|
97
101
|
"SessionCache",
|
|
98
102
|
"SignatureScheme",
|
|
99
103
|
"SigningChain",
|
|
104
|
+
"SocketTransport",
|
|
100
105
|
"StreamTransport",
|
|
101
106
|
"TLSBuffer",
|
|
102
107
|
"TLSClientConfiguration",
|
|
@@ -109,6 +114,7 @@ __all__ = (
|
|
|
109
114
|
"Transport",
|
|
110
115
|
"TrustAnchorIdentifier",
|
|
111
116
|
"TrustStore",
|
|
117
|
+
"Tunnel",
|
|
112
118
|
"WantReadError",
|
|
113
119
|
"WantWriteError",
|
|
114
120
|
)
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import socket
|
|
3
3
|
from collections.abc import Callable, Generator
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from functools import partial
|
|
5
|
-
from typing import
|
|
6
|
+
from typing import Protocol, Self, cast
|
|
6
7
|
|
|
7
8
|
from ssl2.errors import RaggedEOF, WantReadError, WantWriteError
|
|
8
9
|
from ssl2.interface import RECEIVE_SIZE, ClientContext, ServerContext, TLSBuffer
|
|
9
10
|
from ssl2.parameters import CipherSuite, NextProtocol, TLSVersion
|
|
10
11
|
from ssl2.proxy import Proxy, ProxyError, ProxyScheme
|
|
11
12
|
|
|
12
|
-
SHUTDOWN_TIMEOUT
|
|
13
|
+
SHUTDOWN_TIMEOUT = 5.0
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class AsyncTransport(Protocol):
|
|
@@ -20,7 +21,6 @@ class AsyncTransport(Protocol):
|
|
|
20
21
|
async def close(self) -> None: ...
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
@final
|
|
24
24
|
class StreamTransport:
|
|
25
25
|
def __init__(
|
|
26
26
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
|
@@ -39,17 +39,67 @@ class StreamTransport:
|
|
|
39
39
|
self._writer.close()
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
class SocketTransport:
|
|
43
|
+
def __init__(self, sock: socket.socket) -> None:
|
|
44
|
+
self._socket = sock
|
|
45
|
+
self._loop = asyncio.get_running_loop()
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
async def connect(cls, address: tuple[str, int]) -> Self:
|
|
49
|
+
loop = asyncio.get_running_loop()
|
|
50
|
+
host, port = address
|
|
51
|
+
failure = OSError(f"{host}:{port} resolves to no address")
|
|
52
|
+
for family, kind, protocol, _, resolved in await loop.getaddrinfo(
|
|
53
|
+
host, port, type=socket.SOCK_STREAM
|
|
54
|
+
):
|
|
55
|
+
sock = socket.socket(family, kind, protocol)
|
|
56
|
+
sock.setblocking(False)
|
|
57
|
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
58
|
+
try:
|
|
59
|
+
await loop.sock_connect(sock, resolved)
|
|
60
|
+
except OSError as error:
|
|
61
|
+
sock.close()
|
|
62
|
+
failure = error
|
|
63
|
+
continue
|
|
64
|
+
return cls(sock)
|
|
65
|
+
raise failure
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def socket(self) -> socket.socket:
|
|
69
|
+
return self._socket
|
|
70
|
+
|
|
71
|
+
async def read(self, limit: int) -> bytes:
|
|
72
|
+
return await self._loop.sock_recv(self._socket, limit)
|
|
73
|
+
|
|
74
|
+
async def write(self, data: bytes) -> None:
|
|
75
|
+
await self._loop.sock_sendall(self._socket, data)
|
|
76
|
+
|
|
77
|
+
async def close(self) -> None:
|
|
78
|
+
self._socket.close()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, slots=True)
|
|
82
|
+
class Tunnel:
|
|
83
|
+
transport: AsyncTransport
|
|
84
|
+
leftover: bytes
|
|
85
|
+
# the TCP socket under the transport, when TLS can take it over; None behind a TLS hop
|
|
86
|
+
socket: socket.socket | None
|
|
87
|
+
|
|
88
|
+
|
|
43
89
|
class TLSStream:
|
|
44
90
|
def __init__(
|
|
45
91
|
self,
|
|
46
92
|
context: ClientContext | ServerContext,
|
|
47
93
|
buffer: TLSBuffer,
|
|
48
94
|
transport: AsyncTransport,
|
|
95
|
+
sock: socket.socket | None = None,
|
|
49
96
|
) -> None:
|
|
50
97
|
self._context = context
|
|
51
98
|
self._buffer = buffer
|
|
52
99
|
self._transport = transport
|
|
100
|
+
# with a socket, BoringSSL reads and writes the kernel itself; without one the records go through the transport
|
|
101
|
+
self._socket = sock
|
|
102
|
+
self._loop = asyncio.get_running_loop()
|
|
53
103
|
|
|
54
104
|
@classmethod
|
|
55
105
|
async def connect(
|
|
@@ -61,33 +111,39 @@ class TLSStream:
|
|
|
61
111
|
timeout: float | None = None,
|
|
62
112
|
) -> Self:
|
|
63
113
|
async with asyncio.timeout(timeout):
|
|
64
|
-
|
|
114
|
+
tunnel = await cls.tunnel(context, address, proxy)
|
|
65
115
|
try:
|
|
116
|
+
buffer = context.create_buffer(address[0])
|
|
117
|
+
if tunnel.socket is not None and not tunnel.leftover:
|
|
118
|
+
return await cls.open_socket(context, buffer, tunnel.socket)
|
|
66
119
|
return await cls.open(
|
|
67
|
-
context,
|
|
120
|
+
context, buffer, tunnel.transport, tunnel.leftover
|
|
68
121
|
)
|
|
69
122
|
except BaseException:
|
|
70
|
-
await transport.close()
|
|
123
|
+
await tunnel.transport.close()
|
|
71
124
|
raise
|
|
72
125
|
|
|
73
126
|
@classmethod
|
|
74
127
|
async def tunnel(
|
|
75
128
|
cls, context: ClientContext, address: tuple[str, int], proxy: Proxy | None
|
|
76
|
-
) ->
|
|
129
|
+
) -> Tunnel:
|
|
77
130
|
if proxy is None:
|
|
78
|
-
|
|
79
|
-
|
|
131
|
+
direct = await SocketTransport.connect(address)
|
|
132
|
+
return Tunnel(direct, b"", direct.socket)
|
|
133
|
+
hop: AsyncTransport
|
|
134
|
+
sock: socket.socket | None
|
|
80
135
|
if proxy.scheme is ProxyScheme.HTTPS:
|
|
81
|
-
|
|
136
|
+
hop = await cls.connect(context, (proxy.host, proxy.port))
|
|
137
|
+
sock = None
|
|
82
138
|
else:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
139
|
+
plain = await SocketTransport.connect((proxy.host, proxy.port))
|
|
140
|
+
hop, sock = plain, plain.socket
|
|
86
141
|
try:
|
|
87
|
-
|
|
142
|
+
leftover = await cls._tunnel(proxy, address, hop)
|
|
88
143
|
except BaseException:
|
|
89
|
-
await
|
|
144
|
+
await hop.close()
|
|
90
145
|
raise
|
|
146
|
+
return Tunnel(hop, leftover, sock)
|
|
91
147
|
|
|
92
148
|
@classmethod
|
|
93
149
|
async def open(
|
|
@@ -103,6 +159,18 @@ class TLSStream:
|
|
|
103
159
|
await self._run(buffer.do_handshake)
|
|
104
160
|
return self
|
|
105
161
|
|
|
162
|
+
@classmethod
|
|
163
|
+
async def open_socket(
|
|
164
|
+
cls,
|
|
165
|
+
context: ClientContext | ServerContext,
|
|
166
|
+
buffer: TLSBuffer,
|
|
167
|
+
sock: socket.socket,
|
|
168
|
+
) -> Self:
|
|
169
|
+
buffer.attach(sock.fileno())
|
|
170
|
+
self = cls(context, buffer, SocketTransport(sock), sock)
|
|
171
|
+
await self._run(buffer.do_handshake)
|
|
172
|
+
return self
|
|
173
|
+
|
|
106
174
|
@property
|
|
107
175
|
def context(self) -> ClientContext | ServerContext:
|
|
108
176
|
return self._context
|
|
@@ -118,10 +186,9 @@ class TLSStream:
|
|
|
118
186
|
return b""
|
|
119
187
|
|
|
120
188
|
async def write(self, data: bytes) -> None:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
view = view[written:]
|
|
189
|
+
while data:
|
|
190
|
+
written = await self._run(partial(self._buffer.write, data))
|
|
191
|
+
data = data[written:]
|
|
125
192
|
|
|
126
193
|
async def close(self) -> None:
|
|
127
194
|
try:
|
|
@@ -196,15 +263,37 @@ class TLSStream:
|
|
|
196
263
|
try:
|
|
197
264
|
result = operation()
|
|
198
265
|
except WantReadError:
|
|
199
|
-
|
|
200
|
-
|
|
266
|
+
if self._socket is None:
|
|
267
|
+
await self._flush()
|
|
268
|
+
await self._fill()
|
|
269
|
+
else:
|
|
270
|
+
await self._ready(self._loop.add_reader, self._loop.remove_reader)
|
|
201
271
|
continue
|
|
202
272
|
except WantWriteError:
|
|
203
|
-
|
|
273
|
+
if self._socket is None:
|
|
274
|
+
await self._flush()
|
|
275
|
+
else:
|
|
276
|
+
await self._ready(self._loop.add_writer, self._loop.remove_writer)
|
|
204
277
|
continue
|
|
205
|
-
|
|
278
|
+
if self._socket is None:
|
|
279
|
+
await self._flush()
|
|
206
280
|
return result
|
|
207
281
|
|
|
282
|
+
async def _ready(
|
|
283
|
+
self,
|
|
284
|
+
watch: Callable[[int, Callable[[], None]], object],
|
|
285
|
+
unwatch: Callable[[int], object],
|
|
286
|
+
) -> None:
|
|
287
|
+
if self._socket is None:
|
|
288
|
+
return
|
|
289
|
+
fileno = self._socket.fileno()
|
|
290
|
+
waiter: asyncio.Future[None] = self._loop.create_future()
|
|
291
|
+
watch(fileno, lambda: None if waiter.done() else waiter.set_result(None))
|
|
292
|
+
try:
|
|
293
|
+
await waiter
|
|
294
|
+
finally:
|
|
295
|
+
unwatch(fileno)
|
|
296
|
+
|
|
208
297
|
async def _flush(self) -> None:
|
|
209
298
|
# one take of everything buffered: draining a memory BIO in small pieces moves the remainder each time
|
|
210
299
|
while pending := self._buffer.outgoing_bytes_buffered():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from collections.abc import Iterable
|
|
2
2
|
from dataclasses import dataclass, replace
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Self
|
|
4
4
|
|
|
5
5
|
from ssl2.parameters import (
|
|
6
6
|
CertificateCompressionAlgorithm,
|
|
@@ -107,41 +107,9 @@ class ClientHello:
|
|
|
107
107
|
return position == len(part)
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
EMPTY_CLIENT_HELLO
|
|
111
|
-
# the trust anchor identifiers Chrome 155 requests: its root store, in the dotted form of draft-ietf-tls-trust-anchor-ids
|
|
112
|
-
CHROME_TRUST_ANCHOR_IDS: Final = (
|
|
113
|
-
"44947.2.1",
|
|
114
|
-
"44947.2.6",
|
|
115
|
-
"44947.2.13",
|
|
116
|
-
"44947.2.14",
|
|
117
|
-
"44947.2.15",
|
|
118
|
-
"44947.2.18",
|
|
119
|
-
"44947.2.19",
|
|
120
|
-
"44947.2.20",
|
|
121
|
-
"52580.200109.1.7",
|
|
122
|
-
"52580.200109.1.8",
|
|
123
|
-
"52580.200109.1.9",
|
|
124
|
-
"52580.200109.1.10",
|
|
125
|
-
"52580.200109.1.11",
|
|
126
|
-
"52580.200109.1.12",
|
|
127
|
-
"52580.200109.1.13",
|
|
128
|
-
"52580.200109.1.18",
|
|
129
|
-
"52580.200109.1.19",
|
|
130
|
-
"11129.9.1",
|
|
131
|
-
"11129.9.4",
|
|
132
|
-
"11129.9.5",
|
|
133
|
-
"11129.9.6",
|
|
134
|
-
"11129.9.7",
|
|
135
|
-
"11129.9.8",
|
|
136
|
-
"11129.9.10",
|
|
137
|
-
"11129.9.11",
|
|
138
|
-
"11129.9.12",
|
|
139
|
-
"11129.9.13",
|
|
140
|
-
"11129.9.15",
|
|
141
|
-
)
|
|
110
|
+
EMPTY_CLIENT_HELLO = ClientHello(cipher_suites=(), extensions=())
|
|
142
111
|
|
|
143
112
|
|
|
144
|
-
@final
|
|
145
113
|
class TrustAnchorIdentifier:
|
|
146
114
|
# draft-ietf-tls-trust-anchor-ids: a relative object identifier, dotted in the model and base-128 arcs on the wire
|
|
147
115
|
@staticmethod
|
|
@@ -264,7 +232,6 @@ class ClientHelloBuilder:
|
|
|
264
232
|
)
|
|
265
233
|
|
|
266
234
|
|
|
267
|
-
@final
|
|
268
235
|
class ByteReader:
|
|
269
236
|
def __init__(self, data: bytes) -> None:
|
|
270
237
|
self._data = data
|
|
@@ -304,7 +271,6 @@ class ByteReader:
|
|
|
304
271
|
return tuple(items)
|
|
305
272
|
|
|
306
273
|
|
|
307
|
-
@final
|
|
308
274
|
class ClientHelloParser:
|
|
309
275
|
def __init__(self) -> None:
|
|
310
276
|
self._fields: dict[str, object] = {}
|
|
@@ -329,7 +295,7 @@ class ClientHelloParser:
|
|
|
329
295
|
message = bytearray()
|
|
330
296
|
while reader.remaining:
|
|
331
297
|
content_type = reader.integer(1)
|
|
332
|
-
|
|
298
|
+
reader.take(2)
|
|
333
299
|
fragment = reader.vector(2)
|
|
334
300
|
if content_type != ContentType.handshake:
|
|
335
301
|
raise ValueError("record is not a handshake record")
|
|
@@ -342,12 +308,12 @@ class ClientHelloParser:
|
|
|
342
308
|
|
|
343
309
|
def _read_body(self, body: ByteReader) -> None:
|
|
344
310
|
legacy_version = ProtocolVersion(body.integer(2))
|
|
345
|
-
|
|
346
|
-
|
|
311
|
+
body.take(32)
|
|
312
|
+
body.vector(1)
|
|
347
313
|
self._fields["legacy_version"] = legacy_version
|
|
348
314
|
self._fields["supported_versions"] = (legacy_version,)
|
|
349
315
|
self._fields["cipher_suites"] = self._typed(CipherSuite, body.integers(2, 2))
|
|
350
|
-
|
|
316
|
+
body.vector(1)
|
|
351
317
|
extensions: list[ExtensionType] = []
|
|
352
318
|
if body.remaining:
|
|
353
319
|
block = ByteReader(body.vector(2))
|
|
@@ -391,7 +357,7 @@ class ClientHelloParser:
|
|
|
391
357
|
groups: list[int] = []
|
|
392
358
|
while shares.remaining:
|
|
393
359
|
groups.append(shares.integer(2))
|
|
394
|
-
|
|
360
|
+
shares.vector(2)
|
|
395
361
|
self._fields["key_share_groups"] = self._typed(
|
|
396
362
|
NamedGroup, tuple(groups)
|
|
397
363
|
)
|
|
@@ -5,11 +5,11 @@ import sys
|
|
|
5
5
|
from collections.abc import Callable, Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Self
|
|
9
9
|
|
|
10
10
|
from ssl2.errors import ConfigurationError
|
|
11
11
|
|
|
12
|
-
LINUX_CANDIDATES
|
|
12
|
+
LINUX_CANDIDATES = (
|
|
13
13
|
Path("/etc/ssl/certs/ca-certificates.crt"),
|
|
14
14
|
Path("/etc/pki/tls/certs/ca-bundle.crt"),
|
|
15
15
|
Path("/etc/ssl/ca-bundle.pem"),
|
|
@@ -20,11 +20,11 @@ LINUX_CANDIDATES: Final = (
|
|
|
20
20
|
Path("/etc/pki/tls/certs"),
|
|
21
21
|
Path("/system/etc/security/cacerts"),
|
|
22
22
|
)
|
|
23
|
-
DARWIN_CANDIDATES
|
|
23
|
+
DARWIN_CANDIDATES = (
|
|
24
24
|
Path("/etc/ssl/cert.pem"),
|
|
25
25
|
Path("/private/etc/ssl/cert.pem"),
|
|
26
26
|
)
|
|
27
|
-
X509_ASN_ENCODING
|
|
27
|
+
X509_ASN_ENCODING = 0x00000001
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@dataclass(frozen=True, slots=True)
|
|
@@ -115,7 +115,7 @@ class TrustStore(Credential):
|
|
|
115
115
|
+ b"-----END CERTIFICATE-----\n"
|
|
116
116
|
)
|
|
117
117
|
context = enumerate_store(store, context)
|
|
118
|
-
|
|
118
|
+
close_store(store, 0)
|
|
119
119
|
return b"".join(chunks)
|
|
120
120
|
|
|
121
121
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from enum import StrEnum
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Self
|
|
5
5
|
|
|
6
6
|
from ssl2.client_hello import ClientHello, ClientHelloBuilder
|
|
7
7
|
from ssl2.parameters import (
|
|
@@ -15,8 +15,8 @@ from ssl2.parameters import (
|
|
|
15
15
|
SignatureScheme,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
JA4_HASH_LENGTH
|
|
19
|
-
JA4_UNHASHED_EXTENSIONS
|
|
18
|
+
JA4_HASH_LENGTH = 12
|
|
19
|
+
JA4_UNHASHED_EXTENSIONS = frozenset(
|
|
20
20
|
{ExtensionType.server_name, ExtensionType.application_layer_protocol_negotiation}
|
|
21
21
|
)
|
|
22
22
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import errno
|
|
2
|
+
import select
|
|
2
3
|
import socket
|
|
3
4
|
from collections import OrderedDict
|
|
4
5
|
from collections.abc import Buffer, Callable, Sequence
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from threading import Lock
|
|
7
|
-
from typing import
|
|
8
|
+
from typing import Protocol, Self, overload
|
|
8
9
|
|
|
9
10
|
from ssl2.client_hello import ClientHello
|
|
10
11
|
from ssl2.credentials import SigningChain, TrustStore
|
|
@@ -12,7 +13,7 @@ from ssl2.errors import ConfigurationError, RaggedEOF, WantReadError, WantWriteE
|
|
|
12
13
|
from ssl2.parameters import CipherSuite, NextProtocol, TLSVersion
|
|
13
14
|
from ssl2.proxy import Proxy, ProxyError, ProxyScheme
|
|
14
15
|
|
|
15
|
-
RECEIVE_SIZE
|
|
16
|
+
RECEIVE_SIZE = 65536
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@dataclass(frozen=True, slots=True)
|
|
@@ -84,6 +85,10 @@ class TLSBuffer(Protocol):
|
|
|
84
85
|
|
|
85
86
|
def getpeercert(self) -> bytes | None: ...
|
|
86
87
|
|
|
88
|
+
def getpeerchain(self) -> tuple[bytes, ...]: ...
|
|
89
|
+
|
|
90
|
+
def attach(self, fileno: int, /) -> None: ...
|
|
91
|
+
|
|
87
92
|
|
|
88
93
|
class ClientContext(Protocol):
|
|
89
94
|
def __init__(self, configuration: TLSClientConfiguration) -> None: ...
|
|
@@ -114,7 +119,6 @@ class TLSImplementation[ClientContextT: ClientContext, ServerContextT: ServerCon
|
|
|
114
119
|
validate_config: Callable[[TLSClientConfiguration | TLSServerConfiguration], None]
|
|
115
120
|
|
|
116
121
|
|
|
117
|
-
@final
|
|
118
122
|
class SessionCache:
|
|
119
123
|
def __init__(self, limit: int = 1024) -> None:
|
|
120
124
|
self._entries: OrderedDict[str, tuple[bytes, bool]] = OrderedDict()
|
|
@@ -126,7 +130,7 @@ class SessionCache:
|
|
|
126
130
|
self._entries[key] = (session, single_use)
|
|
127
131
|
self._entries.move_to_end(key)
|
|
128
132
|
while len(self._entries) > self._limit:
|
|
129
|
-
|
|
133
|
+
self._entries.popitem(last=False)
|
|
130
134
|
|
|
131
135
|
def take(self, key: str) -> bytes | None:
|
|
132
136
|
with self._lock:
|
|
@@ -170,9 +174,10 @@ class Listening:
|
|
|
170
174
|
class Connected:
|
|
171
175
|
buffer: TLSBuffer
|
|
172
176
|
transport: Transport
|
|
177
|
+
# the socket BoringSSL reads and writes itself; None when the records go through the transport in Python
|
|
178
|
+
socket: socket.socket | None = None
|
|
173
179
|
|
|
174
180
|
|
|
175
|
-
@final
|
|
176
181
|
class TLSSocket:
|
|
177
182
|
def __init__(
|
|
178
183
|
self, context: ClientContext | ServerContext, state: Listening | Connected
|
|
@@ -180,6 +185,8 @@ class TLSSocket:
|
|
|
180
185
|
self._context = context
|
|
181
186
|
self._state = state
|
|
182
187
|
self._unsent = b""
|
|
188
|
+
# the timeout of an attached socket lives here: the socket itself stays non-blocking for BoringSSL
|
|
189
|
+
self._timeout: float | None = None
|
|
183
190
|
|
|
184
191
|
@classmethod
|
|
185
192
|
def connect(
|
|
@@ -190,22 +197,26 @@ class TLSSocket:
|
|
|
190
197
|
proxy: Proxy | None = None,
|
|
191
198
|
timeout: float | None = None,
|
|
192
199
|
) -> Self:
|
|
193
|
-
|
|
200
|
+
buffer = context.create_buffer(address[0])
|
|
194
201
|
if proxy is None:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
transport = cls.connect(context, (proxy.host, proxy.port), timeout=timeout)
|
|
198
|
-
else:
|
|
199
|
-
transport = socket.create_connection(
|
|
200
|
-
(proxy.host, proxy.port), timeout=timeout
|
|
202
|
+
return cls.open_socket(
|
|
203
|
+
context, buffer, socket.create_connection(address, timeout=timeout)
|
|
201
204
|
)
|
|
205
|
+
if proxy.scheme is ProxyScheme.HTTPS:
|
|
206
|
+
hop = cls.connect(context, (proxy.host, proxy.port), timeout=timeout)
|
|
207
|
+
try:
|
|
208
|
+
return cls.open(context, buffer, hop, cls._tunnel(proxy, address, hop))
|
|
209
|
+
except BaseException:
|
|
210
|
+
hop.close()
|
|
211
|
+
raise
|
|
212
|
+
plain = socket.create_connection((proxy.host, proxy.port), timeout=timeout)
|
|
202
213
|
try:
|
|
203
|
-
leftover =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
214
|
+
leftover = cls._tunnel(proxy, address, plain)
|
|
215
|
+
if leftover:
|
|
216
|
+
return cls.open(context, buffer, plain, leftover)
|
|
217
|
+
return cls.open_socket(context, buffer, plain)
|
|
207
218
|
except BaseException:
|
|
208
|
-
|
|
219
|
+
plain.close()
|
|
209
220
|
raise
|
|
210
221
|
|
|
211
222
|
@classmethod
|
|
@@ -222,6 +233,24 @@ class TLSSocket:
|
|
|
222
233
|
self._run(buffer.do_handshake)
|
|
223
234
|
return self
|
|
224
235
|
|
|
236
|
+
@classmethod
|
|
237
|
+
def open_socket(
|
|
238
|
+
cls,
|
|
239
|
+
context: ClientContext | ServerContext,
|
|
240
|
+
buffer: TLSBuffer,
|
|
241
|
+
sock: socket.socket,
|
|
242
|
+
) -> Self:
|
|
243
|
+
buffer.attach(sock.fileno())
|
|
244
|
+
self = cls(context, Connected(buffer, sock, sock))
|
|
245
|
+
self._timeout = sock.gettimeout()
|
|
246
|
+
sock.setblocking(False)
|
|
247
|
+
try:
|
|
248
|
+
self._run(buffer.do_handshake)
|
|
249
|
+
except BaseException:
|
|
250
|
+
sock.close()
|
|
251
|
+
raise
|
|
252
|
+
return self
|
|
253
|
+
|
|
225
254
|
@classmethod
|
|
226
255
|
def bind(cls, context: ServerContext, address: tuple[str | None, int]) -> Self:
|
|
227
256
|
host, port = address
|
|
@@ -276,7 +305,7 @@ class TLSSocket:
|
|
|
276
305
|
accepted: tuple[socket.socket, tuple[str | None, int]] = (
|
|
277
306
|
listening.listener.accept()
|
|
278
307
|
)
|
|
279
|
-
return type(self).
|
|
308
|
+
return type(self).open_socket(
|
|
280
309
|
self._context, listening.accept(), accepted[0]
|
|
281
310
|
), accepted[1]
|
|
282
311
|
|
|
@@ -289,6 +318,9 @@ class TLSSocket:
|
|
|
289
318
|
def getpeercert(self) -> bytes | None:
|
|
290
319
|
return self._connected().buffer.getpeercert()
|
|
291
320
|
|
|
321
|
+
def getpeerchain(self) -> tuple[bytes, ...]:
|
|
322
|
+
return self._connected().buffer.getpeerchain()
|
|
323
|
+
|
|
292
324
|
def cipher(self) -> CipherSuite | int | None:
|
|
293
325
|
return self._connected().buffer.cipher()
|
|
294
326
|
|
|
@@ -300,9 +332,14 @@ class TLSSocket:
|
|
|
300
332
|
return self._connected().buffer.negotiated_tls_version
|
|
301
333
|
|
|
302
334
|
def settimeout(self, value: float | None) -> None:
|
|
303
|
-
self.
|
|
335
|
+
if isinstance(self._state, Connected) and self._state.socket is not None:
|
|
336
|
+
self._timeout = value
|
|
337
|
+
else:
|
|
338
|
+
self._endpoint().settimeout(value)
|
|
304
339
|
|
|
305
340
|
def gettimeout(self) -> float | None:
|
|
341
|
+
if isinstance(self._state, Connected) and self._state.socket is not None:
|
|
342
|
+
return self._timeout
|
|
306
343
|
return self._endpoint().gettimeout()
|
|
307
344
|
|
|
308
345
|
def fileno(self) -> int:
|
|
@@ -358,19 +395,38 @@ class TLSSocket:
|
|
|
358
395
|
|
|
359
396
|
def _run[ResultT](self, operation: Callable[[], ResultT]) -> ResultT:
|
|
360
397
|
connection = self._connected()
|
|
398
|
+
sock = connection.socket
|
|
361
399
|
while True:
|
|
362
400
|
try:
|
|
363
401
|
result = operation()
|
|
364
402
|
except WantReadError:
|
|
365
|
-
|
|
366
|
-
|
|
403
|
+
if sock is None:
|
|
404
|
+
self._flush(connection)
|
|
405
|
+
self._fill(connection)
|
|
406
|
+
elif self._timeout == 0:
|
|
407
|
+
raise
|
|
408
|
+
else:
|
|
409
|
+
self._wait(sock, reading=True)
|
|
367
410
|
continue
|
|
368
411
|
except WantWriteError:
|
|
369
|
-
|
|
412
|
+
if sock is None:
|
|
413
|
+
self._flush(connection)
|
|
414
|
+
elif self._timeout == 0:
|
|
415
|
+
raise
|
|
416
|
+
else:
|
|
417
|
+
self._wait(sock, reading=False)
|
|
370
418
|
continue
|
|
371
|
-
|
|
419
|
+
if sock is None:
|
|
420
|
+
self._flush(connection)
|
|
372
421
|
return result
|
|
373
422
|
|
|
423
|
+
def _wait(self, sock: socket.socket, *, reading: bool) -> None:
|
|
424
|
+
readable, writable, _ = select.select(
|
|
425
|
+
[sock] if reading else [], [] if reading else [sock], [], self._timeout
|
|
426
|
+
)
|
|
427
|
+
if not readable and not writable:
|
|
428
|
+
raise TimeoutError("timed out")
|
|
429
|
+
|
|
374
430
|
def _flush(self, connection: Connected) -> None:
|
|
375
431
|
while self._unsent or connection.buffer.outgoing_bytes_buffered():
|
|
376
432
|
if not self._unsent:
|
|
@@ -393,6 +449,7 @@ class TLSSocket:
|
|
|
393
449
|
def _shutdown_once(self, connection: Connected) -> None:
|
|
394
450
|
try:
|
|
395
451
|
connection.buffer.shutdown()
|
|
396
|
-
except WantReadError:
|
|
452
|
+
except WantReadError, WantWriteError:
|
|
397
453
|
pass
|
|
398
|
-
|
|
454
|
+
if connection.socket is None:
|
|
455
|
+
self._flush(connection)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# generated from the IANA registry CSV files and BoringSSL tls1.h at 8f77af2e, so regenerate rather than edit
|
|
2
2
|
from enum import Enum, IntEnum
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Self, override
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class CodePoint(IntEnum):
|
|
@@ -55,13 +55,13 @@ class ProtocolVersion(CodePoint):
|
|
|
55
55
|
return TLS_VERSIONS.get(self)
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
PROTOCOL_VERSIONS
|
|
58
|
+
PROTOCOL_VERSIONS = {
|
|
59
59
|
TLSVersion.TLSv1_0: ProtocolVersion.TLS_1_0,
|
|
60
60
|
TLSVersion.TLSv1_1: ProtocolVersion.TLS_1_1,
|
|
61
61
|
TLSVersion.TLSv1_2: ProtocolVersion.TLS_1_2,
|
|
62
62
|
TLSVersion.TLSv1_3: ProtocolVersion.TLS_1_3,
|
|
63
63
|
}
|
|
64
|
-
TLS_VERSIONS
|
|
64
|
+
TLS_VERSIONS = {protocol: named for named, protocol in PROTOCOL_VERSIONS.items()}
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class ServerNameType(CodePoint):
|
|
@@ -5,15 +5,15 @@ import struct
|
|
|
5
5
|
from collections.abc import Generator
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import IntEnum, StrEnum
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import Self
|
|
9
9
|
from urllib.parse import unquote, urlsplit
|
|
10
10
|
|
|
11
11
|
type Conversation = Generator[tuple[bytes, int | bytes], bytes, object]
|
|
12
12
|
type Association = Generator[tuple[bytes, int | bytes], bytes, tuple[str, int]]
|
|
13
13
|
|
|
14
|
-
REPLY_LIMIT
|
|
15
|
-
USERNAME_PASSWORD_VERSION
|
|
16
|
-
SOCKS4A_NAME_FOLLOWS
|
|
14
|
+
REPLY_LIMIT = 65536
|
|
15
|
+
USERNAME_PASSWORD_VERSION = 0x01
|
|
16
|
+
SOCKS4A_NAME_FOLLOWS = bytes((0, 0, 0, 1))
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class ProxyError(ConnectionError):
|
|
File without changes
|
|
File without changes
|