Skip to content

Commit 3afc263

Browse files
authored
[3.13] gh-136134: imaplib: fix CRAM-MD5 on FIPS-only environments (GH-136615) (#138055)
(cherry picked from commit 4519b8a)
1 parent b9179c3 commit 3afc263

File tree

4 files changed

+48
-30
lines changed

4 files changed

+48
-30
lines changed

Doc/library/imaplib.rst

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

328+
.. versionchanged:: next
329+
An :exc:`IMAP4.error` is raised if MD5 support is not available.
330+
328331

329332
.. method:: IMAP4.logout()
330333

Lib/imaplib.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -635,9 +635,17 @@ def login_cram_md5(self, user, password):
635635
def _CRAM_MD5_AUTH(self, challenge):
636636
""" Authobject to use with CRAM-MD5 authentication. """
637637
import hmac
638-
pwd = (self.password.encode('utf-8') if isinstance(self.password, str)
639-
else self.password)
640-
return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest()
638+
639+
if isinstance(self.password, str):
640+
password = self.password.encode('utf-8')
641+
else:
642+
password = self.password
643+
644+
try:
645+
authcode = hmac.HMAC(password, challenge, 'md5')
646+
except ValueError: # HMAC-MD5 is not available
647+
raise self.error("CRAM-MD5 authentication is not supported")
648+
return f"{self.user} {authcode.hexdigest()}"
641649

642650

643651
def logout(self):

Lib/test/test_imaplib.py

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,20 @@ def cmd_UNSELECT(self, tag, args):
208208
self._send_tagged(tag, 'BAD', 'No mailbox selected')
209209

210210

211-
class NewIMAPTestsMixin():
211+
class AuthHandler_CRAM_MD5(SimpleIMAPHandler):
212+
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
213+
def cmd_AUTHENTICATE(self, tag, args):
214+
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
215+
'VzdG9uLm1jaS5uZXQ=')
216+
r = yield
217+
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
218+
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
219+
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
220+
else:
221+
self._send_tagged(tag, 'NO', 'No access')
222+
223+
224+
class NewIMAPTestsMixin:
212225
client = None
213226

214227
def _setup(self, imap_handler, connect=True):
@@ -391,40 +404,31 @@ def cmd_AUTHENTICATE(self, tag, args):
391404

392405
@hashlib_helper.requires_hashdigest('md5', openssl=True)
393406
def test_login_cram_md5_bytes(self):
394-
class AuthHandler(SimpleIMAPHandler):
395-
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
396-
def cmd_AUTHENTICATE(self, tag, args):
397-
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
398-
'VzdG9uLm1jaS5uZXQ=')
399-
r = yield
400-
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
401-
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
402-
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
403-
else:
404-
self._send_tagged(tag, 'NO', 'No access')
405-
client, _ = self._setup(AuthHandler)
406-
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
407+
client, _ = self._setup(AuthHandler_CRAM_MD5)
408+
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
407409
ret, _ = client.login_cram_md5("tim", b"tanstaaftanstaaf")
408410
self.assertEqual(ret, "OK")
409411

410412
@hashlib_helper.requires_hashdigest('md5', openssl=True)
411413
def test_login_cram_md5_plain_text(self):
412-
class AuthHandler(SimpleIMAPHandler):
413-
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
414-
def cmd_AUTHENTICATE(self, tag, args):
415-
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
416-
'VzdG9uLm1jaS5uZXQ=')
417-
r = yield
418-
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
419-
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
420-
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
421-
else:
422-
self._send_tagged(tag, 'NO', 'No access')
423-
client, _ = self._setup(AuthHandler)
424-
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
414+
client, _ = self._setup(AuthHandler_CRAM_MD5)
415+
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
425416
ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf")
426417
self.assertEqual(ret, "OK")
427418

419+
def test_login_cram_md5_blocked(self):
420+
def side_effect(*a, **kw):
421+
raise ValueError
422+
423+
client, _ = self._setup(AuthHandler_CRAM_MD5)
424+
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
425+
msg = re.escape("CRAM-MD5 authentication is not supported")
426+
with (
427+
mock.patch("hmac.HMAC", side_effect=side_effect),
428+
self.assertRaisesRegex(imaplib.IMAP4.error, msg)
429+
):
430+
client.login_cram_md5("tim", b"tanstaaftanstaaf")
431+
428432
def test_aborted_authentication(self):
429433
class MyServer(SimpleIMAPHandler):
430434
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)