[3.14] gh-136134: imaplib: fix CRAM-MD5 on FIPS-only environments (GH-136615) (#138054)

This commit is contained in:
Bénédikt Tran 2025-09-04 16:59:49 +02:00 committed by GitHub
parent bfce393614
commit d574f83206
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 49 additions and 31 deletions

View file

@ -413,6 +413,9 @@ An :class:`IMAP4` instance has the following methods:
the password. Will only work if the server ``CAPABILITY`` response includes the the password. Will only work if the server ``CAPABILITY`` response includes the
phrase ``AUTH=CRAM-MD5``. phrase ``AUTH=CRAM-MD5``.
.. versionchanged:: next
An :exc:`IMAP4.error` is raised if MD5 support is not available.
.. method:: IMAP4.logout() .. method:: IMAP4.logout()

View file

@ -21,7 +21,7 @@
# GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005. # GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
# IDLE contributed by Forest <forestix@nom.one> August 2024. # IDLE contributed by Forest <forestix@nom.one> August 2024.
__version__ = "2.59" __version__ = "2.60"
import binascii, errno, random, re, socket, subprocess, sys, time, calendar import binascii, errno, random, re, socket, subprocess, sys, time, calendar
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
@ -725,9 +725,17 @@ def login_cram_md5(self, user, password):
def _CRAM_MD5_AUTH(self, challenge): def _CRAM_MD5_AUTH(self, challenge):
""" Authobject to use with CRAM-MD5 authentication. """ """ Authobject to use with CRAM-MD5 authentication. """
import hmac import hmac
pwd = (self.password.encode('utf-8') if isinstance(self.password, str)
else self.password) if isinstance(self.password, str):
return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest() password = self.password.encode('utf-8')
else:
password = self.password
try:
authcode = hmac.HMAC(password, challenge, 'md5')
except ValueError: # HMAC-MD5 is not available
raise self.error("CRAM-MD5 authentication is not supported")
return f"{self.user} {authcode.hexdigest()}"
def logout(self): def logout(self):

View file

@ -256,7 +256,20 @@ def cmd_IDLE(self, tag, args):
self._send_tagged(tag, 'BAD', 'Expected DONE') self._send_tagged(tag, 'BAD', 'Expected DONE')
class NewIMAPTestsMixin(): class AuthHandler_CRAM_MD5(SimpleIMAPHandler):
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5'
def cmd_AUTHENTICATE(self, tag, args):
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
'VzdG9uLm1jaS5uZXQ=')
r = yield
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
else:
self._send_tagged(tag, 'NO', 'No access')
class NewIMAPTestsMixin:
client = None client = None
def _setup(self, imap_handler, connect=True): def _setup(self, imap_handler, connect=True):
@ -439,40 +452,31 @@ def cmd_AUTHENTICATE(self, tag, args):
@hashlib_helper.requires_hashdigest('md5', openssl=True) @hashlib_helper.requires_hashdigest('md5', openssl=True)
def test_login_cram_md5_bytes(self): def test_login_cram_md5_bytes(self):
class AuthHandler(SimpleIMAPHandler): client, _ = self._setup(AuthHandler_CRAM_MD5)
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5' self.assertIn('AUTH=CRAM-MD5', client.capabilities)
def cmd_AUTHENTICATE(self, tag, args):
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
'VzdG9uLm1jaS5uZXQ=')
r = yield
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
else:
self._send_tagged(tag, 'NO', 'No access')
client, _ = self._setup(AuthHandler)
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
ret, _ = client.login_cram_md5("tim", b"tanstaaftanstaaf") ret, _ = client.login_cram_md5("tim", b"tanstaaftanstaaf")
self.assertEqual(ret, "OK") self.assertEqual(ret, "OK")
@hashlib_helper.requires_hashdigest('md5', openssl=True) @hashlib_helper.requires_hashdigest('md5', openssl=True)
def test_login_cram_md5_plain_text(self): def test_login_cram_md5_plain_text(self):
class AuthHandler(SimpleIMAPHandler): client, _ = self._setup(AuthHandler_CRAM_MD5)
capabilities = 'LOGINDISABLED AUTH=CRAM-MD5' self.assertIn('AUTH=CRAM-MD5', client.capabilities)
def cmd_AUTHENTICATE(self, tag, args):
self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm'
'VzdG9uLm1jaS5uZXQ=')
r = yield
if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT'
b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'):
self._send_tagged(tag, 'OK', 'CRAM-MD5 successful')
else:
self._send_tagged(tag, 'NO', 'No access')
client, _ = self._setup(AuthHandler)
self.assertTrue('AUTH=CRAM-MD5' in client.capabilities)
ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf") ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf")
self.assertEqual(ret, "OK") self.assertEqual(ret, "OK")
def test_login_cram_md5_blocked(self):
def side_effect(*a, **kw):
raise ValueError
client, _ = self._setup(AuthHandler_CRAM_MD5)
self.assertIn('AUTH=CRAM-MD5', client.capabilities)
msg = re.escape("CRAM-MD5 authentication is not supported")
with (
mock.patch("hmac.HMAC", side_effect=side_effect),
self.assertRaisesRegex(imaplib.IMAP4.error, msg)
):
client.login_cram_md5("tim", b"tanstaaftanstaaf")
def test_aborted_authentication(self): def test_aborted_authentication(self):
class MyServer(SimpleIMAPHandler): class MyServer(SimpleIMAPHandler):
def cmd_AUTHENTICATE(self, tag, args): def cmd_AUTHENTICATE(self, tag, args):

View file

@ -0,0 +1,3 @@
:meth:`IMAP4.login_cram_md5 <imaplib.IMAP4.login_cram_md5>` now raises an
:exc:`IMAP4.error <imaplib.IMAP4.error>` if CRAM-MD5 authentication is not
supported. Patch by Bénédikt Tran.