From b379ce43aec06420019284acdf7466b3a410bd0b Mon Sep 17 00:00:00 2001 From: Raphael Nogueira Date: Tue, 1 Jul 2025 21:08:49 +0100 Subject: [PATCH 1/4] "gh-136134: Fallback to next auth method when CRAM-MD5 fails due to unsupported hash (e.g. FIPS) --- Lib/smtplib.py | 4 +++ Lib/test/test_smtplib.py | 57 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/Lib/smtplib.py b/Lib/smtplib.py index 84d6d858e7dec1..b3d4691b5290f7 100644 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -743,6 +743,10 @@ def login(self, user, password, *, initial_response_ok=True): return (code, resp) except SMTPAuthenticationError as e: last_exception = e + except ValueError as e: + last_exception = e + if 'unsupported' in str(e).lower(): + continue # We could not login successfully. Return result of last attempt. raise last_exception diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index 4c9fc14bd43f54..bc9ec50d6d4ea1 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -23,7 +23,7 @@ from test.support import threading_helper from test.support import asyncore from test.support import smtpd -from unittest.mock import Mock +from unittest.mock import Mock, patch support.requires_working_socket(module=True) @@ -1570,5 +1570,60 @@ def testAUTH_PLAIN_initial_response_auth(self): self.assertEqual(code, 235) +class TestSMTPLoginValueError(unittest.TestCase): + def broken_hmac(*args, **kwargs): + raise ValueError("[digital envelope routines] unsupported") + + def test_login_raises_valueerror_when_cram_md5_fails(self): + with patch("hmac.HMAC", self.broken_hmac): + class FakeSMTP(smtplib.SMTP): + def __init__(self): + super().__init__(host='', port=0) + self.esmtp_features = {"auth": "CRAM-MD5"} + self._host = "localhost" + + def ehlo_or_helo_if_needed(self): + pass + + def has_extn(self, ext): + return ext.lower() == "auth" + + def docmd(self, *args, **kwargs): + # Retorna uma challenge base64 vĂ¡lida + return 334, b"Y2hhbGxlbmdl" + + smtp = FakeSMTP() + with self.assertRaises(ValueError) as ctx: + smtp.login("user", "pass") + self.assertIn("unsupported", str(ctx.exception).lower()) + + def test_login_fallbacks_when_cram_md5_raises_valueerror(self): + with patch("hmac.HMAC", self.broken_hmac): + class FakeSMTP(smtplib.SMTP): + def __init__(self): + super().__init__(host='', port=0) + self.esmtp_features = {"auth": "CRAM-MD5 LOGIN"} + self._host = "localhost" + + def ehlo_or_helo_if_needed(self): + pass + + def has_extn(self, ext): + return ext.lower() == "auth" + + def docmd(self, *args, **kwargs): + if args[0] == "AUTH" and args[1].startswith("CRAM-MD5"): + return 334, b"Y2hhbGxlbmdl" # base64('challenge') + return 235, b"Authentication successful" + + def auth_login(self, challenge=None): + return "login response" + + smtp = FakeSMTP() + code, resp = smtp.login("user", "pass") + self.assertEqual(code, 235) + self.assertEqual(resp, b"Authentication successful") + + if __name__ == '__main__': unittest.main() From 73014d5e2a0eda7b6bd86517400028350be3bbb6 Mon Sep 17 00:00:00 2001 From: Raphael Nogueira Date: Tue, 1 Jul 2025 21:39:20 +0100 Subject: [PATCH 2/4] gh-136134: Add code comment explaining CRAM-MD5 fallback and NEWS entry --- Lib/smtplib.py | 3 +++ .../Library/2025-07-01-21-37-17.gh-issue-136134.NhFpu3.rst | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-07-01-21-37-17.gh-issue-136134.NhFpu3.rst diff --git a/Lib/smtplib.py b/Lib/smtplib.py index b3d4691b5290f7..d5ab7c42c0eac0 100644 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -744,6 +744,9 @@ def login(self, user, password, *, initial_response_ok=True): except SMTPAuthenticationError as e: last_exception = e except ValueError as e: + # Some environments (e.g., FIPS) disable certain hashing algorithms like MD5, + # which are required by CRAM-MD5. This raises a ValueError when trying to use HMAC. + # If this happens, we catch the exception and continue trying the next auth method. last_exception = e if 'unsupported' in str(e).lower(): continue diff --git a/Misc/NEWS.d/next/Library/2025-07-01-21-37-17.gh-issue-136134.NhFpu3.rst b/Misc/NEWS.d/next/Library/2025-07-01-21-37-17.gh-issue-136134.NhFpu3.rst new file mode 100644 index 00000000000000..e53bbafd092277 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-01-21-37-17.gh-issue-136134.NhFpu3.rst @@ -0,0 +1,2 @@ +Make smtplib fallback to next auth method if CRAM-MD5 fails due to +unsupported hash (e.g. FIPS). Contributed by Raphael Rodrigues. From d2cc3729f9ad7523a1326980391eac5059a93520 Mon Sep 17 00:00:00 2001 From: Raphael Nogueira Date: Wed, 2 Jul 2025 20:52:46 +0100 Subject: [PATCH 3/4] bpo-136134: Raise SMTPAuthHashUnsupportedError when HMAC fails in CRAM-MD5 Wraps the HMAC call in auth_cram_md5 and raises a dedicated exception to avoid relying on error message content. Also updates the relevant tests. --- Lib/smtplib.py | 18 ++++++++++++------ Lib/test/test_smtplib.py | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Lib/smtplib.py b/Lib/smtplib.py index d5ab7c42c0eac0..9f11d27478ece4 100644 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -54,7 +54,7 @@ __all__ = ["SMTPException", "SMTPNotSupportedError", "SMTPServerDisconnected", "SMTPResponseException", "SMTPSenderRefused", "SMTPRecipientsRefused", "SMTPDataError", - "SMTPConnectError", "SMTPHeloError", "SMTPAuthenticationError", + "SMTPConnectError", "SMTPHeloError", "SMTPAuthenticationError", "SMTPAuthHashUnsupportedError", "quoteaddr", "quotedata", "SMTP"] SMTP_PORT = 25 @@ -141,6 +141,10 @@ class SMTPAuthenticationError(SMTPResponseException): combination provided. """ +class SMTPAuthHashUnsupportedError(SMTPException): + """Raised when the authentication mechanism uses a hash algorithm unsupported by the system.""" + + def quoteaddr(addrstring): """Quote a subset of the email addresses defined by RFC 821. @@ -665,8 +669,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() + try: + return self.user + " " + hmac.HMAC( + self.password.encode('ascii'), challenge, 'md5').hexdigest() + except ValueError as e: + raise SMTPAuthHashUnsupportedError(f'CRAM-MD5 failed: {e}') from e def auth_plain(self, challenge=None): """ Authobject to use with PLAIN authentication. Requires self.user and @@ -743,13 +750,12 @@ def login(self, user, password, *, initial_response_ok=True): return (code, resp) except SMTPAuthenticationError as e: last_exception = e - except ValueError as e: + except SMTPAuthHashUnsupportedError as e: # Some environments (e.g., FIPS) disable certain hashing algorithms like MD5, # which are required by CRAM-MD5. This raises a ValueError when trying to use HMAC. # If this happens, we catch the exception and continue trying the next auth method. last_exception = e - if 'unsupported' in str(e).lower(): - continue + continue # We could not login successfully. Return result of last attempt. raise last_exception diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index bc9ec50d6d4ea1..acbd1a1db24652 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -1572,7 +1572,7 @@ def testAUTH_PLAIN_initial_response_auth(self): class TestSMTPLoginValueError(unittest.TestCase): def broken_hmac(*args, **kwargs): - raise ValueError("[digital envelope routines] unsupported") + raise smtplib.SMTPAuthHashUnsupportedError("CRAM-MD5 failed: [digital envelope routines] unsupported") def test_login_raises_valueerror_when_cram_md5_fails(self): with patch("hmac.HMAC", self.broken_hmac): @@ -1593,7 +1593,7 @@ def docmd(self, *args, **kwargs): return 334, b"Y2hhbGxlbmdl" smtp = FakeSMTP() - with self.assertRaises(ValueError) as ctx: + with self.assertRaises(smtplib.SMTPAuthHashUnsupportedError) as ctx: smtp.login("user", "pass") self.assertIn("unsupported", str(ctx.exception).lower()) From ba5c9917f49a4fbd83d8288918de2d38d616b5c1 Mon Sep 17 00:00:00 2001 From: Raphael Nogueira Date: Wed, 2 Jul 2025 21:13:36 +0100 Subject: [PATCH 4/4] bpo-136134: Clarify comment on handling unsupported hash in CRAM-MD5 --- Lib/smtplib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/smtplib.py b/Lib/smtplib.py index 9f11d27478ece4..4316df93bbca5d 100644 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -752,7 +752,7 @@ def login(self, user, password, *, initial_response_ok=True): last_exception = e except SMTPAuthHashUnsupportedError as e: # Some environments (e.g., FIPS) disable certain hashing algorithms like MD5, - # which are required by CRAM-MD5. This raises a ValueError when trying to use HMAC. + # which are required by CRAM-MD5. This raises a SMTPAuthHashUnsupportedError when trying to use HMAC. # If this happens, we catch the exception and continue trying the next auth method. last_exception = e continue