Skip to content

Commit 53d108c

Browse files
miss-islingtonstratakis
authored andcommitted
[3.8] bpo-38270: Check for hash digest algorithms and avoid MD5 (pythonGH-16382) (pythonGH-16393)
Make it easier to run and test Python on systems with restrict crypto policies: * add requires_hashdigest to test.support to check if a hash digest algorithm is available and working * avoid MD5 in test_hmac * replace MD5 with SHA256 in test_tarfile * mark network tests that require MD5 for MD5-based digest auth or CRAM-MD5 https://bugs.python.org/issue38270 (cherry picked from commit c64a1a6) Co-authored-by: Christian Heimes <[email protected]> https://bugs.python.org/issue38270 Automerge-Triggered-By: @tiran
1 parent 7a3d450 commit 53d108c

File tree

7 files changed

+112
-53
lines changed

7 files changed

+112
-53
lines changed

Lib/test/support/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import fnmatch
1212
import functools
1313
import gc
14+
import hashlib
1415
import importlib
1516
import importlib.util
1617
import io
@@ -627,6 +628,27 @@ def wrapper(*args, **kw):
627628
return wrapper
628629
return decorator
629630

631+
def requires_hashdigest(digestname):
632+
"""Decorator raising SkipTest if a hashing algorithm is not available
633+
634+
The hashing algorithm could be missing or blocked by a strict crypto
635+
policy.
636+
637+
ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS
638+
ValueError: unsupported hash type md4
639+
"""
640+
def decorator(func):
641+
@functools.wraps(func)
642+
def wrapper(*args, **kwargs):
643+
try:
644+
hashlib.new(digestname)
645+
except ValueError:
646+
raise unittest.SkipTest(
647+
f"hash digest '{digestname}' is not available."
648+
)
649+
return func(*args, **kwargs)
650+
return wrapper
651+
return decorator
630652

631653
# Don't use "localhost", since resolving it uses the DNS under recent
632654
# Windows versions (see issue #18792).

Lib/test/test_hmac.py

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import unittest
55
import warnings
66

7+
from test.support import requires_hashdigest
8+
79

810
def ignore_warning(func):
911
@functools.wraps(func)
@@ -17,6 +19,7 @@ def wrapper(*args, **kwargs):
1719

1820
class TestVectorsTestCase(unittest.TestCase):
1921

22+
@requires_hashdigest('md5')
2023
def test_md5_vectors(self):
2124
# Test the HMAC module against test vectors from the RFC.
2225

@@ -63,6 +66,7 @@ def md5test(key, data, digest):
6366
b"and Larger Than One Block-Size Data"),
6467
"6f630fad67cda0ee1fb1f562db3aa53e")
6568

69+
@requires_hashdigest('sha1')
6670
def test_sha_vectors(self):
6771
def shatest(key, data, digest):
6872
h = hmac.HMAC(key, data, digestmod=hashlib.sha1)
@@ -230,23 +234,28 @@ def hmactest(key, data, hexdigests):
230234
'134676fb6de0446065c97440fa8c6a58',
231235
})
232236

237+
@requires_hashdigest('sha224')
233238
def test_sha224_rfc4231(self):
234239
self._rfc4231_test_cases(hashlib.sha224, 'sha224', 28, 64)
235240

241+
@requires_hashdigest('sha256')
236242
def test_sha256_rfc4231(self):
237243
self._rfc4231_test_cases(hashlib.sha256, 'sha256', 32, 64)
238244

245+
@requires_hashdigest('sha384')
239246
def test_sha384_rfc4231(self):
240247
self._rfc4231_test_cases(hashlib.sha384, 'sha384', 48, 128)
241248

249+
@requires_hashdigest('sha512')
242250
def test_sha512_rfc4231(self):
243251
self._rfc4231_test_cases(hashlib.sha512, 'sha512', 64, 128)
244252

253+
@requires_hashdigest('sha256')
245254
def test_legacy_block_size_warnings(self):
246255
class MockCrazyHash(object):
247256
"""Ain't no block_size attribute here."""
248257
def __init__(self, *args):
249-
self._x = hashlib.sha1(*args)
258+
self._x = hashlib.sha256(*args)
250259
self.digest_size = self._x.digest_size
251260
def update(self, v):
252261
self._x.update(v)
@@ -273,88 +282,94 @@ def test_with_digestmod_warning(self):
273282
self.assertEqual(h.hexdigest().upper(), digest)
274283

275284

285+
276286
class ConstructorTestCase(unittest.TestCase):
277287

288+
expected = (
289+
"6c845b47f52b3b47f6590c502db7825aad757bf4fadc8fa972f7cd2e76a5bdeb"
290+
)
278291
@ignore_warning
292+
@requires_hashdigest('sha256')
279293
def test_normal(self):
280294
# Standard constructor call.
281-
failed = 0
282295
try:
283-
h = hmac.HMAC(b"key")
296+
hmac.HMAC(b"key", digestmod='sha256')
284297
except Exception:
285298
self.fail("Standard constructor call raised exception.")
286299

287300
@ignore_warning
301+
@requires_hashdigest('sha256')
288302
def test_with_str_key(self):
289303
# Pass a key of type str, which is an error, because it expects a key
290304
# of type bytes
291305
with self.assertRaises(TypeError):
292-
h = hmac.HMAC("key")
306+
h = hmac.HMAC("key", digestmod='sha256')
293307

294-
@ignore_warning
308+
@requires_hashdigest('sha256')
295309
def test_dot_new_with_str_key(self):
296310
# Pass a key of type str, which is an error, because it expects a key
297311
# of type bytes
298312
with self.assertRaises(TypeError):
299-
h = hmac.new("key")
313+
h = hmac.HMAC("key", digestmod='sha256')
300314

301315
@ignore_warning
316+
@requires_hashdigest('sha256')
302317
def test_withtext(self):
303318
# Constructor call with text.
304319
try:
305-
h = hmac.HMAC(b"key", b"hash this!")
320+
h = hmac.HMAC(b"key", b"hash this!", digestmod='sha256')
306321
except Exception:
307322
self.fail("Constructor call with text argument raised exception.")
308-
self.assertEqual(h.hexdigest(), '34325b639da4cfd95735b381e28cb864')
323+
self.assertEqual(h.hexdigest(), self.expected)
309324

325+
@requires_hashdigest('sha256')
310326
def test_with_bytearray(self):
311327
try:
312328
h = hmac.HMAC(bytearray(b"key"), bytearray(b"hash this!"),
313-
digestmod="md5")
329+
digestmod="sha256")
314330
except Exception:
315331
self.fail("Constructor call with bytearray arguments raised exception.")
316-
self.assertEqual(h.hexdigest(), '34325b639da4cfd95735b381e28cb864')
332+
self.assertEqual(h.hexdigest(), self.expected)
317333

334+
@requires_hashdigest('sha256')
318335
def test_with_memoryview_msg(self):
319336
try:
320-
h = hmac.HMAC(b"key", memoryview(b"hash this!"), digestmod="md5")
337+
h = hmac.HMAC(b"key", memoryview(b"hash this!"), digestmod="sha256")
321338
except Exception:
322339
self.fail("Constructor call with memoryview msg raised exception.")
323-
self.assertEqual(h.hexdigest(), '34325b639da4cfd95735b381e28cb864')
340+
self.assertEqual(h.hexdigest(), self.expected)
324341

342+
@requires_hashdigest('sha256')
325343
def test_withmodule(self):
326344
# Constructor call with text and digest module.
327345
try:
328-
h = hmac.HMAC(b"key", b"", hashlib.sha1)
346+
h = hmac.HMAC(b"key", b"", hashlib.sha256)
329347
except Exception:
330-
self.fail("Constructor call with hashlib.sha1 raised exception.")
348+
self.fail("Constructor call with hashlib.sha256 raised exception.")
331349

332-
class SanityTestCase(unittest.TestCase):
333350

334-
@ignore_warning
335-
def test_default_is_md5(self):
336-
# Testing if HMAC defaults to MD5 algorithm.
337-
# NOTE: this whitebox test depends on the hmac class internals
338-
h = hmac.HMAC(b"key")
339-
self.assertEqual(h.digest_cons, hashlib.md5)
351+
class SanityTestCase(unittest.TestCase):
340352

353+
@requires_hashdigest('sha256')
341354
def test_exercise_all_methods(self):
342355
# Exercising all methods once.
343356
# This must not raise any exceptions
344357
try:
345-
h = hmac.HMAC(b"my secret key", digestmod="md5")
358+
h = hmac.HMAC(b"my secret key", digestmod="sha256")
346359
h.update(b"compute the hash of this text!")
347360
dig = h.digest()
348361
dig = h.hexdigest()
349362
h2 = h.copy()
350363
except Exception:
351364
self.fail("Exception raised during normal usage of HMAC class.")
352365

366+
353367
class CopyTestCase(unittest.TestCase):
354368

369+
@requires_hashdigest('sha256')
355370
def test_attributes(self):
356371
# Testing if attributes are of same type.
357-
h1 = hmac.HMAC(b"key", digestmod="md5")
372+
h1 = hmac.HMAC(b"key", digestmod="sha256")
358373
h2 = h1.copy()
359374
self.assertTrue(h1.digest_cons == h2.digest_cons,
360375
"digest constructors don't match.")
@@ -363,9 +378,10 @@ def test_attributes(self):
363378
self.assertEqual(type(h1.outer), type(h2.outer),
364379
"Types of outer don't match.")
365380

381+
@requires_hashdigest('sha256')
366382
def test_realcopy(self):
367383
# Testing if the copy method created a real copy.
368-
h1 = hmac.HMAC(b"key", digestmod="md5")
384+
h1 = hmac.HMAC(b"key", digestmod="sha256")
369385
h2 = h1.copy()
370386
# Using id() in case somebody has overridden __eq__/__ne__.
371387
self.assertTrue(id(h1) != id(h2), "No real copy of the HMAC instance.")
@@ -374,9 +390,10 @@ def test_realcopy(self):
374390
self.assertTrue(id(h1.outer) != id(h2.outer),
375391
"No real copy of the attribute 'outer'.")
376392

393+
@requires_hashdigest('sha256')
377394
def test_equality(self):
378395
# Testing if the copy has the same digests.
379-
h1 = hmac.HMAC(b"key", digestmod="md5")
396+
h1 = hmac.HMAC(b"key", digestmod="sha256")
380397
h1.update(b"some random text")
381398
h2 = h1.copy()
382399
self.assertEqual(h1.digest(), h2.digest(),

Lib/test/test_imaplib.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
import inspect
1515

1616
from test.support import (reap_threads, verbose, transient_internet,
17-
run_with_tz, run_with_locale, cpython_only)
17+
run_with_tz, run_with_locale, cpython_only,
18+
requires_hashdigest)
1819
import unittest
1920
from unittest import mock
2021
from datetime import datetime, timezone, timedelta
@@ -369,6 +370,7 @@ def cmd_AUTHENTICATE(self, tag, args):
369370
self.assertEqual(code, 'OK')
370371
self.assertEqual(server.response, b'ZmFrZQ==\r\n') # b64 encoded 'fake'
371372

373+
@requires_hashdigest('md5')
372374
def test_login_cram_md5_bytes(self):
373375
class AuthHandler(SimpleIMAPHandler):
374376
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
@@ -386,6 +388,7 @@ def cmd_AUTHENTICATE(self, tag, args):
386388
ret, _ = client.login_cram_md5("tim", b"tanstaaftanstaaf")
387389
self.assertEqual(ret, "OK")
388390

391+
@requires_hashdigest('md5')
389392
def test_login_cram_md5_plain_text(self):
390393
class AuthHandler(SimpleIMAPHandler):
391394
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
@@ -797,6 +800,7 @@ def cmd_AUTHENTICATE(self, tag, args):
797800
b'ZmFrZQ==\r\n') # b64 encoded 'fake'
798801

799802
@reap_threads
803+
@requires_hashdigest('md5')
800804
def test_login_cram_md5(self):
801805

802806
class AuthHandler(SimpleIMAPHandler):

Lib/test/test_poplib.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,9 +304,11 @@ def test_noop(self):
304304
def test_rpop(self):
305305
self.assertOK(self.client.rpop('foo'))
306306

307+
@test_support.requires_hashdigest('md5')
307308
def test_apop_normal(self):
308309
self.assertOK(self.client.apop('foo', 'dummypassword'))
309310

311+
@test_support.requires_hashdigest('md5')
310312
def test_apop_REDOS(self):
311313
# Replace welcome with very long evil welcome.
312314
# NB The upper bound on welcome length is currently 2048.

Lib/test/test_smtplib.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from email.message import EmailMessage
55
from email.base64mime import body_encode as encode_base64
66
import email.utils
7+
import hashlib
78
import hmac
89
import socket
910
import smtpd
@@ -18,6 +19,7 @@
1819

1920
import unittest
2021
from test import support, mock_socket
22+
from test.support import requires_hashdigest
2123
from unittest.mock import Mock
2224

2325
HOST = "localhost"
@@ -968,6 +970,7 @@ def testAUTH_LOGIN(self):
968970
self.assertEqual(resp, (235, b'Authentication Succeeded'))
969971
smtp.close()
970972

973+
@requires_hashdigest('md5')
971974
def testAUTH_CRAM_MD5(self):
972975
self.serv.add_feature("AUTH CRAM-MD5")
973976
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
@@ -984,7 +987,13 @@ def testAUTH_multiple(self):
984987
smtp.close()
985988

986989
def test_auth_function(self):
987-
supported = {'CRAM-MD5', 'PLAIN', 'LOGIN'}
990+
supported = {'PLAIN', 'LOGIN'}
991+
try:
992+
hashlib.md5()
993+
except ValueError:
994+
pass
995+
else:
996+
supported.add('CRAM-MD5')
988997
for mechanism in supported:
989998
self.serv.add_feature("AUTH {}".format(mechanism))
990999
for mechanism in supported:

0 commit comments

Comments
 (0)