Skip to content

bpo-40698: Improve distutils upload hash digests #20260

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

Merged
merged 1 commit into from
May 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,13 @@ and :meth:`~datetime.datetime.isocalendar()` of :class:`datetime.datetime`
methods now returns a :func:`~collections.namedtuple` instead of a :class:`tuple`.
(Contributed by Dong-hee Na in :issue:`24416`.)

distutils
---------

The :command:`upload` command now creates SHA2-256 and Blake2b-256 hash
digests. It skips MD5 on platforms that block MD5 digest.
(Contributed by Christian Heimes in :issue:`40698`.)

fcntl
-----

Expand Down
22 changes: 21 additions & 1 deletion Lib/distutils/command/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@
from distutils.spawn import spawn
from distutils import log


# PyPI Warehouse supports MD5, SHA256, and Blake2 (blake2-256)
# https://bugs.python.org/issue40698
_FILE_CONTENT_DIGESTS = {
"md5_digest": getattr(hashlib, "md5", None),
"sha256_digest": getattr(hashlib, "sha256", None),
"blake2_256_digest": getattr(hashlib, "blake2b", None),
}


class upload(PyPIRCCommand):

description = "upload binary package to PyPI"
Expand Down Expand Up @@ -87,6 +97,7 @@ def upload_file(self, command, pyversion, filename):
content = f.read()
finally:
f.close()

meta = self.distribution.metadata
data = {
# action
Expand All @@ -101,7 +112,6 @@ def upload_file(self, command, pyversion, filename):
'content': (os.path.basename(filename),content),
'filetype': command,
'pyversion': pyversion,
'md5_digest': hashlib.md5(content).hexdigest(),

# additional meta-data
'metadata_version': '1.0',
Expand All @@ -123,6 +133,16 @@ def upload_file(self, command, pyversion, filename):

data['comment'] = ''

# file content digests
for digest_name, digest_cons in _FILE_CONTENT_DIGESTS.items():
if digest_cons is None:
continue
try:
data[digest_name] = digest_cons(content).hexdigest()
except ValueError:
# hash digest not available or blocked by security policy
pass

if self.sign:
with open(filename + ".asc", "rb") as f:
data['gpg_signature'] = (os.path.basename(filename) + ".asc",
Expand Down
24 changes: 20 additions & 4 deletions Lib/distutils/tests/test_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,30 @@ def test_upload(self):

# what did we send ?
headers = dict(self.last_open.req.headers)
self.assertEqual(headers['Content-length'], '2162')
self.assertGreaterEqual(int(headers['Content-length']), 2162)
content_type = headers['Content-type']
self.assertTrue(content_type.startswith('multipart/form-data'))
self.assertEqual(self.last_open.req.get_method(), 'POST')
expected_url = 'https://upload.pypi.org/legacy/'
self.assertEqual(self.last_open.req.get_full_url(), expected_url)
self.assertTrue(b'xxx' in self.last_open.req.data)
self.assertIn(b'protocol_version', self.last_open.req.data)
data = self.last_open.req.data
self.assertIn(b'xxx',data)
self.assertIn(b'protocol_version', data)
self.assertIn(b'sha256_digest', data)
self.assertIn(
b'cd2eb0837c9b4c962c22d2ff8b5441b7b45805887f051d39bf133b583baf'
b'6860',
data
)
if b'md5_digest' in data:
self.assertIn(b'f561aaf6ef0bf14d4208bb46a4ccb3ad', data)
if b'blake2_256_digest' in data:
self.assertIn(
b'b6f289a27d4fe90da63c503bfe0a9b761a8f76bb86148565065f040be'
b'6d1c3044cf7ded78ef800509bccb4b648e507d88dc6383d67642aadcc'
b'ce443f1534330a',
data
)

# The PyPI response body was echoed
results = self.get_logs(INFO)
Expand Down Expand Up @@ -166,7 +182,7 @@ def test_upload_correct_cr(self):
cmd.run()

headers = dict(self.last_open.req.headers)
self.assertEqual(headers['Content-length'], '2172')
self.assertGreaterEqual(int(headers['Content-length']), 2172)
self.assertIn(b'long description\r', self.last_open.req.data)

def test_upload_fails(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:mod:`distutils` upload creates SHA2-256 and Blake2b-256 digests. MD5
digests is skipped if platform blocks MD5.