Skip to content

Commit c64a1a6

Browse files
tiranmiss-islington
authored andcommitted
bpo-38270: Check for hash digest algorithms and avoid MD5 (GH-16382)
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
1 parent 417089e commit c64a1a6

File tree

8 files changed

+119
-45
lines changed

8 files changed

+119
-45
lines changed

Lib/test/support/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import functools
1313
import gc
1414
import glob
15+
import hashlib
1516
import importlib
1617
import importlib.util
1718
import locale
@@ -648,6 +649,29 @@ def wrapper(*args, **kw):
648649
return decorator
649650

650651

652+
def requires_hashdigest(digestname):
653+
"""Decorator raising SkipTest if a hashing algorithm is not available
654+
655+
The hashing algorithm could be missing or blocked by a strict crypto
656+
policy.
657+
658+
ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS
659+
ValueError: unsupported hash type md4
660+
"""
661+
def decorator(func):
662+
@functools.wraps(func)
663+
def wrapper(*args, **kwargs):
664+
try:
665+
hashlib.new(digestname)
666+
except ValueError:
667+
raise unittest.SkipTest(
668+
f"hash digest '{digestname}' is not available."
669+
)
670+
return func(*args, **kwargs)
671+
return wrapper
672+
return decorator
673+
674+
651675
HOST = "localhost"
652676
HOSTv4 = "127.0.0.1"
653677
HOSTv6 = "::1"

Lib/test/test_hmac.py

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import unittest.mock
77
import warnings
88

9+
from test.support import requires_hashdigest
10+
911

1012
def ignore_warning(func):
1113
@functools.wraps(func)
@@ -19,6 +21,7 @@ def wrapper(*args, **kwargs):
1921

2022
class TestVectorsTestCase(unittest.TestCase):
2123

24+
@requires_hashdigest('md5')
2225
def test_md5_vectors(self):
2326
# Test the HMAC module against test vectors from the RFC.
2427

@@ -76,6 +79,7 @@ def md5test(key, data, digest):
7679
b"and Larger Than One Block-Size Data"),
7780
"6f630fad67cda0ee1fb1f562db3aa53e")
7881

82+
@requires_hashdigest('sha1')
7983
def test_sha_vectors(self):
8084
def shatest(key, data, digest):
8185
h = hmac.HMAC(key, data, digestmod=hashlib.sha1)
@@ -268,23 +272,28 @@ def hmactest(key, data, hexdigests):
268272
'134676fb6de0446065c97440fa8c6a58',
269273
})
270274

275+
@requires_hashdigest('sha224')
271276
def test_sha224_rfc4231(self):
272277
self._rfc4231_test_cases(hashlib.sha224, 'sha224', 28, 64)
273278

279+
@requires_hashdigest('sha256')
274280
def test_sha256_rfc4231(self):
275281
self._rfc4231_test_cases(hashlib.sha256, 'sha256', 32, 64)
276282

283+
@requires_hashdigest('sha384')
277284
def test_sha384_rfc4231(self):
278285
self._rfc4231_test_cases(hashlib.sha384, 'sha384', 48, 128)
279286

287+
@requires_hashdigest('sha512')
280288
def test_sha512_rfc4231(self):
281289
self._rfc4231_test_cases(hashlib.sha512, 'sha512', 64, 128)
282290

291+
@requires_hashdigest('sha256')
283292
def test_legacy_block_size_warnings(self):
284293
class MockCrazyHash(object):
285294
"""Ain't no block_size attribute here."""
286295
def __init__(self, *args):
287-
self._x = hashlib.sha1(*args)
296+
self._x = hashlib.sha256(*args)
288297
self.digest_size = self._x.digest_size
289298
def update(self, v):
290299
self._x.update(v)
@@ -308,77 +317,92 @@ def test_with_digestmod_no_default(self):
308317
data = b"Hi There"
309318
hmac.HMAC(key, data, digestmod=None)
310319

320+
311321
class ConstructorTestCase(unittest.TestCase):
312322

323+
expected = (
324+
"6c845b47f52b3b47f6590c502db7825aad757bf4fadc8fa972f7cd2e76a5bdeb"
325+
)
326+
327+
@requires_hashdigest('sha256')
313328
def test_normal(self):
314329
# Standard constructor call.
315-
failed = 0
316330
try:
317-
h = hmac.HMAC(b"key", digestmod='md5')
331+
hmac.HMAC(b"key", digestmod='sha256')
318332
except Exception:
319333
self.fail("Standard constructor call raised exception.")
320334

335+
@requires_hashdigest('sha256')
321336
def test_with_str_key(self):
322337
# Pass a key of type str, which is an error, because it expects a key
323338
# of type bytes
324339
with self.assertRaises(TypeError):
325-
h = hmac.HMAC("key", digestmod='md5')
340+
h = hmac.HMAC("key", digestmod='sha256')
326341

342+
@requires_hashdigest('sha256')
327343
def test_dot_new_with_str_key(self):
328344
# Pass a key of type str, which is an error, because it expects a key
329345
# of type bytes
330346
with self.assertRaises(TypeError):
331-
h = hmac.new("key", digestmod='md5')
347+
h = hmac.new("key", digestmod='sha256')
332348

349+
@requires_hashdigest('sha256')
333350
def test_withtext(self):
334351
# Constructor call with text.
335352
try:
336-
h = hmac.HMAC(b"key", b"hash this!", digestmod='md5')
353+
h = hmac.HMAC(b"key", b"hash this!", digestmod='sha256')
337354
except Exception:
338355
self.fail("Constructor call with text argument raised exception.")
339-
self.assertEqual(h.hexdigest(), '34325b639da4cfd95735b381e28cb864')
356+
self.assertEqual(h.hexdigest(), self.expected)
340357

358+
@requires_hashdigest('sha256')
341359
def test_with_bytearray(self):
342360
try:
343361
h = hmac.HMAC(bytearray(b"key"), bytearray(b"hash this!"),
344-
digestmod="md5")
362+
digestmod="sha256")
345363
except Exception:
346364
self.fail("Constructor call with bytearray arguments raised exception.")
347-
self.assertEqual(h.hexdigest(), '34325b639da4cfd95735b381e28cb864')
365+
self.assertEqual(h.hexdigest(), self.expected)
348366

367+
@requires_hashdigest('sha256')
349368
def test_with_memoryview_msg(self):
350369
try:
351-
h = hmac.HMAC(b"key", memoryview(b"hash this!"), digestmod="md5")
370+
h = hmac.HMAC(b"key", memoryview(b"hash this!"), digestmod="sha256")
352371
except Exception:
353372
self.fail("Constructor call with memoryview msg raised exception.")
354-
self.assertEqual(h.hexdigest(), '34325b639da4cfd95735b381e28cb864')
373+
self.assertEqual(h.hexdigest(), self.expected)
355374

375+
@requires_hashdigest('sha256')
356376
def test_withmodule(self):
357377
# Constructor call with text and digest module.
358378
try:
359-
h = hmac.HMAC(b"key", b"", hashlib.sha1)
379+
h = hmac.HMAC(b"key", b"", hashlib.sha256)
360380
except Exception:
361-
self.fail("Constructor call with hashlib.sha1 raised exception.")
381+
self.fail("Constructor call with hashlib.sha256 raised exception.")
382+
362383

363384
class SanityTestCase(unittest.TestCase):
364385

386+
@requires_hashdigest('sha256')
365387
def test_exercise_all_methods(self):
366388
# Exercising all methods once.
367389
# This must not raise any exceptions
368390
try:
369-
h = hmac.HMAC(b"my secret key", digestmod="md5")
391+
h = hmac.HMAC(b"my secret key", digestmod="sha256")
370392
h.update(b"compute the hash of this text!")
371393
dig = h.digest()
372394
dig = h.hexdigest()
373395
h2 = h.copy()
374396
except Exception:
375397
self.fail("Exception raised during normal usage of HMAC class.")
376398

399+
377400
class CopyTestCase(unittest.TestCase):
378401

402+
@requires_hashdigest('sha256')
379403
def test_attributes(self):
380404
# Testing if attributes are of same type.
381-
h1 = hmac.HMAC(b"key", digestmod="md5")
405+
h1 = hmac.HMAC(b"key", digestmod="sha256")
382406
h2 = h1.copy()
383407
self.assertTrue(h1.digest_cons == h2.digest_cons,
384408
"digest constructors don't match.")
@@ -387,9 +411,10 @@ def test_attributes(self):
387411
self.assertEqual(type(h1.outer), type(h2.outer),
388412
"Types of outer don't match.")
389413

414+
@requires_hashdigest('sha256')
390415
def test_realcopy(self):
391416
# Testing if the copy method created a real copy.
392-
h1 = hmac.HMAC(b"key", digestmod="md5")
417+
h1 = hmac.HMAC(b"key", digestmod="sha256")
393418
h2 = h1.copy()
394419
# Using id() in case somebody has overridden __eq__/__ne__.
395420
self.assertTrue(id(h1) != id(h2), "No real copy of the HMAC instance.")
@@ -398,9 +423,10 @@ def test_realcopy(self):
398423
self.assertTrue(id(h1.outer) != id(h2.outer),
399424
"No real copy of the attribute 'outer'.")
400425

426+
@requires_hashdigest('sha256')
401427
def test_equality(self):
402428
# Testing if the copy has the same digests.
403-
h1 = hmac.HMAC(b"key", digestmod="md5")
429+
h1 = hmac.HMAC(b"key", digestmod="sha256")
404430
h1.update(b"some random text")
405431
h2 = h1.copy()
406432
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
@@ -10,7 +10,8 @@
1010
import socket
1111

1212
from test.support import (reap_threads, verbose, transient_internet,
13-
run_with_tz, run_with_locale, cpython_only)
13+
run_with_tz, run_with_locale, cpython_only,
14+
requires_hashdigest)
1415
import unittest
1516
from unittest import mock
1617
from datetime import datetime, timezone, timedelta
@@ -370,6 +371,7 @@ def cmd_AUTHENTICATE(self, tag, args):
370371
self.assertEqual(code, 'OK')
371372
self.assertEqual(server.response, b'ZmFrZQ==\r\n') # b64 encoded 'fake'
372373

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

392+
@requires_hashdigest('md5')
390393
def test_login_cram_md5_plain_text(self):
391394
class AuthHandler(SimpleIMAPHandler):
392395
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
@@ -309,9 +309,11 @@ def test_noop(self):
309309
def test_rpop(self):
310310
self.assertOK(self.client.rpop('foo'))
311311

312+
@test_support.requires_hashdigest('md5')
312313
def test_apop_normal(self):
313314
self.assertOK(self.client.apop('foo', 'dummypassword'))
314315

316+
@test_support.requires_hashdigest('md5')
315317
def test_apop_REDOS(self):
316318
# Replace welcome with very long evil welcome.
317319
# 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
@@ -21,6 +22,7 @@
2122
from test import support, mock_socket
2223
from test.support import HOST
2324
from test.support import threading_setup, threading_cleanup, join_thread
25+
from test.support import requires_hashdigest
2426
from unittest.mock import Mock
2527

2628

@@ -1009,6 +1011,7 @@ def testAUTH_LOGIN(self):
10091011
self.assertEqual(resp, (235, b'Authentication Succeeded'))
10101012
smtp.close()
10111013

1014+
@requires_hashdigest('md5')
10121015
def testAUTH_CRAM_MD5(self):
10131016
self.serv.add_feature("AUTH CRAM-MD5")
10141017
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
@@ -1025,7 +1028,13 @@ def testAUTH_multiple(self):
10251028
smtp.close()
10261029

10271030
def test_auth_function(self):
1028-
supported = {'CRAM-MD5', 'PLAIN', 'LOGIN'}
1031+
supported = {'PLAIN', 'LOGIN'}
1032+
try:
1033+
hashlib.md5()
1034+
except ValueError:
1035+
pass
1036+
else:
1037+
supported.add('CRAM-MD5')
10291038
for mechanism in supported:
10301039
self.serv.add_feature("AUTH {}".format(mechanism))
10311040
for mechanism in supported:

0 commit comments

Comments
 (0)