mirror of
https://github.com/python/cpython.git
synced 2025-12-31 04:23:37 +00:00
[3.14] gh-136134: smtplib: fix CRAM-MD5 on FIPS-only environments (GH-136623) (#138086)
This commit is contained in:
parent
460265fe96
commit
bfce393614
3 changed files with 65 additions and 9 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue