[3.14] gh-136134: smtplib: fix CRAM-MD5 on FIPS-only environments (GH-136623) (#138086)

This commit is contained in:
Bénédikt Tran 2025-09-04 15:07:59 +02:00 committed by GitHub
parent 460265fe96
commit bfce393614
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 65 additions and 9 deletions

View file

@ -177,6 +177,15 @@ def _quote_periods(bindata):
def _fix_eols(data):
return re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data)
try:
hmac.digest(b'', b'', 'md5')
except ValueError:
_have_cram_md5_support = False
else:
_have_cram_md5_support = True
try:
import ssl
except ImportError:
@ -665,8 +674,11 @@ def auth_cram_md5(self, challenge=None):
# CRAM-MD5 does not support initial-response.
if challenge is None:
return None
return self.user + " " + hmac.HMAC(
self.password.encode('ascii'), challenge, 'md5').hexdigest()
if not _have_cram_md5_support:
raise SMTPException("CRAM-MD5 is not supported")
password = self.password.encode('ascii')
authcode = hmac.HMAC(password, challenge, 'md5')
return f"{self.user} {authcode.hexdigest()}"
def auth_plain(self, challenge=None):
""" Authobject to use with PLAIN authentication. Requires self.user and
@ -718,8 +730,10 @@ def login(self, user, password, *, initial_response_ok=True):
advertised_authlist = self.esmtp_features["auth"].split()
# Authentication methods we can handle in our preferred order:
preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
if _have_cram_md5_support:
preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
else:
preferred_auths = ['PLAIN', 'LOGIN']
# We try the supported authentications in our preferred order, if
# the server supports them.
authlist = [auth for auth in preferred_auths

View file

@ -17,6 +17,7 @@
import threading
import unittest
import unittest.mock as mock
from test import support, mock_socket
from test.support import hashlib_helper
from test.support import socket_helper
@ -926,11 +927,14 @@ def _auth_cram_md5(self, arg=None):
except ValueError as e:
self.push('535 Splitting response {!r} into user and password '
'failed: {}'.format(logpass, e))
return False
valid_hashed_pass = hmac.HMAC(
sim_auth[1].encode('ascii'),
self._decode_base64(sim_cram_md5_challenge).encode('ascii'),
'md5').hexdigest()
return
pwd = sim_auth[1].encode('ascii')
msg = self._decode_base64(sim_cram_md5_challenge).encode('ascii')
try:
valid_hashed_pass = hmac.HMAC(pwd, msg, 'md5').hexdigest()
except ValueError:
self.push('504 CRAM-MD5 is not supported')
return
self._authenticated(user, hashed_pass == valid_hashed_pass)
# end AUTH related stuff.
@ -1181,6 +1185,39 @@ def testAUTH_CRAM_MD5(self):
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()
@mock.patch("hmac.HMAC")
@mock.patch("smtplib._have_cram_md5_support", False)
def testAUTH_CRAM_MD5_blocked(self, hmac_constructor):
# CRAM-MD5 is the only "known" method by the server,
# but it is not supported by the client. In particular,
# no challenge will ever be sent.
self.serv.add_feature("AUTH CRAM-MD5")
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=support.LOOPBACK_TIMEOUT)
self.addCleanup(smtp.close)
msg = re.escape("No suitable authentication method found.")
with self.assertRaisesRegex(smtplib.SMTPException, msg):
smtp.login(sim_auth[0], sim_auth[1])
hmac_constructor.assert_not_called() # call has been bypassed
@mock.patch("smtplib._have_cram_md5_support", False)
def testAUTH_CRAM_MD5_blocked_and_fallback(self):
# Test that PLAIN is tried after CRAM-MD5 failed
self.serv.add_feature("AUTH CRAM-MD5 PLAIN")
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
timeout=support.LOOPBACK_TIMEOUT)
self.addCleanup(smtp.close)
with (
mock.patch.object(smtp, "auth_cram_md5") as smtp_auth_cram_md5,
mock.patch.object(
smtp, "auth_plain", wraps=smtp.auth_plain
) as smtp_auth_plain
):
resp = smtp.login(sim_auth[0], sim_auth[1])
smtp_auth_plain.assert_called_once()
smtp_auth_cram_md5.assert_not_called() # no call to HMAC constructor
self.assertEqual(resp, (235, b'Authentication Succeeded'))
@hashlib_helper.requires_hashdigest('md5', openssl=True)
def testAUTH_multiple(self):
# Test that multiple authentication methods are tried.

View file

@ -0,0 +1,5 @@
:meth:`!SMTP.auth_cram_md5` now raises an :exc:`~smtplib.SMTPException`
instead of a :exc:`ValueError` if Python has been built without MD5 support.
In particular, :class:`~smtplib.SMTP` clients will not attempt to use this
method even if the remote server is assumed to support it. Patch by Bénédikt
Tran.