Opened 17 months ago

Closed 17 months ago

Last modified 17 months ago

#34504 closed Bug (needsinfo)

SSLCertVerificationError on outgoing emails for some mailboxes

Reported by: Kamen Kalchev Owned by: nobody
Component: Core (Mail) Version: 4.2
Severity: Normal Keywords: smtplib, ssl, Django4.2
Cc: Adam Johnson Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Kamen Kalchev)

It looks like this was previously reported in a different scenario and marked as fixed but is still not working as expected. Downgrading Django to 4.1.7 fixes the problem with outgoing messages to the mailbox.

Environment:

Python3.10.6
Django4.2
EMAIL_USE_SSL=True
EMAIL_USE_TLS=False

EMAIL_SSL_CERTFILE=Does not make a difference if we provide a file location or not
EMAIL_SSL_KEYFILE=Does not make a difference if we provide a file location or not

Link to previously reported issue:
https://code.djangoproject.com/ticket/34386

Stacktrace:

File "/usr/local/lib/env3/lib/python3.10/site-packages/django/core/mail/message.py" line 298 in send [args] [locals]
return self.get_connection(fail_silently).send_messages([self])
File "/usr/local/lib/env3/lib/python3.10/site-packages/django/core/mail/backends/smtp.py" line 127 in send_messages [args] [locals]
new_conn_created = self.open()
File "/usr/local/lib/env3/lib/python3.10/site-packages/django/core/mail/backends/smtp.py" line 85 in open [args] [locals]
self.connection = self.connection_class(
File "/usr/lib/python3.10/smtplib.py" line 1050 in __init__ [args] [locals]
SMTP.__init__(self, host, port, local_hostname, timeout,
File "/usr/lib/python3.10/smtplib.py" line 255 in __init__ [args] [locals]
(code, msg) = self.connect(host, port)
File "/usr/lib/python3.10/smtplib.py" line 341 in connect [args] [locals]
self.sock = self._get_socket(host, port, self.timeout)
File "/usr/lib/python3.10/smtplib.py" line 1057 in _get_socket [args] [locals]
new_socket = self.context.wrap_socket(new_socket,
File "/usr/lib/python3.10/ssl.py" line 513 in wrap_socket [args] [locals]
return self.sslsocket_class._create(
File "/usr/lib/python3.10/ssl.py" line 1071 in _create [args] [locals]
self.do_handshake()
File "/usr/lib/python3.10/ssl.py" line 1342 in do_handshake [args] [locals]
self._sslobj.do_handshake()
SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:997)

Change History (9)

comment:1 by Kamen Kalchev, 17 months ago

Description: modified (diff)

comment:2 by Mariusz Felisiak, 17 months ago

Resolution: needsinfo
Status: newclosed

Thanks for the ticket, however I don't see much difference between the current implementation and Python < 3.12 behavior in creating a default SSL context. The only difference is that now check_hostname is set to True, does it work for you with the following diff?

  • django/core/mail/backends/smtp.py

    diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py
    index 1ee48269ae..132bed29be 100644
    a b class EmailBackend(BaseEmailBackend):  
    6060        if self.ssl_certfile or self.ssl_keyfile:
    6161            ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
    6262            ssl_context.load_cert_chain(self.ssl_certfile, self.ssl_keyfile)
     63            ssl_context.check_hostname = False
    6364            return ssl_context
    6465        else:
    6566            return ssl.create_default_context()

I'm not sure we'd like to change that.

in reply to:  2 comment:3 by Kamen Kalchev, 17 months ago

Hello and thank you for the prompt answer, Mariusz.

We have played around with your suggestion and what worked in our case was adding check_hostname = False and verify_mode = ssl.CERT_NONE in the else clause (since we are not passing in a specific cert/ key file) in an overridden ssl_context method for a child class of EmailBackend.

To be honest, we are not sure if this should be changed for everyone but it looks like something was omitted in creating the default ssl context, as in Django version 4.1.7 we did not have to manually set those params and it worked. Anyway, we really appreciate your response and wish you a good week ahead.

    @cached_property
    def ssl_context(self):
        if self.ssl_certfile or self.ssl_keyfile:
            ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
            ssl_context.load_cert_chain(self.ssl_certfile, self.ssl_keyfile)
            return ssl_context
        else:
            ssl_context = ssl.create_default_context()
            ssl_context.check_hostname = False
            ssl_context.verify_mode = ssl.CERT_NONE
            return ssl_context

Replying to Mariusz Felisiak:

Thanks for the ticket, however I don't see much difference between the current implementation and Python < 3.12 behavior in creating a default SSL context. The only difference is that now check_hostname is set to True, does it work for you with the following diff?

  • django/core/mail/backends/smtp.py

    diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py
    index 1ee48269ae..132bed29be 100644
    a b class EmailBackend(BaseEmailBackend):  
    6060        if self.ssl_certfile or self.ssl_keyfile:
    6161            ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
    6262            ssl_context.load_cert_chain(self.ssl_certfile, self.ssl_keyfile)
     63            ssl_context.check_hostname = False
    6364            return ssl_context
    6465        else:
    6566            return ssl.create_default_context()

I'm not sure we'd like to change that.

comment:4 by Mariusz Felisiak, 17 months ago

Cc: Adam Johnson added

Yes, sorry check_hostname = False should be in the "default context" branch.

It's from ssl._create_unverified_context() that is "less restrict than create_default_context() to increase backward compatibility.". It worked because the host wasn't checked. As far as I'm aware, this shows a problem with verification of host certificate chain.

comment:5 by Mariusz Felisiak, 17 months ago

#34524 was a duplicate.

comment:6 by Mariusz Felisiak, 17 months ago

Kamen, what do you think about adding a backward incompatibility note? see PR.

comment:7 by GitHub <noreply@…>, 17 months ago

In 5a6d4d3b:

Refs #34118, Refs #34504 -- Added backward incompatibility note about EmailBackend.ssl_context.

Follow up to 2848e5d0ce5cf3c31fe87525536093b21d570f69.

comment:8 by Mariusz Felisiak <felisiak.mariusz@…>, 17 months ago

In 4f343a1:

[4.2.x] Refs #34118, Refs #34504 -- Added backward incompatibility note about EmailBackend.ssl_context.

Follow up to 2848e5d0ce5cf3c31fe87525536093b21d570f69.
Backport of 5a6d4d3bfde07daab9777545694beb014c832264 from main

in reply to:  6 comment:9 by Kamen Kalchev, 17 months ago

Looks good, Mariusz. Thank you once again for looking into this ticket.

Replying to Mariusz Felisiak:

Kamen, what do you think about adding a backward incompatibility note? see PR.

Note: See TracTickets for help on using tickets.
Back to Top