boring-ssl2 2.0.1__py3-none-win_amd64.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.
@@ -0,0 +1,27 @@
1
+ from boring_ssl2.buffer import TLSBuffer
2
+ from boring_ssl2.compression import BrotliDecoder
3
+ from boring_ssl2.contexts import BoringSSL, ClientContext, ServerContext
4
+ from boring_ssl2.errors import BoringSSLError, CertificateVerificationError
5
+ from boring_ssl2.library import Library
6
+ from boring_ssl2.profile import ClientHelloProfile
7
+ from boring_ssl2.quic import EncryptionLevel, PacketProtection, QuicHandshake, QuicVersion, Suite
8
+ from boring_ssl2.revocation import CertificateRevocationList, ParsedCertificate
9
+
10
+ __all__ = (
11
+ "BoringSSL",
12
+ "BoringSSLError",
13
+ "BrotliDecoder",
14
+ "CertificateRevocationList",
15
+ "CertificateVerificationError",
16
+ "ClientContext",
17
+ "ClientHelloProfile",
18
+ "EncryptionLevel",
19
+ "Library",
20
+ "PacketProtection",
21
+ "ParsedCertificate",
22
+ "QuicHandshake",
23
+ "QuicVersion",
24
+ "ServerContext",
25
+ "Suite",
26
+ "TLSBuffer",
27
+ )
boring_ssl2/binding.py ADDED
@@ -0,0 +1,101 @@
1
+ import ctypes
2
+ import sys
3
+ from collections.abc import Callable
4
+ from ctypes import (
5
+ CFUNCTYPE,
6
+ c_char_p,
7
+ c_int,
8
+ c_long,
9
+ c_size_t,
10
+ c_ssize_t,
11
+ c_uint,
12
+ c_uint8,
13
+ c_uint16,
14
+ c_uint32,
15
+ c_void_p,
16
+ )
17
+ from enum import IntEnum, StrEnum
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from ssl2.errors import ConfigurationError
22
+
23
+ from boring_ssl2.errors import BoringSSLError
24
+
25
+ type Foreign = type[c_char_p | c_int | c_long | c_size_t | c_ssize_t | c_uint | c_uint16 | c_uint32 | c_void_p]
26
+
27
+
28
+ class FileName(StrEnum):
29
+ # what CMake installs next to this package on each platform
30
+ LINUX = "libboring_ssl2.so"
31
+ DARWIN = "libboring_ssl2.dylib"
32
+ WINDOWS = "boring_ssl2.dll"
33
+
34
+
35
+ class Size(IntEnum):
36
+ ERROR_STRING = 256
37
+
38
+
39
+ class Callback:
40
+ # the C function pointer types BoringSSL and brotli call back into
41
+ CERTIFICATE_COMPRESS = CFUNCTYPE(c_int, c_void_p, c_void_p, c_void_p, c_size_t)
42
+ CERTIFICATE_DECOMPRESS = CFUNCTYPE(c_int, c_void_p, c_void_p, c_size_t, c_void_p, c_size_t)
43
+ PROTOCOL_SELECT = CFUNCTYPE(c_int, c_void_p, c_void_p, c_void_p, c_void_p, c_uint, c_void_p)
44
+ SERVER_NAME = CFUNCTYPE(c_int, c_void_p, c_void_p, c_void_p)
45
+ NEW_SESSION = CFUNCTYPE(c_int, c_void_p, c_void_p)
46
+ KEY_LOG = CFUNCTYPE(None, c_void_p, c_char_p)
47
+ QUIC_SET_SECRET = CFUNCTYPE(c_int, c_void_p, c_int, c_void_p, c_void_p, c_size_t)
48
+ QUIC_ADD_HANDSHAKE_DATA = CFUNCTYPE(c_int, c_void_p, c_int, c_void_p, c_size_t)
49
+ QUIC_FLUSH_FLIGHT = CFUNCTYPE(c_int, c_void_p)
50
+ QUIC_SEND_ALERT = CFUNCTYPE(c_int, c_void_p, c_int, c_uint8)
51
+
52
+
53
+ class LibraryPath:
54
+ @staticmethod
55
+ def bundled() -> Path:
56
+ directory = Path(__file__).parent
57
+ if sys.platform == "win32":
58
+ name = FileName.WINDOWS
59
+ elif sys.platform in ("darwin", "ios"):
60
+ name = FileName.DARWIN
61
+ else:
62
+ name = FileName.LINUX
63
+ path = directory / name
64
+ if not path.exists():
65
+ raise ConfigurationError(
66
+ f"no BoringSSL build at {path}; build one with cmake (see README.md) or pass Library(path=...)"
67
+ )
68
+ return path
69
+
70
+
71
+ class Binder:
72
+ # loads one shared object and binds its functions the first time each is used, so a build that lacks
73
+ # a function fails at the call that needs it and names it
74
+ def __init__(self, path: Path | str | None = None) -> None:
75
+ self.path = Path(path) if path is not None else LibraryPath.bundled()
76
+ self._handle = ctypes.CDLL(str(self.path))
77
+
78
+ def bind(self, name: str, result: Foreign | None, *arguments: Foreign) -> Callable[..., Any]:
79
+ try:
80
+ function = self._handle[name]
81
+ except AttributeError:
82
+ raise ConfigurationError(
83
+ f"{self.path} has no {name}; this BoringSSL build predates the feature that needs it"
84
+ ) from None
85
+ function.restype = result
86
+ function.argtypes = arguments
87
+ return function
88
+
89
+ def errors(self) -> tuple[str, ...]:
90
+ # drains BoringSSL's error queue into readable reasons
91
+ get_error = self.bind("ERR_get_error", c_uint32)
92
+ error_string = self.bind("ERR_error_string_n", c_char_p, c_uint32, c_void_p, c_size_t)
93
+ text = ctypes.create_string_buffer(Size.ERROR_STRING)
94
+ messages: list[str] = []
95
+ while code := get_error():
96
+ error_string(code, text, len(text))
97
+ messages.append(ctypes.string_at(text).decode("ascii", "replace"))
98
+ return tuple(messages)
99
+
100
+ def error(self, operation: str) -> BoringSSLError:
101
+ return BoringSSLError(operation, self.errors())
Binary file
boring_ssl2/buffer.py ADDED
@@ -0,0 +1,247 @@
1
+ import ctypes
2
+ from collections.abc import Buffer, Callable
3
+ from enum import IntEnum
4
+ from typing import overload
5
+
6
+ from ssl2.errors import EarlyDataRejectedError, RaggedEOF, WantReadError, WantWriteError
7
+ from ssl2.interface import ClientContext, Limit, ServerContext
8
+ from ssl2.registry import CipherSuite, NextProtocol, ProtocolVersion, TLSVersion
9
+
10
+ from boring_ssl2.errors import BoringSSLError, CertificateVerificationError
11
+ from boring_ssl2.library import Library
12
+
13
+
14
+ class SSLErrorCode(IntEnum):
15
+ # SSL_get_error
16
+ NONE = 0
17
+ SSL = 1
18
+ WANT_READ = 2
19
+ WANT_WRITE = 3
20
+ SYSCALL = 5
21
+ ZERO_RETURN = 6
22
+ EARLY_DATA_REJECTED = 15
23
+
24
+
25
+ class VerifyResult(IntEnum):
26
+ OK = 0
27
+
28
+
29
+ class ExDataIndex(IntEnum):
30
+ # slot 0 is SSL_set_app_data; the new session callback reads the cache key back from it
31
+ SESSION_KEY = 0
32
+
33
+
34
+ class CInt(IntEnum):
35
+ # the most SSL_read and SSL_write take in one call
36
+ MAXIMUM = 2**31 - 1
37
+
38
+
39
+ class TLSBuffer:
40
+ def __init__(
41
+ self,
42
+ context: ClientContext | ServerContext,
43
+ library: Library,
44
+ context_handle: int,
45
+ *,
46
+ server: bool,
47
+ session_key: str | None = None,
48
+ ) -> None:
49
+ self._context = context
50
+ self._library = library
51
+ handle = library.SSL_new(context_handle)
52
+ if handle is None:
53
+ raise library.error("SSL_new")
54
+ self._handle = handle
55
+ incoming, outgoing = library.BIO_new(library.BIO_s_mem()), library.BIO_new(library.BIO_s_mem())
56
+ if incoming is None or outgoing is None:
57
+ raise library.error("BIO_new")
58
+ self._incoming = incoming
59
+ self._outgoing = outgoing
60
+ library.SSL_set_bio(handle, incoming, outgoing)
61
+ if server:
62
+ library.SSL_set_accept_state(handle)
63
+ else:
64
+ library.SSL_set_connect_state(handle)
65
+ self._closing = False
66
+ self._storage = ctypes.create_string_buffer(Limit.RECEIVE)
67
+ self._session_key = None if session_key is None else ctypes.create_string_buffer(session_key.encode())
68
+ if self._session_key is not None:
69
+ library.check(
70
+ library.SSL_set_ex_data(handle, ExDataIndex.SESSION_KEY, ctypes.addressof(self._session_key)),
71
+ "SSL_set_ex_data",
72
+ )
73
+
74
+ def __del__(self) -> None:
75
+ self._library.SSL_free(self._handle)
76
+
77
+ @property
78
+ def handle(self) -> int:
79
+ return self._handle
80
+
81
+ @property
82
+ def library(self) -> Library:
83
+ return self._library
84
+
85
+ @property
86
+ def context(self) -> ClientContext | ServerContext:
87
+ return self._context
88
+
89
+ @property
90
+ def session_reused(self) -> bool:
91
+ return self._library.SSL_session_reused(self._handle) == 1
92
+
93
+ @overload
94
+ def read(self, amount: int) -> bytes: ...
95
+
96
+ @overload
97
+ def read(self, amount: int, buffer: Buffer) -> int: ...
98
+
99
+ def read(self, amount: int, buffer: Buffer | None = None) -> bytes | int:
100
+ if buffer is None:
101
+ size = min(amount, CInt.MAXIMUM)
102
+ storage = (
103
+ (ctypes.c_char * size).from_buffer(self._storage)
104
+ if size <= Limit.RECEIVE
105
+ else ctypes.create_string_buffer(size)
106
+ )
107
+ count = self._read_into(storage)
108
+ return ctypes.string_at(storage, count)
109
+ view = memoryview(buffer).cast("B")
110
+ return self._read_into((ctypes.c_char * min(amount, view.nbytes, CInt.MAXIMUM)).from_buffer(view))
111
+
112
+ def write(self, data: Buffer) -> int:
113
+ payload = bytes(data)
114
+ if not payload:
115
+ # BoringSSL holds post-handshake tickets until the first write; a zero-length write releases them
116
+ self._library.SSL_write(self._handle, payload, 0)
117
+ return 0
118
+ size = min(len(payload), CInt.MAXIMUM)
119
+ return self._operate(lambda: self._library.SSL_write(self._handle, payload, size), "write")
120
+
121
+ def attach(self, fileno: int) -> None:
122
+ # the socket becomes BoringSSL's own BIO: records move between the kernel and the TLS state with no stop here
123
+ self._library.check(self._library.SSL_set_fd(self._handle, fileno), "SSL_set_fd")
124
+
125
+ def do_handshake(self) -> None:
126
+ self._operate(lambda: self._library.SSL_do_handshake(self._handle), "handshake")
127
+
128
+ def cipher(self) -> CipherSuite | int | None:
129
+ cipher = self._library.SSL_get_current_cipher(self._handle)
130
+ return None if cipher is None else CipherSuite(self._library.SSL_CIPHER_get_protocol_id(cipher))
131
+
132
+ def negotiated_protocol(self) -> NextProtocol | bytes | None:
133
+ data, length = ctypes.c_void_p(), ctypes.c_uint()
134
+ self._library.SSL_get0_alpn_selected(self._handle, ctypes.byref(data), ctypes.byref(length))
135
+ if data.value is None or not length.value:
136
+ return None
137
+ selected = ctypes.string_at(data.value, length.value)
138
+ try:
139
+ return NextProtocol(selected)
140
+ except ValueError:
141
+ return selected
142
+
143
+ @property
144
+ def negotiated_tls_version(self) -> TLSVersion | None:
145
+ if not self._library.SSL_is_init_finished(self._handle):
146
+ return None
147
+ return ProtocolVersion(self._library.SSL_version(self._handle)).tls_version
148
+
149
+ def shutdown(self) -> None:
150
+ if not self._closing:
151
+ self._library.ERR_clear_error()
152
+ result = self._library.SSL_shutdown(self._handle)
153
+ if result < 0:
154
+ failure = self._failure(result, "shutdown")
155
+ if failure is not None:
156
+ raise failure
157
+ self._closing = True
158
+ if result == 1:
159
+ return
160
+ # BoringSSL refuses application data that arrives before the peer's close_notify, so it is read away
161
+ storage = ctypes.create_string_buffer(Limit.RECEIVE)
162
+ while self._read_into(storage):
163
+ pass
164
+
165
+ def process_incoming(self, data_from_network: bytes) -> None:
166
+ written = self._library.BIO_write(self._incoming, data_from_network, len(data_from_network))
167
+ if written != len(data_from_network):
168
+ raise self._library.error("BIO_write")
169
+
170
+ def incoming_bytes_buffered(self) -> int:
171
+ return self._library.BIO_ctrl_pending(self._incoming)
172
+
173
+ def process_outgoing(self, amount_bytes_for_network: int) -> bytes:
174
+ storage = ctypes.create_string_buffer(min(amount_bytes_for_network, CInt.MAXIMUM))
175
+ count = self._library.BIO_read(self._outgoing, storage, len(storage))
176
+ return ctypes.string_at(storage, count) if count > 0 else b""
177
+
178
+ def outgoing_bytes_buffered(self) -> int:
179
+ return self._library.BIO_ctrl_pending(self._outgoing)
180
+
181
+ def getpeercert(self) -> bytes | None:
182
+ chain = self.getpeerchain()
183
+ return chain[0] if chain else None
184
+
185
+ def getpeerchain(self) -> tuple[bytes, ...]:
186
+ library = self._library
187
+ stack = library.SSL_get0_peer_certificates(self._handle)
188
+ if stack is None:
189
+ return ()
190
+ certificates: list[bytes] = []
191
+ for index in range(library.OPENSSL_sk_num(stack)):
192
+ item = library.OPENSSL_sk_value(stack, index)
193
+ certificates.append(ctypes.string_at(library.CRYPTO_BUFFER_data(item), library.CRYPTO_BUFFER_len(item)))
194
+ return tuple(certificates)
195
+
196
+ def _read_into(self, storage: ctypes.Array[ctypes.c_char]) -> int:
197
+ size = len(storage)
198
+ if not size:
199
+ return 0
200
+ handle, library = self._handle, self._library
201
+ total = self._operate(lambda: library.SSL_read(handle, storage, size), "read")
202
+ # one SSL_read returns one record; the records already in the memory BIO or the kernel follow in this call.
203
+ # An attached socket is non-blocking, so BoringSSL answers WANT_READ when they run out, never a block
204
+ while 0 < total < size:
205
+ rest = (ctypes.c_char * (size - total)).from_buffer(storage, total)
206
+ more = library.SSL_read(handle, rest, size - total)
207
+ if more <= 0:
208
+ break
209
+ total += more
210
+ return total
211
+
212
+ def _operate(self, call: Callable[[], int], operation: str) -> int:
213
+ self._library.ERR_clear_error()
214
+ result = call()
215
+ if result > 0:
216
+ return result
217
+ failure = self._failure(result, operation)
218
+ if failure is None:
219
+ return 0
220
+ raise failure
221
+
222
+ def _failure(self, result: int, operation: str) -> Exception | None:
223
+ library = self._library
224
+ code = library.SSL_get_error(self._handle, result)
225
+ reasons = library.errors()
226
+ match code:
227
+ case SSLErrorCode.WANT_READ:
228
+ return WantReadError(f"{operation} needs more data from the peer")
229
+ case SSLErrorCode.WANT_WRITE:
230
+ return WantWriteError(f"{operation} needs the outgoing buffer drained")
231
+ case SSLErrorCode.ZERO_RETURN:
232
+ return None
233
+ case SSLErrorCode.EARLY_DATA_REJECTED:
234
+ return EarlyDataRejectedError(f"{operation}: the server declined the early data")
235
+ case SSLErrorCode.SSL:
236
+ verdict = library.SSL_get_verify_result(self._handle)
237
+ if verdict != VerifyResult.OK:
238
+ return CertificateVerificationError(
239
+ operation, reasons, library.X509_verify_cert_error_string(verdict).decode("ascii")
240
+ )
241
+ return BoringSSLError(operation, reasons)
242
+ case SSLErrorCode.SYSCALL:
243
+ # end of stream, a reset or a timeout on the socket: the connection is gone either way
244
+ detail = f": {'; '.join(reasons)}" if reasons else ""
245
+ return RaggedEOF(f"{operation}: the connection ended without close_notify{detail}")
246
+ case _:
247
+ return BoringSSLError(operation, (f"SSL_get_error={code}", *reasons))
@@ -0,0 +1,148 @@
1
+ import ctypes
2
+ import zlib
3
+ from compression import zstd
4
+ from enum import IntEnum
5
+
6
+ from ssl2.errors import ConfigurationError
7
+ from ssl2.registry import CertificateCompressionAlgorithm
8
+
9
+ from boring_ssl2.binding import Callback
10
+ from boring_ssl2.library import Library
11
+
12
+
13
+ class BrotliEncoderSetting(IntEnum):
14
+ # the quality, window and mode brotli's one-shot encoder takes
15
+ QUALITY = 11
16
+ WINDOW = 22
17
+ MODE_GENERIC = 0
18
+
19
+
20
+ class BrotliResult(IntEnum):
21
+ # what the one-shot calls and the stream decoder answer
22
+ FALSE = 0
23
+ TRUE = 1
24
+ NEEDS_MORE_INPUT = 2
25
+ NEEDS_MORE_OUTPUT = 3
26
+
27
+
28
+ class Size(IntEnum):
29
+ OUTPUT_CHUNK = 65536
30
+
31
+
32
+ class BrotliDecoder:
33
+ # a streaming decoder for brotli bodies, fed in pieces as they arrive
34
+ def __init__(self, library: Library | None = None) -> None:
35
+ self._library = library or Library.default()
36
+ state = self._library.BrotliDecoderCreateInstance(None, None, None)
37
+ if state is None:
38
+ raise MemoryError("brotli decoder state")
39
+ self._state = state
40
+ self.finished = False
41
+
42
+ def __del__(self) -> None:
43
+ self._library.BrotliDecoderDestroyInstance(self._state)
44
+
45
+ def decode(self, data: bytes) -> bytes:
46
+ storage = ctypes.create_string_buffer(data, len(data))
47
+ available_in = ctypes.c_size_t(len(data))
48
+ next_in = ctypes.c_void_p(ctypes.addressof(storage))
49
+ chunks: list[bytes] = []
50
+ while True:
51
+ output = ctypes.create_string_buffer(Size.OUTPUT_CHUNK)
52
+ available_out = ctypes.c_size_t(Size.OUTPUT_CHUNK)
53
+ next_out = ctypes.c_void_p(ctypes.addressof(output))
54
+ result = self._library.BrotliDecoderDecompressStream(
55
+ self._state,
56
+ ctypes.byref(available_in),
57
+ ctypes.byref(next_in),
58
+ ctypes.byref(available_out),
59
+ ctypes.byref(next_out),
60
+ None,
61
+ )
62
+ chunks.append(output.raw[: Size.OUTPUT_CHUNK - available_out.value])
63
+ match result:
64
+ case BrotliResult.NEEDS_MORE_OUTPUT:
65
+ continue
66
+ case BrotliResult.NEEDS_MORE_INPUT:
67
+ return b"".join(chunks)
68
+ case BrotliResult.TRUE:
69
+ self.finished = True
70
+ return b"".join(chunks)
71
+ case _:
72
+ raise ValueError("corrupt brotli stream")
73
+
74
+
75
+ class CertificateCompression:
76
+ # RFC 8879: the client decompresses the server's certificate, the server compresses its own
77
+ def __init__(self, library: Library) -> None:
78
+ self._library = library
79
+ self._callbacks: list[object] = []
80
+
81
+ def register(self, handle: int, algorithm: CertificateCompressionAlgorithm, *, client: bool) -> None:
82
+ if not algorithm.is_assigned:
83
+ raise ConfigurationError(f"no certificate compression codec for {algorithm.name}")
84
+ library = self._library
85
+
86
+ def compress(_ssl: int, out: int, data: int, length: int) -> int:
87
+ try:
88
+ encoded = self._encode(algorithm, ctypes.string_at(data, length))
89
+ except ValueError, zlib.error, zstd.ZstdError:
90
+ return BrotliResult.FALSE
91
+ return library.CBB_add_bytes(out, encoded, len(encoded))
92
+
93
+ def decompress(_ssl: int, out: int, size: int, data: int, length: int) -> int:
94
+ try:
95
+ decoded = self._decode(algorithm, ctypes.string_at(data, length), size)
96
+ except ValueError, zlib.error, zstd.ZstdError:
97
+ return BrotliResult.FALSE
98
+ buffer = library.CRYPTO_BUFFER_new(decoded, len(decoded), None)
99
+ if len(decoded) != size or buffer is None:
100
+ return BrotliResult.FALSE
101
+ ctypes.c_void_p.from_address(out).value = buffer
102
+ return BrotliResult.TRUE
103
+
104
+ compressor = None if client else Callback.CERTIFICATE_COMPRESS(compress)
105
+ decompressor = Callback.CERTIFICATE_DECOMPRESS(decompress) if client else None
106
+ self._callbacks += (compressor, decompressor)
107
+ library.check(
108
+ library.SSL_CTX_add_cert_compression_alg(handle, algorithm, compressor, decompressor),
109
+ "SSL_CTX_add_cert_compression_alg",
110
+ )
111
+
112
+ def _encode(self, algorithm: CertificateCompressionAlgorithm, data: bytes) -> bytes:
113
+ match algorithm:
114
+ case CertificateCompressionAlgorithm.zlib:
115
+ return zlib.compress(data)
116
+ case CertificateCompressionAlgorithm.zstd:
117
+ return zstd.compress(data)
118
+ case CertificateCompressionAlgorithm.brotli:
119
+ library = self._library
120
+ capacity = library.BrotliEncoderMaxCompressedSize(len(data))
121
+ storage = ctypes.create_string_buffer(capacity)
122
+ size = ctypes.c_size_t(capacity)
123
+ accepted = library.BrotliEncoderCompress(
124
+ BrotliEncoderSetting.QUALITY,
125
+ BrotliEncoderSetting.WINDOW,
126
+ BrotliEncoderSetting.MODE_GENERIC,
127
+ len(data),
128
+ data,
129
+ ctypes.byref(size),
130
+ storage,
131
+ )
132
+ if accepted != BrotliResult.TRUE:
133
+ raise ValueError("brotli compression failed")
134
+ return storage.raw[: size.value]
135
+
136
+ def _decode(self, algorithm: CertificateCompressionAlgorithm, data: bytes, size: int) -> bytes:
137
+ match algorithm:
138
+ case CertificateCompressionAlgorithm.zlib:
139
+ return zlib.decompress(data, bufsize=size)
140
+ case CertificateCompressionAlgorithm.zstd:
141
+ return zstd.decompress(data)
142
+ case CertificateCompressionAlgorithm.brotli:
143
+ storage = ctypes.create_string_buffer(size)
144
+ decoded = ctypes.c_size_t(size)
145
+ accepted = self._library.BrotliDecoderDecompress(len(data), data, ctypes.byref(decoded), storage)
146
+ if accepted != BrotliResult.TRUE:
147
+ raise ValueError("brotli decompression failed")
148
+ return storage.raw[: decoded.value]