Skip to content

Commit f12e7cb

Browse files
authored
[3.13] gh-136134: smtplib: fix CRAM-MD5 on FIPS-only environments (GH-136623) (#138087)
(cherry picked from commit 766614f) (cherry picked from commit ab1bef8)
1 parent 3afc263 commit f12e7cb

File tree

3 files changed

+65
-9
lines changed

3 files changed

+65
-9
lines changed

Lib/smtplib.py

Lines changed: 18 additions & 4 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,8 +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-
735+
if _have_cram_md5_support:
736+
preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
737+
else:
738+
preferred_auths = ['PLAIN', 'LOGIN']
725739
# We try the supported authentications in our preferred order, if
726740
# the server supports them.
727741
authlist = [auth for auth in preferred_auths

Lib/test/test_smtplib.py

Lines changed: 42 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,39 @@ def testAUTH_CRAM_MD5(self):
11811185
self.assertEqual(resp, (235, b'Authentication Succeeded'))
11821186
smtp.close()
11831187

1188+
@mock.patch("hmac.HMAC")
1189+
@mock.patch("smtplib._have_cram_md5_support", False)
1190+
def testAUTH_CRAM_MD5_blocked(self, hmac_constructor):
1191+
# CRAM-MD5 is the only "known" method by the server,
1192+
# but it is not supported by the client. In particular,
1193+
# no challenge will ever be sent.
1194+
self.serv.add_feature("AUTH CRAM-MD5")
1195+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
1196+
timeout=support.LOOPBACK_TIMEOUT)
1197+
self.addCleanup(smtp.close)
1198+
msg = re.escape("No suitable authentication method found.")
1199+
with self.assertRaisesRegex(smtplib.SMTPException, msg):
1200+
smtp.login(sim_auth[0], sim_auth[1])
1201+
hmac_constructor.assert_not_called() # call has been bypassed
1202+
1203+
@mock.patch("smtplib._have_cram_md5_support", False)
1204+
def testAUTH_CRAM_MD5_blocked_and_fallback(self):
1205+
# Test that PLAIN is tried after CRAM-MD5 failed
1206+
self.serv.add_feature("AUTH CRAM-MD5 PLAIN")
1207+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
1208+
timeout=support.LOOPBACK_TIMEOUT)
1209+
self.addCleanup(smtp.close)
1210+
with (
1211+
mock.patch.object(smtp, "auth_cram_md5") as smtp_auth_cram_md5,
1212+
mock.patch.object(
1213+
smtp, "auth_plain", wraps=smtp.auth_plain
1214+
) as smtp_auth_plain
1215+
):
1216+
resp = smtp.login(sim_auth[0], sim_auth[1])
1217+
smtp_auth_plain.assert_called_once()
1218+
smtp_auth_cram_md5.assert_not_called() # no call to HMAC constructor
1219+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1220+
11841221
@hashlib_helper.requires_hashdigest('md5', openssl=True)
11851222
def testAUTH_multiple(self):
11861223
# 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)