From e572c7f6dbe5397153803eab256e4a4ca3384f80 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 20 May 2020 16:37:25 +0200 Subject: [PATCH] bpo-40698: Improve distutils upload hash digests (GH-20260) - Fix upload test on systems that blocks MD5 - Add SHA2-256 and Blake2b-256 digests based on new Warehous and twine specs. Signed-off-by: Christian Heimes --- Doc/whatsnew/3.9.rst | 7 ++++++ Lib/distutils/command/upload.py | 22 ++++++++++++++++- Lib/distutils/tests/test_upload.py | 24 +++++++++++++++---- .../2020-05-20-14-38-04.bpo-40698.zwl5Hc.rst | 2 ++ 4 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-05-20-14-38-04.bpo-40698.zwl5Hc.rst diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 86458a39df2..a483b19d151 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -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 ----- diff --git a/Lib/distutils/command/upload.py b/Lib/distutils/command/upload.py index d822ba01338..95e9fda186f 100644 --- a/Lib/distutils/command/upload.py +++ b/Lib/distutils/command/upload.py @@ -16,6 +16,16 @@ from distutils.core import PyPIRCCommand 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" @@ -87,6 +97,7 @@ class upload(PyPIRCCommand): content = f.read() finally: f.close() + meta = self.distribution.metadata data = { # action @@ -101,7 +112,6 @@ class upload(PyPIRCCommand): 'content': (os.path.basename(filename),content), 'filetype': command, 'pyversion': pyversion, - 'md5_digest': hashlib.md5(content).hexdigest(), # additional meta-data 'metadata_version': '1.0', @@ -123,6 +133,16 @@ class upload(PyPIRCCommand): 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", diff --git a/Lib/distutils/tests/test_upload.py b/Lib/distutils/tests/test_upload.py index c17d8e7d54e..bca5516d2f7 100644 --- a/Lib/distutils/tests/test_upload.py +++ b/Lib/distutils/tests/test_upload.py @@ -130,14 +130,30 @@ class uploadTestCase(BasePyPIRCCommandTestCase): # 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) @@ -166,7 +182,7 @@ class uploadTestCase(BasePyPIRCCommandTestCase): 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): diff --git a/Misc/NEWS.d/next/Library/2020-05-20-14-38-04.bpo-40698.zwl5Hc.rst b/Misc/NEWS.d/next/Library/2020-05-20-14-38-04.bpo-40698.zwl5Hc.rst new file mode 100644 index 00000000000..e57624819d5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-20-14-38-04.bpo-40698.zwl5Hc.rst @@ -0,0 +1,2 @@ +:mod:`distutils` upload creates SHA2-256 and Blake2b-256 digests. MD5 +digests is skipped if platform blocks MD5.