pybrws 0.1.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.
Files changed (116) hide show
  1. pybrws-0.1.0/LICENSE +21 -0
  2. pybrws-0.1.0/PKG-INFO +86 -0
  3. pybrws-0.1.0/README.md +46 -0
  4. pybrws-0.1.0/pybrws/__init__.py +3 -0
  5. pybrws-0.1.0/pybrws/__main__.py +3 -0
  6. pybrws-0.1.0/pybrws/certs.py +275 -0
  7. pybrws-0.1.0/pybrws/cli.py +303 -0
  8. pybrws-0.1.0/pybrws/client.py +303 -0
  9. pybrws-0.1.0/pybrws/cookies.py +234 -0
  10. pybrws-0.1.0/pybrws/data/__init__.py +21 -0
  11. pybrws-0.1.0/pybrws/data/chrome-1.hex +61 -0
  12. pybrws-0.1.0/pybrws/data/reference-firefox-157-peet.json +47 -0
  13. pybrws-0.1.0/pybrws/data/reference-firefox-157-windows-fresh.hex +60 -0
  14. pybrws-0.1.0/pybrws/differ.py +138 -0
  15. pybrws-0.1.0/pybrws/emitter.py +366 -0
  16. pybrws-0.1.0/pybrws/h3.py +341 -0
  17. pybrws-0.1.0/pybrws/handshake.py +814 -0
  18. pybrws-0.1.0/pybrws/headers.py +171 -0
  19. pybrws-0.1.0/pybrws/hexio.py +58 -0
  20. pybrws-0.1.0/pybrws/hosts.py +49 -0
  21. pybrws-0.1.0/pybrws/http1.py +268 -0
  22. pybrws-0.1.0/pybrws/http2.py +761 -0
  23. pybrws-0.1.0/pybrws/keyexchange.py +107 -0
  24. pybrws-0.1.0/pybrws/keyschedule.py +185 -0
  25. pybrws-0.1.0/pybrws/message.py +348 -0
  26. pybrws-0.1.0/pybrws/model.py +125 -0
  27. pybrws-0.1.0/pybrws/parser.py +123 -0
  28. pybrws-0.1.0/pybrws/pool.py +159 -0
  29. pybrws-0.1.0/pybrws/profiles/__init__.py +166 -0
  30. pybrws-0.1.0/pybrws/profiles/chrome155.py +579 -0
  31. pybrws-0.1.0/pybrws/profiles/firefox157.py +318 -0
  32. pybrws-0.1.0/pybrws/profiles/firefox157_quic.py +311 -0
  33. pybrws-0.1.0/pybrws/proxies.py +277 -0
  34. pybrws-0.1.0/pybrws/proxy.py +407 -0
  35. pybrws-0.1.0/pybrws/quic.py +454 -0
  36. pybrws-0.1.0/pybrws/quic_impersonate.py +510 -0
  37. pybrws-0.1.0/pybrws/records.py +161 -0
  38. pybrws-0.1.0/pybrws/session.py +305 -0
  39. pybrws-0.1.0/pybrws/tickets.py +122 -0
  40. pybrws-0.1.0/pybrws/tls12.py +374 -0
  41. pybrws-0.1.0/pybrws/tools/__init__.py +7 -0
  42. pybrws-0.1.0/pybrws/tools/_selfsigned.py +93 -0
  43. pybrws-0.1.0/pybrws/tools/ab_test.py +496 -0
  44. pybrws-0.1.0/pybrws/tools/catch_h2.py +382 -0
  45. pybrws-0.1.0/pybrws/tools/catch_hello.py +218 -0
  46. pybrws-0.1.0/pybrws/tools/catch_quic.py +231 -0
  47. pybrws-0.1.0/pybrws/tools/catch_request.py +288 -0
  48. pybrws-0.1.0/pybrws/tools/catch_resumed.py +285 -0
  49. pybrws-0.1.0/pybrws/tools/detect_waf.py +165 -0
  50. pybrws-0.1.0/pybrws/tools/isolate.py +117 -0
  51. pybrws-0.1.0/pybrws/tools/ja4.py +205 -0
  52. pybrws-0.1.0/pybrws/tools/probe_fingerprint.py +199 -0
  53. pybrws-0.1.0/pybrws/tools/read_quic.py +458 -0
  54. pybrws-0.1.0/pybrws/tools/verify.py +664 -0
  55. pybrws-0.1.0/pybrws/wire.py +27 -0
  56. pybrws-0.1.0/pybrws.egg-info/PKG-INFO +86 -0
  57. pybrws-0.1.0/pybrws.egg-info/SOURCES.txt +114 -0
  58. pybrws-0.1.0/pybrws.egg-info/dependency_links.txt +1 -0
  59. pybrws-0.1.0/pybrws.egg-info/entry_points.txt +2 -0
  60. pybrws-0.1.0/pybrws.egg-info/requires.txt +27 -0
  61. pybrws-0.1.0/pybrws.egg-info/top_level.txt +1 -0
  62. pybrws-0.1.0/pyproject.toml +68 -0
  63. pybrws-0.1.0/setup.cfg +4 -0
  64. pybrws-0.1.0/tests/test_ab_tool.py +229 -0
  65. pybrws-0.1.0/tests/test_certs.py +185 -0
  66. pybrws-0.1.0/tests/test_chrome_profile.py +556 -0
  67. pybrws-0.1.0/tests/test_cli.py +243 -0
  68. pybrws-0.1.0/tests/test_cli_subcommands.py +101 -0
  69. pybrws-0.1.0/tests/test_cookie_captures.py +97 -0
  70. pybrws-0.1.0/tests/test_cookie_slot.py +123 -0
  71. pybrws-0.1.0/tests/test_cookies.py +174 -0
  72. pybrws-0.1.0/tests/test_cookies_redirects.py +470 -0
  73. pybrws-0.1.0/tests/test_decoding_limits.py +325 -0
  74. pybrws-0.1.0/tests/test_differ.py +35 -0
  75. pybrws-0.1.0/tests/test_docs_config.py +117 -0
  76. pybrws-0.1.0/tests/test_emitter.py +263 -0
  77. pybrws-0.1.0/tests/test_fidelity_fixes.py +60 -0
  78. pybrws-0.1.0/tests/test_h2_profiles.py +94 -0
  79. pybrws-0.1.0/tests/test_h3.py +178 -0
  80. pybrws-0.1.0/tests/test_headers.py +117 -0
  81. pybrws-0.1.0/tests/test_http1.py +239 -0
  82. pybrws-0.1.0/tests/test_http1_hostile.py +130 -0
  83. pybrws-0.1.0/tests/test_http2.py +446 -0
  84. pybrws-0.1.0/tests/test_http2_hostile.py +391 -0
  85. pybrws-0.1.0/tests/test_keyexchange.py +103 -0
  86. pybrws-0.1.0/tests/test_keyschedule.py +130 -0
  87. pybrws-0.1.0/tests/test_method_articles.py +117 -0
  88. pybrws-0.1.0/tests/test_packaged_data.py +39 -0
  89. pybrws-0.1.0/tests/test_packaging.py +157 -0
  90. pybrws-0.1.0/tests/test_parser.py +62 -0
  91. pybrws-0.1.0/tests/test_plumbing.py +47 -0
  92. pybrws-0.1.0/tests/test_pool.py +178 -0
  93. pybrws-0.1.0/tests/test_post_profiles.py +177 -0
  94. pybrws-0.1.0/tests/test_profile_loader.py +109 -0
  95. pybrws-0.1.0/tests/test_proxies.py +276 -0
  96. pybrws-0.1.0/tests/test_proxy.py +176 -0
  97. pybrws-0.1.0/tests/test_published_text.py +95 -0
  98. pybrws-0.1.0/tests/test_quic.py +336 -0
  99. pybrws-0.1.0/tests/test_quic_hostile.py +533 -0
  100. pybrws-0.1.0/tests/test_quic_impersonate.py +327 -0
  101. pybrws-0.1.0/tests/test_quic_profile.py +243 -0
  102. pybrws-0.1.0/tests/test_read_quic.py +331 -0
  103. pybrws-0.1.0/tests/test_records.py +142 -0
  104. pybrws-0.1.0/tests/test_reference_pages.py +90 -0
  105. pybrws-0.1.0/tests/test_resumed.py +166 -0
  106. pybrws-0.1.0/tests/test_retry.py +141 -0
  107. pybrws-0.1.0/tests/test_review_followups.py +458 -0
  108. pybrws-0.1.0/tests/test_server_auth.py +83 -0
  109. pybrws-0.1.0/tests/test_session.py +192 -0
  110. pybrws-0.1.0/tests/test_session_proxy_hostile.py +457 -0
  111. pybrws-0.1.0/tests/test_streaming.py +263 -0
  112. pybrws-0.1.0/tests/test_tickets.py +208 -0
  113. pybrws-0.1.0/tests/test_tls12.py +395 -0
  114. pybrws-0.1.0/tests/test_tls_hostile.py +614 -0
  115. pybrws-0.1.0/tests/test_tool_package.py +174 -0
  116. pybrws-0.1.0/tests/test_verify_capture.py +887 -0
pybrws-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ernesto Pérez Costa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pybrws-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: pybrws
3
+ Version: 0.1.0
4
+ Summary: An HTTPS client that sends what Firefox 157 and Chrome 155 send over TLS and HTTP/2, and what Firefox 157 sends over HTTP/3
5
+ Author: Ernesto
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/epcosta17/pybrws
8
+ Project-URL: Documentation, https://pybrws.readthedocs.io/
9
+ Project-URL: Source, https://github.com/epcosta17/pybrws
10
+ Project-URL: Issues, https://github.com/epcosta17/pybrws/issues
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: cryptography>=50
21
+ Requires-Dist: hpack
22
+ Requires-Dist: brotli
23
+ Requires-Dist: publicsuffixlist
24
+ Requires-Dist: idna
25
+ Requires-Dist: certifi
26
+ Requires-Dist: zstandard; python_version < "3.14"
27
+ Provides-Extra: client
28
+ Provides-Extra: zstd
29
+ Provides-Extra: http3
30
+ Requires-Dist: aioquic<2,>=1.3; extra == "http3"
31
+ Provides-Extra: measure
32
+ Requires-Dist: httpx[http2]<1,>=0.28; extra == "measure"
33
+ Provides-Extra: docs
34
+ Requires-Dist: zensical<0.2,>=0.1.1; extra == "docs"
35
+ Requires-Dist: mkdocstrings-python; extra == "docs"
36
+ Provides-Extra: dev
37
+ Requires-Dist: aioquic<2,>=1.3; extra == "dev"
38
+ Requires-Dist: httpx[http2]<1,>=0.28; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # pybrws
42
+
43
+ Python + browser: an HTTPS client that sends what a real browser sends
44
+
45
+ pybrws is written from scratch in Python. It sends what Firefox 157 and Chrome 155 send on the wire: the TLS ClientHello, the HTTP/2 start and the headers. It also sends Firefox's QUIC hello over HTTP/3. It is a learning and measurement tool that you can install, and it began as a way to see how browsers look on the wire.
46
+
47
+ It is not a replacement for [curl_cffi](https://pypi.org/project/curl-cffi/). For production use, use curl_cffi. pybrws makes no promise that any site will accept its requests.
48
+
49
+ ## Install
50
+
51
+ ```
52
+ pip install pybrws
53
+ ```
54
+
55
+ Python 3.11 or newer. Two optional extras:
56
+
57
+ - `pip install "pybrws[http3]"` adds aioquic, for HTTP/3.
58
+ - `pip install "pybrws[measure]"` adds httpx, for the A/B controls of `pybrws ab`.
59
+
60
+ ## Use it from Python
61
+
62
+ ```python
63
+ from pybrws.client import get
64
+ from pybrws.profiles import chrome155
65
+
66
+ response = get("https://example.com/", profile=chrome155)
67
+ print(response.status, response.header("content-type"))
68
+ print(response.text()[:200])
69
+ ```
70
+
71
+ The default profile is `firefox157`. `post` and `request` work the same way. HTTP/3 is `pybrws.h3.get(url)`, Firefox only.
72
+
73
+ ## Use it from the command line
74
+
75
+ ```
76
+ pybrws https://example.com/
77
+ pybrws verify --local
78
+ ```
79
+
80
+ The first fetches a page like curl. The second compares the default profile's ClientHello with the browser capture shipped in the package, offline, and prints "same string as the capture". Other subcommands: `capture`, `read-quic`, `ja4`, `verify`, `probe`, `ab`, `isolate`, `waf`.
81
+
82
+ ## More
83
+
84
+ - Documentation: https://pybrws.readthedocs.io/
85
+ - Source: https://github.com/epcosta17/pybrws
86
+ - Licence: [MIT](https://github.com/epcosta17/pybrws/blob/master/LICENSE)
pybrws-0.1.0/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # pybrws
2
+
3
+ Python + browser: an HTTPS client that sends what a real browser sends
4
+
5
+ pybrws is written from scratch in Python. It sends what Firefox 157 and Chrome 155 send on the wire: the TLS ClientHello, the HTTP/2 start and the headers. It also sends Firefox's QUIC hello over HTTP/3. It is a learning and measurement tool that you can install, and it began as a way to see how browsers look on the wire.
6
+
7
+ It is not a replacement for [curl_cffi](https://pypi.org/project/curl-cffi/). For production use, use curl_cffi. pybrws makes no promise that any site will accept its requests.
8
+
9
+ ## Install
10
+
11
+ ```
12
+ pip install pybrws
13
+ ```
14
+
15
+ Python 3.11 or newer. Two optional extras:
16
+
17
+ - `pip install "pybrws[http3]"` adds aioquic, for HTTP/3.
18
+ - `pip install "pybrws[measure]"` adds httpx, for the A/B controls of `pybrws ab`.
19
+
20
+ ## Use it from Python
21
+
22
+ ```python
23
+ from pybrws.client import get
24
+ from pybrws.profiles import chrome155
25
+
26
+ response = get("https://example.com/", profile=chrome155)
27
+ print(response.status, response.header("content-type"))
28
+ print(response.text()[:200])
29
+ ```
30
+
31
+ The default profile is `firefox157`. `post` and `request` work the same way. HTTP/3 is `pybrws.h3.get(url)`, Firefox only.
32
+
33
+ ## Use it from the command line
34
+
35
+ ```
36
+ pybrws https://example.com/
37
+ pybrws verify --local
38
+ ```
39
+
40
+ The first fetches a page like curl. The second compares the default profile's ClientHello with the browser capture shipped in the package, offline, and prints "same string as the capture". Other subcommands: `capture`, `read-quic`, `ja4`, `verify`, `probe`, `ab`, `isolate`, `waf`.
41
+
42
+ ## More
43
+
44
+ - Documentation: https://pybrws.readthedocs.io/
45
+ - Source: https://github.com/epcosta17/pybrws
46
+ - Licence: [MIT](https://github.com/epcosta17/pybrws/blob/master/LICENSE)
@@ -0,0 +1,3 @@
1
+ """pybrws - a from-scratch browser-fingerprinted TLS client."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,275 @@
1
+ """Certificate handling: parsing, decompression, chain and signature checks.
2
+
3
+ Path validation is delegated to `cryptography.x509.verification`. Hand-rolling
4
+ it would mean reimplementing name constraints, policy handling and revocation
5
+ semantics, which is the kind of code that is wrong in ways nobody notices
6
+ until it matters.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ipaddress
12
+ import ssl
13
+ import warnings
14
+
15
+ from cryptography import x509
16
+ from cryptography.exceptions import InvalidSignature
17
+ from cryptography.hazmat.primitives import hashes
18
+ from cryptography.hazmat.primitives.asymmetric import ec, ed25519, padding, rsa
19
+ from cryptography.x509.verification import PolicyBuilder, Store
20
+
21
+ from . import hosts
22
+ from .parser import _Reader
23
+
24
+ # RFC 8879
25
+ COMPRESSION_ZLIB = 1
26
+ COMPRESSION_BROTLI = 2
27
+ COMPRESSION_ZSTD = 3
28
+
29
+ # The string a TLS 1.3 server signs, RFC 8446 section 4.4.3.
30
+ SIGNATURE_CONTEXT = b" " * 64 + b"TLS 1.3, server CertificateVerify" + b"\x00"
31
+
32
+ RSA_PSS = {
33
+ 0x0804: hashes.SHA256,
34
+ 0x0805: hashes.SHA384,
35
+ 0x0806: hashes.SHA512,
36
+ }
37
+ ECDSA = {
38
+ 0x0403: hashes.SHA256,
39
+ 0x0503: hashes.SHA384,
40
+ 0x0603: hashes.SHA512,
41
+ }
42
+ RSA_PKCS1 = {
43
+ 0x0401: hashes.SHA256,
44
+ 0x0501: hashes.SHA384,
45
+ 0x0601: hashes.SHA512,
46
+ }
47
+ ED25519 = 0x0807
48
+
49
+
50
+ class CertificateError(Exception):
51
+ pass
52
+
53
+
54
+ def decompress_certificate(message: bytes) -> bytes:
55
+ """Turn an RFC 8879 CompressedCertificate body into a Certificate body.
56
+
57
+ The output is bounded by the declared length *while* decompressing.
58
+ Checking it afterwards would be too late: this message arrives before the
59
+ server has proved anything, and a kilobyte of brotli can expand to a
60
+ gigabyte. RFC 8879 section 4 asks for exactly this bound. The declared
61
+ length is a uint24, so its own ceiling of 2**24 - 1 holds by construction.
62
+ """
63
+ reader = _Reader(message, "CompressedCertificate")
64
+ algorithm = reader.uint(2)
65
+ expected = reader.uint(3)
66
+ compressed = reader.vector(3)
67
+ # One byte past the declared length is enough to know it was a lie.
68
+ limit = expected + 1
69
+
70
+ try:
71
+ if algorithm == COMPRESSION_ZLIB:
72
+ import zlib
73
+
74
+ plain = zlib.decompressobj().decompress(compressed, limit)
75
+ elif algorithm == COMPRESSION_BROTLI:
76
+ import brotli
77
+
78
+ # Bounded to within one internal block of the limit, which is
79
+ # enough: the length check below rejects anything past it.
80
+ plain = brotli.Decompressor().process(
81
+ compressed, output_buffer_limit=limit)
82
+ elif algorithm == COMPRESSION_ZSTD:
83
+ try:
84
+ from compression import zstd
85
+ except ImportError:
86
+ import io
87
+
88
+ import zstandard
89
+
90
+ with zstandard.ZstdDecompressor().stream_reader(
91
+ io.BytesIO(compressed)) as source:
92
+ plain = source.read(limit)
93
+ else:
94
+ plain = zstd.ZstdDecompressor().decompress(
95
+ compressed, max_length=limit)
96
+ else:
97
+ raise CertificateError(
98
+ "unknown certificate compression " + str(algorithm))
99
+ except CertificateError:
100
+ raise
101
+ except Exception as exc:
102
+ raise CertificateError(
103
+ "certificate decompression failed: " + str(exc)) from exc
104
+
105
+ if len(plain) != expected:
106
+ raise CertificateError(
107
+ "decompressed to %d bytes, the server said %d" % (len(plain), expected)
108
+ )
109
+ return plain
110
+
111
+
112
+ def parse_certificate(body: bytes) -> list[bytes]:
113
+ """Return the DER certificates in a Certificate message body, leaf first."""
114
+ reader = _Reader(body, "Certificate")
115
+ reader.vector(1) # certificate_request_context, empty for a server
116
+ entries = _Reader(reader.vector(3), "certificate_list")
117
+ chain = []
118
+ while entries.remaining:
119
+ chain.append(entries.vector(3))
120
+ entries.vector(2) # per-certificate extensions
121
+ if not chain:
122
+ raise CertificateError("the server sent an empty certificate list")
123
+ return chain
124
+
125
+
126
+ def _parse_anchors(ders) -> list:
127
+ anchors = []
128
+ for der in ders:
129
+ try:
130
+ anchors.append(x509.load_der_x509_certificate(der))
131
+ except Exception:
132
+ # Bundles carry the occasional malformed anchor. One
133
+ # unparseable certificate should not cost the other few hundred.
134
+ continue
135
+ return anchors
136
+
137
+
138
+ def _platform_roots() -> list[bytes]:
139
+ context = ssl.create_default_context()
140
+ return list(context.get_ca_certs(binary_form=True))
141
+
142
+
143
+ def _mozilla_roots() -> list[bytes]:
144
+ """The DER of each certificate in the certifi bundle, which is Mozilla's
145
+ root program. A bundle that cannot be read counts as having none."""
146
+ import certifi
147
+
148
+ try:
149
+ with open(certifi.where(), encoding="ascii") as bundle:
150
+ text = bundle.read()
151
+ except (OSError, UnicodeDecodeError):
152
+ return []
153
+ begin, end = "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----"
154
+ ders = []
155
+ for block in text.split(end)[:-1]:
156
+ # The bundle puts a comment above each certificate.
157
+ _, found, body = block.partition(begin)
158
+ if not found:
159
+ continue
160
+ try:
161
+ ders.append(ssl.PEM_cert_to_DER_cert(begin + body + end + "\n"))
162
+ except ValueError:
163
+ continue
164
+ return ders
165
+
166
+
167
+ def system_store() -> Store:
168
+ """The platform's trust anchors alone, as Python's ssl module sees them.
169
+
170
+ Raises:
171
+ CertificateError: if the platform has none.
172
+ """
173
+ warnings.filterwarnings("ignore", message="Parsed a serial number")
174
+ anchors = _parse_anchors(_platform_roots())
175
+ if not anchors:
176
+ raise CertificateError("no trust anchors available from the system store")
177
+ return Store(anchors)
178
+
179
+
180
+ def default_store() -> Store:
181
+ """The roots a connection is verified against by default: Mozilla's
182
+ (the certifi bundle) plus whatever the platform adds, an enterprise CA
183
+ for example. That is close to Firefox's mix: Firefox adds only the roots
184
+ an administrator or user installed on the system, not the platform's
185
+ whole root program.
186
+
187
+ A certificate that is in both counts once. A source that cannot be read
188
+ counts as having none.
189
+
190
+ Raises:
191
+ CertificateError: if both sources yield no anchors.
192
+ """
193
+ warnings.filterwarnings("ignore", message="Parsed a serial number")
194
+ seen = set()
195
+ unique = []
196
+ for der in _mozilla_roots() + _platform_roots():
197
+ if der not in seen:
198
+ seen.add(der)
199
+ unique.append(der)
200
+ anchors = _parse_anchors(unique)
201
+ if not anchors:
202
+ raise CertificateError("no trust anchors available from certifi or the system store")
203
+ return Store(anchors)
204
+
205
+
206
+ def verify_chain(chain: list[bytes], hostname: str, store: Store | None = None):
207
+ """Validate the chain for `hostname`, returning the leaf certificate.
208
+
209
+ Raises:
210
+ CertificateError: if the chain does not validate.
211
+ """
212
+ certificates = [x509.load_der_x509_certificate(der) for der in chain]
213
+ leaf, intermediates = certificates[0], certificates[1:]
214
+ if hosts.is_ip_literal(hostname):
215
+ # An address is matched against an IP address SAN, never a DNS one.
216
+ subject = x509.IPAddress(ipaddress.ip_address(hostname.strip("[]")))
217
+ else:
218
+ subject = x509.DNSName(hostname)
219
+ verifier = (
220
+ PolicyBuilder()
221
+ .store(store if store is not None else default_store())
222
+ .build_server_verifier(subject)
223
+ )
224
+ try:
225
+ verifier.verify(leaf, intermediates)
226
+ except Exception as exc:
227
+ raise CertificateError("certificate chain rejected: " + str(exc)) from exc
228
+ return leaf
229
+
230
+
231
+ def verify_signature(leaf, scheme: int, signature: bytes, transcript_hash: bytes) -> None:
232
+ """Check the CertificateVerify signature over the handshake transcript.
233
+
234
+ This is what proves the peer holds the key in the certificate, rather
235
+ than merely having a copy of someone else's chain.
236
+ """
237
+ signed = SIGNATURE_CONTEXT + transcript_hash
238
+ key = leaf.public_key()
239
+ try:
240
+ if scheme in RSA_PSS:
241
+ algorithm = RSA_PSS[scheme]()
242
+ key.verify(signature, signed,
243
+ padding.PSS(mgf=padding.MGF1(algorithm),
244
+ salt_length=algorithm.digest_size),
245
+ algorithm)
246
+ elif scheme in ECDSA:
247
+ key.verify(signature, signed, ec.ECDSA(ECDSA[scheme]()))
248
+ elif scheme == ED25519:
249
+ key.verify(signature, signed)
250
+ elif scheme in RSA_PKCS1:
251
+ # Not permitted for TLS 1.3 CertificateVerify, but accepting it
252
+ # silently would hide a server doing the wrong thing.
253
+ raise CertificateError(
254
+ "server signed with rsa_pkcs1, which TLS 1.3 forbids here"
255
+ )
256
+ else:
257
+ raise CertificateError("unsupported signature scheme 0x%04x" % scheme)
258
+ except InvalidSignature as exc:
259
+ raise CertificateError("CertificateVerify signature is invalid") from exc
260
+ except CertificateError:
261
+ raise
262
+ except Exception as exc:
263
+ raise CertificateError("CertificateVerify could not be checked: "
264
+ + str(exc)) from exc
265
+
266
+
267
+ def expected_key_type(leaf) -> str:
268
+ key = leaf.public_key()
269
+ if isinstance(key, rsa.RSAPublicKey):
270
+ return "rsa"
271
+ if isinstance(key, ec.EllipticCurvePublicKey):
272
+ return "ecdsa"
273
+ if isinstance(key, ed25519.Ed25519PublicKey):
274
+ return "ed25519"
275
+ return type(key).__name__