Skip to content

Commit a1cd61b

Browse files
committed
Support immutable VCS references in hash-checking mode.
1 parent 88ac144 commit a1cd61b

File tree

5 files changed

+90
-21
lines changed

5 files changed

+90
-21
lines changed

docs/html/topics/secure-installs.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ FooProject == 1.2 \
4343

4444
This prevents a surprising hash mismatch upon the release of a new version that matches the requirement specifier.
4545

46+
```{versionadded} 23.2
47+
VCS URLs that reference an commit hash are now supported in hash checking mode,
48+
as pip considers the VCS provides the required source immutability guarantees. This is only
49+
supported with `git` URLs at the time of writing.
50+
```
51+
4652
### Forcing Hash-checking mode
4753

4854
It is possible to force the hash checking mode to be enabled, by passing `--require-hashes` command-line option.

news/6469.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Validate VCS urls in hash-checking mode using their commit hashes.

src/pip/_internal/exceptions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,32 @@ class VcsHashUnsupported(HashError):
501501
)
502502

503503

504+
class CacheEntryTypeHashNotSupported(HashError):
505+
"""A wheel cache entry was build from a URL that does not support hash checking."""
506+
507+
order = 0
508+
head = (
509+
"Can't verify hashes for these cached requirements because they are "
510+
"from a URL that does not support hash checking:"
511+
)
512+
513+
514+
class DependencyVcsHashNotSupported(HashError):
515+
order = 0
516+
head = (
517+
"Can't verify hashes for these VCS requirements because they are "
518+
"not user supplied, so we don't assume their VCS ref is trusted:"
519+
)
520+
521+
522+
class MutableVcsRefHashNotSupported(HashError):
523+
order = 0
524+
head = (
525+
"Can't verify hashes for these VCS requirements because their ref "
526+
"is not immutable:"
527+
)
528+
529+
504530
class DirectoryUrlHashUnsupported(HashError):
505531
"""A hash was provided for a version-control-system-based requirement, but
506532
we don't have a method for hashing those."""

src/pip/_internal/operations/prepare.py

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@
1515
from pip._internal.distributions import make_distribution_for_install_requirement
1616
from pip._internal.distributions.installed import InstalledDistribution
1717
from pip._internal.exceptions import (
18+
CacheEntryTypeHashNotSupported,
19+
DependencyVcsHashNotSupported,
1820
DirectoryUrlHashUnsupported,
1921
HashMismatch,
2022
HashUnpinned,
2123
InstallationError,
2224
MetadataInconsistent,
25+
MutableVcsRefHashNotSupported,
2326
NetworkConnectionError,
2427
PreviousBuildDirError,
2528
VcsHashUnsupported,
2629
)
2730
from pip._internal.index.package_finder import PackageFinder
2831
from pip._internal.metadata import BaseDistribution, get_metadata_distribution
29-
from pip._internal.models.direct_url import ArchiveInfo
32+
from pip._internal.models.direct_url import ArchiveInfo, VcsInfo
3033
from pip._internal.models.link import Link
3134
from pip._internal.models.wheel import Wheel
3235
from pip._internal.network.download import BatchDownloader, Downloader
@@ -41,7 +44,7 @@
4144
direct_url_for_editable,
4245
direct_url_from_link,
4346
)
44-
from pip._internal.utils.hashes import Hashes, MissingHashes
47+
from pip._internal.utils.hashes import Hashes, MissingHashes, VcsHashes
4548
from pip._internal.utils.logging import indent_log
4649
from pip._internal.utils.misc import (
4750
display_path,
@@ -72,10 +75,14 @@ def _get_prepared_distribution(
7275
return abstract_dist.get_metadata_distribution()
7376

7477

75-
def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None:
78+
def unpack_vcs_link(
79+
link: Link, location: str, verbosity: int, hashes: Optional[Hashes] = None
80+
) -> None:
7681
vcs_backend = vcs.get_backend_for_scheme(link.scheme)
7782
assert vcs_backend is not None
7883
vcs_backend.unpack(location, url=hide_url(link.url), verbosity=verbosity)
84+
if hashes and not vcs_backend.is_immutable_rev_checkout(link.url, location):
85+
raise MutableVcsRefHashNotSupported()
7986

8087

8188
class File:
@@ -152,7 +159,7 @@ def unpack_url(
152159
"""
153160
# non-editable vcs urls
154161
if link.is_vcs:
155-
unpack_vcs_link(link, location, verbosity=verbosity)
162+
unpack_vcs_link(link, location, verbosity=verbosity, hashes=hashes)
156163
return None
157164

158165
assert not link.is_existing_dir()
@@ -335,6 +342,14 @@ def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:
335342
# and raise some more informative errors than otherwise.
336343
# (For example, we can raise VcsHashUnsupported for a VCS URL
337344
# rather than HashMissing.)
345+
346+
# Check that --hash is not used with VCS and local directories direct URLs.
347+
if req.original_link:
348+
if req.original_link.is_vcs and req.hashes(trust_internet=False):
349+
raise VcsHashUnsupported()
350+
if req.original_link.is_existing_dir() and req.hashes(trust_internet=False):
351+
raise DirectoryUrlHashUnsupported()
352+
338353
if not self.require_hashes:
339354
return req.hashes(trust_internet=True)
340355

@@ -343,7 +358,9 @@ def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:
343358
# report less-useful error messages for unhashable
344359
# requirements, complaining that there's no hash provided.
345360
if req.link.is_vcs:
346-
raise VcsHashUnsupported()
361+
if not req.user_supplied:
362+
raise DependencyVcsHashNotSupported()
363+
return VcsHashes()
347364
if req.link.is_existing_dir():
348365
raise DirectoryUrlHashUnsupported()
349366

@@ -559,24 +576,33 @@ def _prepare_linked_requirement(
559576
assert link.is_file
560577
# We need to verify hashes, and we have found the requirement in the cache
561578
# of locally built wheels.
562-
if (
563-
isinstance(req.download_info.info, ArchiveInfo)
564-
and req.download_info.info.hashes
565-
and hashes.has_one_of(req.download_info.info.hashes)
566-
):
567-
# At this point we know the requirement was built from a hashable source
568-
# artifact, and we verified that the cache entry's hash of the original
569-
# artifact matches one of the hashes we expect. We don't verify hashes
570-
# against the cached wheel, because the wheel is not the original.
579+
if isinstance(req.download_info.info, ArchiveInfo):
580+
if req.download_info.info.hashes and hashes.has_one_of(
581+
req.download_info.info.hashes
582+
):
583+
# At this point we know the requirement was built from a hashable
584+
# source artifact, and we verified that the cache entry's hash of
585+
# the original artifact matches one of the hashes we expect. We
586+
# don't verify hashes against the cached wheel, because the wheel is
587+
# not the original.
588+
hashes = None
589+
else:
590+
logger.warning(
591+
"The hashes of the source archive found in cache entry "
592+
"don't match, ignoring cached built wheel "
593+
"and re-downloading source."
594+
)
595+
req.link = req.cached_wheel_source_link
596+
link = req.link
597+
elif isinstance(req.download_info.info, VcsInfo):
598+
if not req.user_supplied:
599+
raise DependencyVcsHashNotSupported()
600+
# Don't verify hashes against the cached wheel: if it is in cache,
601+
# it means it was built from a URL referencing an immutable commit
602+
# hash.
571603
hashes = None
572604
else:
573-
logger.warning(
574-
"The hashes of the source archive found in cache entry "
575-
"don't match, ignoring cached built wheel "
576-
"and re-downloading source."
577-
)
578-
req.link = req.cached_wheel_source_link
579-
link = req.link
605+
raise CacheEntryTypeHashNotSupported()
580606

581607
self._ensure_link_req_src_dir(req, parallel_builds)
582608

src/pip/_internal/utils/hashes.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,13 @@ def __init__(self) -> None:
149149

150150
def _raise(self, gots: Dict[str, "_Hash"]) -> "NoReturn":
151151
raise HashMissing(gots[FAVORITE_HASH].hexdigest())
152+
153+
154+
class VcsHashes(MissingHashes):
155+
"""A workalike for Hashes used for VCS references
156+
157+
It never matches, and is used as a sentinel to indicate that we should
158+
check the VCS reference is an immutable commit reference.
159+
"""
160+
161+
pass

0 commit comments

Comments
 (0)