Skip to content

Commit 6450d07

Browse files
committed
[3.14] pythongh-136134: imaplib: fix CRAM-MD5 on FIPS-only environments (pythonGH-136615)
(cherry picked from commit 4519b8a) Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent 57c4601 commit 6450d07

File tree

4 files changed

+49
-31
lines changed

4 files changed

+49
-31
lines changed

Doc/library/imaplib.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,9 @@ An :class:`IMAP4` instance has the following methods:
413413
the password. Will only work if the server ``CAPABILITY`` response includes the
414414
phrase ``AUTH=CRAM-MD5``.
415415

416+
.. versionchanged:: next
417+
An :exc:`IMAP4.error` is raised if MD5 support is not available.
418+
416419

417420
.. method:: IMAP4.logout()
418421

Lib/imaplib.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
# GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
2222
# IDLE contributed by Forest <forestix@nom.one> August 2024.
2323

24-
__version__ = "2.59"
24+
__version__ = "2.60"
2525

2626
import binascii, errno, random, re, socket, subprocess, sys, time, calendar
2727
from datetime import datetime, timezone, timedelta
@@ -725,9 +725,17 @@ def login_cram_md5(self, user, password):
725725
def _CRAM_MD5_AUTH(self, challenge):
726726
""" Authobject to use with CRAM-MD5 authentication. """
727727
import hmac
728-
pwd = (self.password.encode('utf-8') if isinstance(self.password, str)
729-
else self.password)
730-
return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest()
728+
729+
if isinstance(self.password, str):
730+
password = self.password.encode('utf-8')
731+
else:
732+
password = self.password
733+
734+
try:
735+
authcode = hmac.HMAC(password, challenge, 'md5')
736+
except ValueError: # HMAC-MD5 is not available
737+
raise self.error("CRAM-MD5 authentication is not supported")
738+
return f"{self.user} {authcode.hexdigest()}"
731739

732740

733741
def logout(self):

Lib/test/test_imaplib.py

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,20 @@ def cmd_IDLE(self, tag, args):
256256
self._send_tagged(tag, 'BAD', 'Expected DONE')
257257

258258

259-
class NewIMAPTestsMixin():
259+
class AuthHandler_CRAM_MD5(SimpleIMAPHandler):
260+
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
261+
def cmd_AUTHENTICATE(self, tag, args):
262+
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
263+
'VzdG9uLm1jaS5uZXQ=')
264+
r = yield
265+
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
266+
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
267+
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
268+
else:
269+
self._send_tagged(tag, 'NO', 'No access')
270+
271+
272+
class NewIMAPTestsMixin:
260273
client = None
261274

262275
def _setup(self, imap_handler, connect=True):
@@ -439,40 +452,31 @@ def cmd_AUTHENTICATE(self, tag, args):
439452

440453
@hashlib_helper.requires_hashdigest('md5', openssl=True)
441454
def test_login_cram_md5_bytes(self):
442-
class AuthHandler(SimpleIMAPHandler):
443-
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
444-
def cmd_AUTHENTICATE(self, tag, args):
445-
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
446-
'VzdG9uLm1jaS5uZXQ=')
447-
r = yield
448-
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
449-
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
450-
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
451-
else:
452-
self._send_tagged(tag, 'NO', 'No access')
453-
client, _ = self._setup(AuthHandler)
454-
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
455+
client, _ = self._setup(AuthHandler_CRAM_MD5)
456+
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
455457
ret, _ = client.login_cram_md5("tim", b"tanstaaftanstaaf")
456458
self.assertEqual(ret, "OK")
457459

458460
@hashlib_helper.requires_hashdigest('md5', openssl=True)
459461
def test_login_cram_md5_plain_text(self):
460-
class AuthHandler(SimpleIMAPHandler):
461-
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
462-
def cmd_AUTHENTICATE(self, tag, args):
463-
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
464-
'VzdG9uLm1jaS5uZXQ=')
465-
r = yield
466-
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
467-
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
468-
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
469-
else:
470-
self._send_tagged(tag, 'NO', 'No access')
471-
client, _ = self._setup(AuthHandler)
472-
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
462+
client, _ = self._setup(AuthHandler_CRAM_MD5)
463+
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
473464
ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf")
474465
self.assertEqual(ret, "OK")
475466

467+
def test_login_cram_md5_blocked(self):
468+
def side_effect(*a, **kw):
469+
raise ValueError
470+
471+
client, _ = self._setup(AuthHandler_CRAM_MD5)
472+
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
473+
msg = re.escape("CRAM-MD5 authentication is not supported")
474+
with (
475+
mock.patch("hmac.HMAC", side_effect=side_effect),
476+
self.assertRaisesRegex(imaplib.IMAP4.error, msg)
477+
):
478+
client.login_cram_md5("tim", b"tanstaaftanstaaf")
479+
476480
def test_aborted_authentication(self):
477481
class MyServer(SimpleIMAPHandler):
478482
def cmd_AUTHENTICATE(self, tag, args):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:meth:`IMAP4.login_cram_md5 <imaplib.IMAP4.login_cram_md5>` now raises an
2+
:exc:`IMAP4.error <imaplib.IMAP4.error>` if CRAM-MD5 authentication is not
3+
supported. Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)