Skip to content

PEP-458 Implementation (Secure downloads with signed metadata) #9041

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 48 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a6ff178
Initial tuf-handling code -- no integration yet
Aug 25, 2020
e463cfa
tuf: use confined_target_dirs
Aug 28, 2020
bd97b78
Expose USER_DATA_DIR in locations
Sep 2, 2020
0e922df
SessionCommandMixin: Add get_tuf_updaters()
Sep 2, 2020
2246d9b
tuf: Make the cache handling more consistent
Sep 2, 2020
08a8718
TUF: Download distribution files with TUF
Sep 8, 2020
37f642b
TUF: Implement index file downloads
Sep 10, 2020
23be41c
TUF: Add Updater.__str__()
Sep 13, 2020
484ed5a
tuf: Workaround the mirror config issues
Sep 14, 2020
7aecaeb
tuf: set log level to ERROR
Sep 16, 2020
17ceb69
Rename, refactor
Sep 30, 2020
dcba9db
secure_updater: Split distribution url more robustly
Oct 1, 2020
a1758ab
self version check: Fix to work with SecureUpdateSession
Oct 1, 2020
63ffca3
secure_update: Handle --no-cache situation
Oct 1, 2020
760a32a
secure_update: Include initial metadata with pip
Oct 1, 2020
2c17f1d
secure_update: Add sanity check to target_name parsing
Oct 2, 2020
f211e71
Support secure update in download and wheel commands
Sep 24, 2020
d038df1
secure_update: Avoid logger.info() if debug() will do
Oct 2, 2020
0807bdf
Silence most output on failing index download
Sep 24, 2020
f55ae72
TUF: raise NetWorkConnectionError on failing download
Sep 24, 2020
3316cc5
secure_update: Handle UnknownTargetError
Oct 2, 2020
44e35f6
secure_update: Log the same things as regular download
Oct 2, 2020
77b76f1
secure_update: Add belt-and-suspenders check for pypi
Oct 2, 2020
8416c3a
secure_update: Improve comments
Oct 2, 2020
4492ffa
Fix all linter issues in secure_update related code
Oct 2, 2020
cf6ff71
list: support SecureUpdateSession
Oct 3, 2020
e2e6d87
Improve comments in SessionCommandMixin
Oct 5, 2020
457084d
secure_update: Improve docstrings and comments
Oct 5, 2020
a4b02e5
Standardize parsing index url from project url
Oct 9, 2020
da72ae7
Update secure updater bootstrap metadata
Oct 9, 2020
031da9d
LinkCollector: rename fetch_page -> fetch_project_page
Oct 9, 2020
0d4ddc0
secure_update: Clean up if metadata bootstrap fails
Oct 9, 2020
a34211b
secure_update: Remove broken url canonicalization
Oct 9, 2020
52a93c6
prepare: link.comes_from can be None
Oct 14, 2020
04d6a33
secure_update: Allow datadir=None argument
Oct 14, 2020
6214c85
options.cache_dir can be False
Oct 14, 2020
098a9d4
secure_update: No real need to use the tuf.log module
Oct 14, 2020
6a57c11
tests: Fix most test secure_update test fails
Oct 14, 2020
4033662
secure_update: Use MissingLocalRepositoryError
Oct 9, 2020
a42b4aa
secure_update: Update mirror configs to TUF 0.15
Oct 17, 2020
9d9175d
Vendor tuf and securesystemslib
Oct 12, 2020
0277f50
Add the vendored sources for tuf and securesystemslib
Oct 12, 2020
ba4f8f8
Fix linter issues
Oct 23, 2020
9bbd0e8
Merge branch 'tuf-mvp' into tuf-mvp-vendored
Oct 23, 2020
70353a5
Fix linter issues
Oct 23, 2020
4e424e3
vendoring: Update tuf files
Oct 23, 2020
e4d385f
test_build_env: Handle SecureUpdateSession
Oct 23, 2020
07db95e
Merge branch 'tuf-mvp' into tuf-mvp-vendored
Oct 23, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/html/development/architecture/package-finding.rst
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ class's main method is the ``collect_links()`` method. The :ref:`PackageFinder
<package-finder-class>` class invokes this method as the first step of its
``find_all_candidates()`` method.

``LinkCollector`` also has a ``fetch_page()`` method to fetch the HTML from a
``LinkCollector`` also has a ``fetch_project_page()`` method to fetch the HTML from a
project page URL. This method is "unintelligent" in that it doesn't parse the
HTML.

Expand Down
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ drop = [
"setuptools",
"pkg_resources/_vendor/",
"pkg_resources/extern/",
# tuf parts that are not needed by updater client
"tuf/api/*",
"tuf/scripts/*",
"tuf/developer_tool.py",
"tuf/repository_tool.py",
"tuf/repository_lib.py",
"tuf/unittest_toolbox.py",
# No need for gpg support in securesystemslib
"securesystemslib/gpg/*",
]

[tool.vendoring.typing-stubs]
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ def get_version(rel_path):
exclude=["contrib", "docs", "tests*", "tasks"],
),
package_data={
"pip._internal.network": [
"secure_update_bootstrap/*/*"
],
"pip._vendor": ["vendor.txt"],
"pip._vendor.certifi": ["*.pem"],
"pip._vendor.requests": ["*.pem"],
Expand Down
33 changes: 30 additions & 3 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
from pip._internal.exceptions import CommandError, PreviousBuildDirError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
from pip._internal.locations import USER_DATA_DIR
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.network.secure_update import SecureUpdateSession
from pip._internal.network.session import PipSession
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.constructors import (
Expand Down Expand Up @@ -47,12 +49,15 @@
class SessionCommandMixin(CommandContextMixIn):

"""
A class mixin for command classes needing _build_session().
A class mixin for command classes needing a PipSession and/or
a SecureUpdateSession: these are commands that need to connect to
a repository.
"""
def __init__(self):
# type: () -> None
super(SessionCommandMixin, self).__init__()
self._session = None # Optional[PipSession]
self._secure_update_session = None # type: Optional[SecureUpdateSession]

@classmethod
def _get_index_urls(cls, options):
Expand Down Expand Up @@ -119,6 +124,21 @@ def _build_session(self, options, retries=None, timeout=None):

return session

def get_secure_update_session(self, options):
# type: (Values) -> SecureUpdateSession
"""Get a SecureUpdateSession singleton."""

if self._secure_update_session is None:
index_urls = self._get_index_urls(options)
cache_dir = options.cache_dir if options.cache_dir else None
self._secure_update_session = SecureUpdateSession(
index_urls, USER_DATA_DIR, cache_dir
)
logger.debug("Initialized secure update session (TUF): %s",
str(self._secure_update_session))

return self._secure_update_session


class IndexGroupCommand(Command, SessionCommandMixin):

Expand Down Expand Up @@ -147,8 +167,9 @@ def handle_pip_version_check(self, options):
retries=0,
timeout=min(5, options.timeout)
)
secure_update_session = self.get_secure_update_session(options)
with session:
pip_self_version_check(session, options)
pip_self_version_check(session, secure_update_session, options)


KEEPABLE_TEMPDIR_TYPES = [
Expand Down Expand Up @@ -200,6 +221,7 @@ def make_requirement_preparer(
options, # type: Values
req_tracker, # type: RequirementTracker
session, # type: PipSession
secure_update_session, # type: SecureUpdateSession
finder, # type: PackageFinder
use_user_site, # type: bool
download_dir=None, # type: str
Expand Down Expand Up @@ -232,6 +254,7 @@ def make_requirement_preparer(
req_tracker=req_tracker,
session=session,
progress_bar=options.progress_bar,
secure_update_session=secure_update_session,
finder=finder,
require_hashes=options.require_hashes,
use_user_site=use_user_site,
Expand Down Expand Up @@ -383,6 +406,7 @@ def _build_package_finder(
self,
options, # type: Values
session, # type: PipSession
secure_update_session, # type: SecureUpdateSession
target_python=None, # type: Optional[TargetPython]
ignore_requires_python=None, # type: Optional[bool]
):
Expand All @@ -393,7 +417,10 @@ def _build_package_finder(
:param ignore_requires_python: Whether to ignore incompatible
"Requires-Python" values in links. Defaults to False.
"""
link_collector = LinkCollector.create(session, options=options)
link_collector = LinkCollector.create(
session=session,
secure_update_session=secure_update_session,
options=options)
selection_prefs = SelectionPreferences(
allow_yanked=True,
format_control=options.format_control,
Expand Down
3 changes: 3 additions & 0 deletions src/pip/_internal/commands/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@ def run(self, options, args):
ensure_dir(options.download_dir)

session = self.get_default_session(options)
secure_update_session = self.get_secure_update_session(options)

target_python = make_target_python(options)
finder = self._build_package_finder(
options=options,
session=session,
secure_update_session=secure_update_session,
target_python=target_python,
)
build_delete = (not (options.no_clean or options.build_dir))
Expand All @@ -115,6 +117,7 @@ def run(self, options, args):
options=options,
req_tracker=req_tracker,
session=session,
secure_update_session=secure_update_session,
finder=finder,
download_dir=options.download_dir,
use_user_site=False,
Expand Down
3 changes: 3 additions & 0 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,13 @@ def run(self, options, args):
global_options = options.global_options or []

session = self.get_default_session(options)
secure_update_session = self.get_secure_update_session(options)

target_python = make_target_python(options)
finder = self._build_package_finder(
options=options,
session=session,
secure_update_session=secure_update_session,
target_python=target_python,
ignore_requires_python=options.ignore_requires_python,
)
Expand Down Expand Up @@ -301,6 +303,7 @@ def run(self, options, args):
options=options,
req_tracker=req_tracker,
session=session,
secure_update_session=secure_update_session,
finder=finder,
use_user_site=options.use_user_site,
)
Expand Down
17 changes: 15 additions & 2 deletions src/pip/_internal/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,12 @@ def _build_package_finder(self, options, session):
"""
Create a package finder appropriate to this list command.
"""
link_collector = LinkCollector.create(session, options=options)
secure_update_session = self.get_secure_update_session(options)
link_collector = LinkCollector.create(
session=session,
secure_update_session=secure_update_session,
options=options
)

# Pass allow_yanked=False to ignore yanked versions.
selection_prefs = SelectionPreferences(
Expand Down Expand Up @@ -225,7 +230,15 @@ def latest_info(dist):
dist.latest_filetype = typ
return dist

for dist in map_multithread(latest_info, packages):
# NOTE: TUF is not currently threadsafe: do not run multithreaded
# if we have any SecureUpdaters
secure_update_session = self.get_secure_update_session(options)
if secure_update_session.downloaders:
map_func = map
else:
map_func = map_multithread # type: ignore

for dist in map_func(latest_info, packages):
if dist is not None:
yield dist

Expand Down
4 changes: 3 additions & 1 deletion src/pip/_internal/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ def run(self, options, args):
cmdoptions.check_install_build_global(options)

session = self.get_default_session(options)
secure_update_session = self.get_secure_update_session(options)

finder = self._build_package_finder(options, session)
finder = self._build_package_finder(options, session, secure_update_session)
build_delete = (not (options.no_clean or options.build_dir))
wheel_cache = WheelCache(options.cache_dir, options.format_control)

Expand All @@ -137,6 +138,7 @@ def run(self, options, args):
options=options,
req_tracker=req_tracker,
session=session,
secure_update_session=secure_update_session,
finder=finder,
download_dir=options.wheel_dir,
use_user_site=False,
Expand Down
42 changes: 35 additions & 7 deletions src/pip/_internal/index/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

from pip._vendor.requests import Response

from pip._internal.network.secure_update import SecureUpdateSession
from pip._internal.network.session import PipSession

HTMLElement = xml.etree.ElementTree.Element
Expand Down Expand Up @@ -582,16 +583,18 @@ class LinkCollector(object):

def __init__(
self,
session, # type: PipSession
search_scope, # type: SearchScope
session, # type: PipSession
secure_update_session, # type: SecureUpdateSession
search_scope, # type: SearchScope
):
# type: (...) -> None
self.search_scope = search_scope
self.session = session
self.secure_update_session = secure_update_session

@classmethod
def create(cls, session, options, suppress_no_index=False):
# type: (PipSession, Values, bool) -> LinkCollector
def create(cls, session, secure_update_session, options, suppress_no_index=False):
# type: (PipSession, SecureUpdateSession, Values, bool) -> LinkCollector
"""
:param session: The Session to use to make requests.
:param suppress_no_index: Whether to ignore the --no-index option
Expand All @@ -612,7 +615,9 @@ def create(cls, session, options, suppress_no_index=False):
find_links=find_links, index_urls=index_urls,
)
link_collector = LinkCollector(
session=session, search_scope=search_scope,
session=session,
secure_update_session=secure_update_session,
search_scope=search_scope,
)
return link_collector

Expand All @@ -621,12 +626,35 @@ def find_links(self):
# type: () -> List[str]
return self.search_scope.find_links

def fetch_page(self, location):
def fetch_project_page(self, location):
# type: (Link) -> Optional[HTMLPage]
"""
Fetch an HTML page containing package links.
"""
return _get_html_page(location, session=self.session)
# check if secure update (TUF) should be used: parse url to find the
# index url, then see if we have a secure updater for the index url
index_url, _, project = location.url.rstrip('/').rpartition('/')
if not project:
raise ValueError(
'Failed to parse {} as project index URL'.format(location.url)
)

downloader = self.secure_update_session.get_downloader(index_url)
if downloader:
logger.debug('SecureDownloader found: %s', str(downloader))
index_file = downloader.download_index(project)
if index_file is None:
return None
else:
with open(index_file, "rb") as f:
return HTMLPage(
content=f.read(),
encoding=None,
url=location.url, # TODO should this be the real URL?
cache_link_parsing=False)
else:
logger.debug('SecureDownloader not found for %s', index_url)
return _get_html_page(location, session=self.session)

def collect_links(self, project_name):
# type: (str) -> CollectedLinks
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/index/package_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ def process_project_url(self, project_url, link_evaluator):
logger.debug(
'Fetching project page and analyzing links: %s', project_url,
)
html_page = self._link_collector.fetch_page(project_url)
html_page = self._link_collector.fetch_project_page(project_url)
if html_page is None:
return []

Expand Down
1 change: 1 addition & 0 deletions src/pip/_internal/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

# Application Directories
USER_CACHE_DIR = appdirs.user_cache_dir("pip")
USER_DATA_DIR = appdirs.user_data_dir("pip")


def get_major_minor_version():
Expand Down
Loading