使用Python smtplib仅凭证书文件发送邮件,无需密钥文件

3 投票
2 回答
2767 浏览
提问于 2025-04-18 09:59

我正在尝试用一个证书文件发送邮件,使用的脚本如下:

import smtplib

client = smtplib.SMTP(myhost, myport)
client.ehlo()
client.starttls(certfile=mycertfile)
client.ehlo()

client.login(myusername, mypassword)
client.sendmail(sender, receiver, Message)
client.quit()

但是我遇到了以下错误:

SSLError: error:140B0009:SSL routines:SSL_CTX_use_PrivateKey_file:PEM lib

我查了一下文档(smtplib.htmlssl.html),上面说我需要提供一个私钥。但我只有证书文件(是base64的PEM格式)。我的运维人员说在这种情况下不需要私钥,因为我不需要确认连接的本地身份。

问题

有没有办法在不提供私钥的情况下发送邮件?如果需要私钥,那是为什么呢?

2 个回答

1

这里有一段来自这个页面的猴子补丁:

class SMTPExt(smtplib.SMTP):
    """
    This class extends smtplib.SMTP and overrides the starttls method
    allowing extra parameters and forwarding them to ssl.wrap_socket.
    """

    def starttls(self, keyfile=None, certfile=None, **kwargs):
        self.ehlo_or_helo_if_needed()
        if not self.has_extn("starttls"):
            raise SMTPException("STARTTLS extension not supported by server.")
        (resp, reply) = self.docmd("STARTTLS")
        if resp == 220:
            self.sock = ssl.wrap_socket(self.sock, keyfile, certfile, **kwargs)
            self.file = SSLFakeFile(self.sock)
            # RFC 3207:
            # The client MUST discard any knowledge obtained from
            # the server, such as the list of SMTP service extensions,
            # which was not obtained from the TLS negotiation itself.
            self.helo_resp = None
            self.ehlo_resp = None
            self.esmtp_features = {}
            self.does_esmtp = 0
        return (resp, reply)

使用来自requests的根证书

2

使用SSL/TLS有两种方式:一种是客户端认证,另一种是“基本”连接,客户端不需要认证。在客户端认证的连接中,服务器和客户端都会互相发送证书。而在“基本”连接中,只有服务器会发送证书。

如果你没有提供证书或密钥文件,smtplib会使用基本连接,这种情况下客户端不需要认证。

如果你使用了证书,那么这就会变成客户端认证的连接。在这种情况下,服务器会要求客户端证明它拥有这个证书,方法是用私钥签署一个握手消息。为了做到这一点,客户端还需要私钥,这个私钥可以在证书文件里,也可以是一个单独的密钥文件。

简单来说,如果你想使用客户端证书,就必须同时使用一个密钥。如果不想用证书和密钥,那就可以都留空。


另一方面,也许你有一个服务器证书文件或者CA列表想要在连接中使用?

在这种情况下,你需要把它传递给ssl.wrap_socketca_certs参数。由于你使用的是Python 2.6,所以没有简单的方法可以通过smtplib来做到这一点(Python 3.3及以上版本在starttls中有一个context参数)。

如何解决这个问题取决于你的应用。例如,如果你不需要ssl做其他事情,一个比较“黑科技”的解决方案是用一个提供你的ca_cert的函数来替换ssl.wrap_socket(可能还需要cert_reqs=CERT_REQUIRED)。

一个更全面的解决方案是扩展smtplib.SMTP,创建你自己的版本,以便允许传入这些参数。

撰写回答