Skip to content

Commit 9ee4b8c

Browse files
committed
Vendor truststore
1 parent 8c24fd2 commit 9ee4b8c

File tree

14 files changed

+1474
-27
lines changed

14 files changed

+1474
-27
lines changed

docs/html/topics/https-certificates.md

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,19 +28,9 @@ It is possible to use the system trust store, instead of the bundled certifi
2828
certificates for verifying HTTPS certificates. This approach will typically
2929
support corporate proxy certificates without additional configuration.
3030

31-
In order to use system trust stores, you need to:
32-
33-
- Use Python 3.10 or newer.
34-
- Install the {pypi}`truststore` package, in the Python environment you're
35-
running pip in.
36-
37-
This is typically done by installing this package using a system package
38-
manager or by using pip in {ref}`Hash-checking mode` for this package and
39-
trusting the network using the `--trusted-host` flag.
31+
In order to use system trust stores, you need to use Python 3.10 or newer.
4032

4133
```{pip-cli}
42-
$ python -m pip install truststore
43-
[...]
4434
$ python -m pip install SomePackage --use-feature=truststore
4535
[...]
4636
Successfully installed SomePackage

news/truststore.vendor.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add truststore 0.7.0

src/pip/_internal/cli/req_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def _create_truststore_ssl_context() -> Optional["SSLContext"]:
5858
return None
5959

6060
try:
61-
import truststore
61+
from ..._vendor import truststore
6262
except ImportError:
6363
raise CommandError(
6464
"To use the truststore feature, 'truststore' must be installed into "

src/pip/_vendor/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,5 @@ def vendored(modulename):
117117
vendored("rich.traceback")
118118
vendored("tenacity")
119119
vendored("tomli")
120+
vendored("truststore")
120121
vendored("urllib3")

src/pip/_vendor/truststore/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2022 Seth Michael Larson
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
13+
all 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
21+
THE SOFTWARE.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Verify certificates using native system trust stores"""
2+
3+
import sys as _sys
4+
5+
if _sys.version_info < (3, 10):
6+
raise ImportError("truststore requires Python 3.10 or later")
7+
8+
from ._api import SSLContext, extract_from_ssl, inject_into_ssl # noqa: E402
9+
10+
del _api, _sys # type: ignore[name-defined] # noqa: F821
11+
12+
__all__ = ["SSLContext", "inject_into_ssl", "extract_from_ssl"]
13+
__version__ = "0.7.0"

src/pip/_vendor/truststore/_api.py

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import array
2+
import ctypes
3+
import mmap
4+
import os
5+
import pickle
6+
import platform
7+
import socket
8+
import ssl
9+
import typing
10+
11+
import _ssl # type: ignore[import]
12+
13+
from ._ssl_constants import _original_SSLContext, _original_super_SSLContext
14+
15+
if platform.system() == "Windows":
16+
from ._windows import _configure_context, _verify_peercerts_impl
17+
elif platform.system() == "Darwin":
18+
from ._macos import _configure_context, _verify_peercerts_impl
19+
else:
20+
from ._openssl import _configure_context, _verify_peercerts_impl
21+
22+
# From typeshed/stdlib/ssl.pyi
23+
_StrOrBytesPath: typing.TypeAlias = str | bytes | os.PathLike[str] | os.PathLike[bytes]
24+
_PasswordType: typing.TypeAlias = str | bytes | typing.Callable[[], str | bytes]
25+
26+
# From typeshed/stdlib/_typeshed/__init__.py
27+
_ReadableBuffer: typing.TypeAlias = typing.Union[
28+
bytes,
29+
memoryview,
30+
bytearray,
31+
"array.array[typing.Any]",
32+
mmap.mmap,
33+
"ctypes._CData",
34+
pickle.PickleBuffer,
35+
]
36+
37+
38+
def inject_into_ssl() -> None:
39+
"""Injects the :class:`truststore.SSLContext` into the ``ssl``
40+
module by replacing :class:`ssl.SSLContext`.
41+
"""
42+
setattr(ssl, "SSLContext", SSLContext)
43+
# urllib3 holds on to its own reference of ssl.SSLContext
44+
# so we need to replace that reference too.
45+
try:
46+
import pip._vendor.urllib3.util.ssl_ as urllib3_ssl
47+
48+
setattr(urllib3_ssl, "SSLContext", SSLContext)
49+
except ImportError:
50+
pass
51+
52+
53+
def extract_from_ssl() -> None:
54+
"""Restores the :class:`ssl.SSLContext` class to its original state"""
55+
setattr(ssl, "SSLContext", _original_SSLContext)
56+
try:
57+
import pip._vendor.urllib3.util.ssl_ as urllib3_ssl
58+
59+
urllib3_ssl.SSLContext = _original_SSLContext
60+
except ImportError:
61+
pass
62+
63+
64+
class SSLContext(ssl.SSLContext):
65+
"""SSLContext API that uses system certificates on all platforms"""
66+
67+
def __init__(self, protocol: int = None) -> None: # type: ignore[assignment]
68+
self._ctx = _original_SSLContext(protocol)
69+
70+
class TruststoreSSLObject(ssl.SSLObject):
71+
# This object exists because wrap_bio() doesn't
72+
# immediately do the handshake so we need to do
73+
# certificate verifications after SSLObject.do_handshake()
74+
75+
def do_handshake(self) -> None:
76+
ret = super().do_handshake()
77+
_verify_peercerts(self, server_hostname=self.server_hostname)
78+
return ret
79+
80+
self._ctx.sslobject_class = TruststoreSSLObject
81+
82+
def wrap_socket(
83+
self,
84+
sock: socket.socket,
85+
server_side: bool = False,
86+
do_handshake_on_connect: bool = True,
87+
suppress_ragged_eofs: bool = True,
88+
server_hostname: str | None = None,
89+
session: ssl.SSLSession | None = None,
90+
) -> ssl.SSLSocket:
91+
# Use a context manager here because the
92+
# inner SSLContext holds on to our state
93+
# but also does the actual handshake.
94+
with _configure_context(self._ctx):
95+
ssl_sock = self._ctx.wrap_socket(
96+
sock,
97+
server_side=server_side,
98+
server_hostname=server_hostname,
99+
do_handshake_on_connect=do_handshake_on_connect,
100+
suppress_ragged_eofs=suppress_ragged_eofs,
101+
session=session,
102+
)
103+
try:
104+
_verify_peercerts(ssl_sock, server_hostname=server_hostname)
105+
except Exception:
106+
ssl_sock.close()
107+
raise
108+
return ssl_sock
109+
110+
def wrap_bio(
111+
self,
112+
incoming: ssl.MemoryBIO,
113+
outgoing: ssl.MemoryBIO,
114+
server_side: bool = False,
115+
server_hostname: str | None = None,
116+
session: ssl.SSLSession | None = None,
117+
) -> ssl.SSLObject:
118+
with _configure_context(self._ctx):
119+
ssl_obj = self._ctx.wrap_bio(
120+
incoming,
121+
outgoing,
122+
server_hostname=server_hostname,
123+
server_side=server_side,
124+
session=session,
125+
)
126+
return ssl_obj
127+
128+
def load_verify_locations(
129+
self,
130+
cafile: str | bytes | os.PathLike[str] | os.PathLike[bytes] | None = None,
131+
capath: str | bytes | os.PathLike[str] | os.PathLike[bytes] | None = None,
132+
cadata: str | _ReadableBuffer | None = None,
133+
) -> None:
134+
return self._ctx.load_verify_locations(
135+
cafile=cafile, capath=capath, cadata=cadata
136+
)
137+
138+
def load_cert_chain(
139+
self,
140+
certfile: _StrOrBytesPath,
141+
keyfile: _StrOrBytesPath | None = None,
142+
password: _PasswordType | None = None,
143+
) -> None:
144+
return self._ctx.load_cert_chain(
145+
certfile=certfile, keyfile=keyfile, password=password
146+
)
147+
148+
def load_default_certs(
149+
self, purpose: ssl.Purpose = ssl.Purpose.SERVER_AUTH
150+
) -> None:
151+
return self._ctx.load_default_certs(purpose)
152+
153+
def set_alpn_protocols(self, alpn_protocols: typing.Iterable[str]) -> None:
154+
return self._ctx.set_alpn_protocols(alpn_protocols)
155+
156+
def set_npn_protocols(self, npn_protocols: typing.Iterable[str]) -> None:
157+
return self._ctx.set_npn_protocols(npn_protocols)
158+
159+
def set_ciphers(self, __cipherlist: str) -> None:
160+
return self._ctx.set_ciphers(__cipherlist)
161+
162+
def get_ciphers(self) -> typing.Any:
163+
return self._ctx.get_ciphers()
164+
165+
def session_stats(self) -> dict[str, int]:
166+
return self._ctx.session_stats()
167+
168+
def cert_store_stats(self) -> dict[str, int]:
169+
raise NotImplementedError()
170+
171+
@typing.overload
172+
def get_ca_certs(
173+
self, binary_form: typing.Literal[False] = ...
174+
) -> list[typing.Any]:
175+
...
176+
177+
@typing.overload
178+
def get_ca_certs(self, binary_form: typing.Literal[True] = ...) -> list[bytes]:
179+
...
180+
181+
@typing.overload
182+
def get_ca_certs(self, binary_form: bool = ...) -> typing.Any:
183+
...
184+
185+
def get_ca_certs(self, binary_form: bool = False) -> list[typing.Any] | list[bytes]:
186+
raise NotImplementedError()
187+
188+
@property
189+
def check_hostname(self) -> bool:
190+
return self._ctx.check_hostname
191+
192+
@check_hostname.setter
193+
def check_hostname(self, value: bool) -> None:
194+
self._ctx.check_hostname = value
195+
196+
@property
197+
def hostname_checks_common_name(self) -> bool:
198+
return self._ctx.hostname_checks_common_name
199+
200+
@hostname_checks_common_name.setter
201+
def hostname_checks_common_name(self, value: bool) -> None:
202+
self._ctx.hostname_checks_common_name = value
203+
204+
@property
205+
def keylog_filename(self) -> str:
206+
return self._ctx.keylog_filename
207+
208+
@keylog_filename.setter
209+
def keylog_filename(self, value: str) -> None:
210+
self._ctx.keylog_filename = value
211+
212+
@property
213+
def maximum_version(self) -> ssl.TLSVersion:
214+
return self._ctx.maximum_version
215+
216+
@maximum_version.setter
217+
def maximum_version(self, value: ssl.TLSVersion) -> None:
218+
_original_super_SSLContext.maximum_version.__set__( # type: ignore[attr-defined]
219+
self._ctx, value
220+
)
221+
222+
@property
223+
def minimum_version(self) -> ssl.TLSVersion:
224+
return self._ctx.minimum_version
225+
226+
@minimum_version.setter
227+
def minimum_version(self, value: ssl.TLSVersion) -> None:
228+
_original_super_SSLContext.minimum_version.__set__( # type: ignore[attr-defined]
229+
self._ctx, value
230+
)
231+
232+
@property
233+
def options(self) -> ssl.Options:
234+
return self._ctx.options
235+
236+
@options.setter
237+
def options(self, value: ssl.Options) -> None:
238+
_original_super_SSLContext.options.__set__( # type: ignore[attr-defined]
239+
self._ctx, value
240+
)
241+
242+
@property
243+
def post_handshake_auth(self) -> bool:
244+
return self._ctx.post_handshake_auth
245+
246+
@post_handshake_auth.setter
247+
def post_handshake_auth(self, value: bool) -> None:
248+
self._ctx.post_handshake_auth = value
249+
250+
@property
251+
def protocol(self) -> ssl._SSLMethod:
252+
return self._ctx.protocol
253+
254+
@property
255+
def security_level(self) -> int: # type: ignore[override]
256+
return self._ctx.security_level
257+
258+
@property
259+
def verify_flags(self) -> ssl.VerifyFlags:
260+
return self._ctx.verify_flags
261+
262+
@verify_flags.setter
263+
def verify_flags(self, value: ssl.VerifyFlags) -> None:
264+
_original_super_SSLContext.verify_flags.__set__( # type: ignore[attr-defined]
265+
self._ctx, value
266+
)
267+
268+
@property
269+
def verify_mode(self) -> ssl.VerifyMode:
270+
return self._ctx.verify_mode
271+
272+
@verify_mode.setter
273+
def verify_mode(self, value: ssl.VerifyMode) -> None:
274+
_original_super_SSLContext.verify_mode.__set__( # type: ignore[attr-defined]
275+
self._ctx, value
276+
)
277+
278+
279+
def _verify_peercerts(
280+
sock_or_sslobj: ssl.SSLSocket | ssl.SSLObject, server_hostname: str | None
281+
) -> None:
282+
"""
283+
Verifies the peer certificates from an SSLSocket or SSLObject
284+
against the certificates in the OS trust store.
285+
"""
286+
sslobj: ssl.SSLObject = sock_or_sslobj # type: ignore[assignment]
287+
try:
288+
while not hasattr(sslobj, "get_unverified_chain"):
289+
sslobj = sslobj._sslobj # type: ignore[attr-defined]
290+
except AttributeError:
291+
pass
292+
293+
# SSLObject.get_unverified_chain() returns 'None'
294+
# if the peer sends no certificates. This is common
295+
# for the server-side scenario.
296+
unverified_chain: typing.Sequence[_ssl.Certificate] = (
297+
sslobj.get_unverified_chain() or () # type: ignore[attr-defined]
298+
)
299+
cert_bytes = [cert.public_bytes(_ssl.ENCODING_DER) for cert in unverified_chain]
300+
_verify_peercerts_impl(
301+
sock_or_sslobj.context, cert_bytes, server_hostname=server_hostname
302+
)

0 commit comments

Comments
 (0)