Skip to content

Commit 21fd62a

Browse files
committed
[3.13] pythongh-136134: smtplib: fix CRAM-MD5 on FIPS-only environments (pythonGH-136623)
(cherry picked from commit 766614f) Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent 57b5baf commit 21fd62a

File tree

3 files changed

+64
-10
lines changed

3 files changed

+64
-10
lines changed

Lib/smtplib.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,15 @@ def _quote_periods(bindata):
179179
def _fix_eols(data):
180180
return re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data)
181181

182+
183+
try:
184+
hmac.digest(b'', b'', 'md5')
185+
except ValueError:
186+
_have_cram_md5_support = False
187+
else:
188+
_have_cram_md5_support = True
189+
190+
182191
try:
183192
import ssl
184193
except ImportError:
@@ -667,8 +676,11 @@ def auth_cram_md5(self, challenge=None):
667676
# CRAM-MD5 does not support initial-response.
668677
if challenge is None:
669678
return None
670-
return self.user + " " + hmac.HMAC(
671-
self.password.encode('ascii'), challenge, 'md5').hexdigest()
679+
if not _have_cram_md5_support:
680+
raise SMTPException("CRAM-MD5 is not supported")
681+
password = self.password.encode('ascii')
682+
authcode = hmac.HMAC(password, challenge, 'md5')
683+
return f"{self.user} {authcode.hexdigest()}"
672684

673685
def auth_plain(self, challenge=None):
674686
""" Authobject to use with PLAIN authentication. Requires self.user and
@@ -720,9 +732,10 @@ def login(self, user, password, *, initial_response_ok=True):
720732
advertised_authlist = self.esmtp_features["auth"].split()
721733

722734
# Authentication methods we can handle in our preferred order:
723-
preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
724-
725-
# We try the supported authentications in our preferred order, if
735+
if _have_cram_md5_support:
736+
preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
737+
else:
738+
preferred_auths = ['PLAIN', 'LOGIN']
726739
# the server supports them.
727740
authlist = [auth for auth in preferred_auths
728741
if auth in advertised_authlist]

Lib/test/test_smtplib.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import threading
1818

1919
import unittest
20+
import unittest.mock as mock
2021
from test import support, mock_socket
2122
from test.support import hashlib_helper
2223
from test.support import socket_helper
@@ -926,11 +927,14 @@ def _auth_cram_md5(self, arg=None):
926927
except ValueError as e:
927928
self.push('535 Splitting response {!r} into user and password '
928929
'failed: {}'.format(logpass, e))
929-
return False
930-
valid_hashed_pass = hmac.HMAC(
931-
sim_auth[1].encode('ascii'),
932-
self._decode_base64(sim_cram_md5_challenge).encode('ascii'),
933-
'md5').hexdigest()
930+
return
931+
pwd = sim_auth[1].encode('ascii')
932+
msg = self._decode_base64(sim_cram_md5_challenge).encode('ascii')
933+
try:
934+
valid_hashed_pass = hmac.HMAC(pwd, msg, 'md5').hexdigest()
935+
except ValueError:
936+
self.push('504 CRAM-MD5 is not supported')
937+
return
934938
self._authenticated(user, hashed_pass == valid_hashed_pass)
935939
# end AUTH related stuff.
936940

@@ -1181,6 +1185,38 @@ def testAUTH_CRAM_MD5(self):
11811185
self.assertEqual(resp, (235, b'Authentication Succeeded'))
11821186
smtp.close()
11831187

1188+
@mock.patch("smtplib._have_cram_md5_support", False)
1189+
def testAUTH_CRAM_MD5_blocked(self, hmac_constructor):
1190+
# CRAM-MD5 is the only "known" method by the server,
1191+
# but it is not supported by the client. In particular,
1192+
# no challenge will ever be sent.
1193+
self.serv.add_feature("AUTH CRAM-MD5")
1194+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
1195+
timeout=support.LOOPBACK_TIMEOUT)
1196+
self.addCleanup(smtp.close)
1197+
msg = re.escape("No suitable authentication method found.")
1198+
with self.assertRaisesRegex(smtplib.SMTPException, msg):
1199+
smtp.login(sim_auth[0], sim_auth[1])
1200+
hmac_constructor.assert_not_called() # call has been bypassed
1201+
1202+
@mock.patch("smtplib._have_cram_md5_support", False)
1203+
def testAUTH_CRAM_MD5_blocked_and_fallback(self):
1204+
# Test that PLAIN is tried after CRAM-MD5 failed
1205+
self.serv.add_feature("AUTH CRAM-MD5 PLAIN")
1206+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
1207+
timeout=support.LOOPBACK_TIMEOUT)
1208+
self.addCleanup(smtp.close)
1209+
with (
1210+
mock.patch.object(smtp, "auth_cram_md5") as smtp_auth_cram_md5,
1211+
mock.patch.object(
1212+
smtp, "auth_plain", wraps=smtp.auth_plain
1213+
) as smtp_auth_plain
1214+
):
1215+
resp = smtp.login(sim_auth[0], sim_auth[1])
1216+
smtp_auth_plain.assert_called_once()
1217+
smtp_auth_cram_md5.assert_not_called()
1218+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1219+
11841220
@hashlib_helper.requires_hashdigest('md5', openssl=True)
11851221
def testAUTH_multiple(self):
11861222
# Test that multiple authentication methods are tried.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:meth:`!SMTP.auth_cram_md5` now raises an :exc:`~smtplib.SMTPException`
2+
instead of a :exc:`ValueError` if Python has been built without MD5 support.
3+
In particular, :class:`~smtplib.SMTP` clients will not attempt to use this
4+
method even if the remote server is assumed to support it. Patch by Bénédikt
5+
Tran.

0 commit comments

Comments
 (0)