在Mavericks上使用Python时SSL证书验证失败

3

我被一个持久的SSL验证问题困扰。

SSL: CERTIFICATE_VERIFY_FAILED

我在构建一个使用Mozilla Persona进行用户身份验证的Django应用程序时发现了这个错误。

(python3.4)> import requests
(python3.4)> requests.get('https://verifier.login.persona.org')

我从requestsurllib3再到ssl发现出现了SSL: CERTIFICATE_VERIFY_FAILED的追溯。
...
"/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/ssl.py", line 805, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:598)

...
"/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/requests-2.4.1-py3.4.egg/requests/packages/urllib3/connectionpool.py", line 543, in urlopen
    raise SSLError(e)
requests.packages.urllib3.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:598)

...
"/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/site-packages/requests-2.4.1-py3.4.egg/requests/adapters.py", line 420, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:598)

Python3和Python2的区别

这里开始变得有趣了:当使用Python2.7时,我没有遇到同样的问题:

(python2.7)> import requests
(python2.7)> requests.get('https://verifier.login.persona.org')
<Response [200]>

我的第一个想法是,两个版本的requests可能使用了不同的证书[1],所以当我发现这两个文件完全相同时,我非常惊讶:

(bash)$ diff `python3.4 -c "import requests; print(requests.certs.where())"` \
             `python2.7 -c "import requests; print requests.certs.where()"`
# no diff

使用-CAFile在openssl中重现错误并解决

有趣的是,这个问题并不局限于python3.4 [2]。

(bash)$ openssl s_client -connect github.com:443
...
Verify return code: 20 (unable to get local issuer certificate)

编辑 Steffen 的评论告诉我,这种“调试”方法实际上并不提供信息,因为 s_client 需要 -CApath 才能进行验证。然而,我可以指定与 requests 包使用的相同证书,但我没有收到相同的错误仍然很有趣:

(bash)$ openssl s_client -connect github.com:443 \
        -CAfile `python3 -c 'import requests; print(requests.certs.where())'`
...
Verify return code: 0 (ok)

在这一点上,我完全不懂。我不知道这是否真的是一个 openssl 问题,还是与OSX Mavericks[3]有关。这是我正在使用的openssl版本:

(bash)$ openssl version
OpenSSL 1.0.1f 6 Jan 2014

Mavericks KeyChain.app

针对操作系统的解决方案,我已经尝试清除我的登录 KeyChain[4],但仍然无法解决问题。

pip相关问题

还有一个可能与此相关的细节。python3.4 自带 pip,但 pip3 命令对我来说毫无用处。无论我尝试安装什么:

(bash)$ pip3 install [new-lib] # pip 1.5.6

I get:

Downloading/unpacking [new-lib]
    Cannot fetch index base URL https://pypi.python.org/simple/
    Could not find any downloads that satisfy the requirement [new-lib]
Cleaning up...
    No distributions at all found for [new-lib]
    Storing debug log for failure in ~/.pip/pip.log

尽管这不是一个(明显的)SSL错误,但它看起来很相似[5],成功的解决方法是在我的virtualenvs中使用easy_install安装旧版本的pip[5]。 我抱着希望,认为这两个问题是相关的。
总结:
  • 寻求SSL证书失败错误的可能解决方案(不使用verify = Falserequests调用中)。
  • 我在python3.4中遇到错误,但在python2.7中没有遇到,尽管在两种情况下使用的cert.pem完全相同。
  • 虽然我可以使用openssl s_client -connect重新创建SSL错误,但是我可以通过将-CAFile指定给requests库使用的cert.pem 来避免该错误。
  • 我最好的猜测是这是特定于Mavericks的问题,但我不知道如何继续。
  • 我希望找到一个解决方案,能让我像预期的那样使用pip3来安装python3.4包。
感谢您的帮助!
[1]:我的机器上的python2.7是使用Enthought安装的。 但是安装系统版本的python2.7和requests库也可以正常工作。
[2]:请参见openssl,python requests error: "certificate verify failed",了解在使用python 2.7时出现类似问题的情况。
[3]:看起来Mavericks引入了openssl的一个更改?http://curl.haxx.se/mail/archive-2013-10/0036.html [4]:从这里清除KeyChain.app:https://superuser.com/a/721629/261875 [5]:使用pip3出现SSL错误:https://stackoverflow.com/a/22051466/2506078

这绝对是与您的机器非常特定的问题。使用requests,我可以连接到该URL而没有任何问题。我也在Mavericks上,但openssl version返回自2013年2月起的“0.9.8y”(早于您发布的有关OpenSSL更改的链接)。 - Ian Stapleton Cordasco
关于“s_client”的第一个错误:由于“s_client”没有默认的验证路径,因此除非您明确地提供CApath,否则任何验证都会返回错误,无论您连接到哪个主机。 - Steffen Ullrich
你安装 Python 3.4 的来源在哪里?请展示对于成功运行的 Python 2 和失败的 Python 3 执行 pythonx.x -c 'import sys;print(sys.version)' 的结果。 - Ned Deily
@Ned Python 3.4是从python.org安装的(.dmg):python 3.4.1 (v3.4.1:c0e311e010fc, May 18 2014, 00:22:19), [GCC 4.2.1 (Apple Inc. build 5577)];Python 2.7是从Enthought发行版安装的:python 2.7.6 | 32-bit (default, Apr 11 2014, 12:06:39) [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]。然而,我的系统Python 2.7也可以正常工作:python 2.7.6 (default, Nov 18 2013, 15:12:51) [GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.2.79)]。 - pedmiston
2个回答

2
根据您提供的额外信息,看起来您已安装了仅支持32位的Python 3.4.1 from python.org版本。该版本主要用于OS X 10.5系统;因此,它与由Apple提供的10.5版OpenSSL版本链接在一起。您可以通过使用python.org提供的64位/32位3.4.1安装程序来避免此问题;该版本建议在OS X 10.6+上使用,并与较新版本的Apple OpenSSL链接在一起。否则,您可以使用curl或浏览器手动从PyPI下载分发文件,并让pip从已下载的文件中进行安装。

1
很容易证明,pip 可以与从 python.org 下载的 64 位/32 位 Python 3.4.1 安装程序的 Python 3.4.1 配合良好,但无法与仅支持 32 位的安装程序配合使用,并且很容易证明它们与不同版本的 Apple 提供的 OpenSSl 库链接:pythonx.y -c 'import ssl;print(ssl.OPENSSL_VERSION)。后者显示 0.9.7 版本;而前者使用的是添加了 SNI 支持的 0.9.8 版本。 - Ned Deily
pypi.python.org、verifier.login.persona.org或github.com都不依赖SNI。这意味着,无论您使用SNI与否,它们返回的证书都是相同的,因此这不可能是问题所在。 - Steffen Ullrich
@sigmavirus24 对不起,当然你是对的,两个 OS X SSL 版本之间可能存在的差异不仅仅是 SNI 支持。有什么建议吗? - Ned Deily
它们在支持的密码等方面有所不同,但从我看到的情况来看,这也不应该是一个问题。所有这些站点都可以使用SSLv3客户端工作,直到TLS1.1客户端(可能还包括TLS1.2)。因为错误明确指出验证失败,所以不能是共享密码的问题。我认为区别在于是否回退到OS X钥匙扣或不回退(请参见我的回复)。 - Steffen Ullrich
谢谢,密钥环支持方面的差异可能是原因。我稍后会进一步研究它。我已经编辑了我的答案,删除了对SNI的引用。无论如何,对于OP来说,解决方案都是相同的:使用其他python.org安装程序。 - Ned Deily
显示剩余2条评论

1
只是猜测:Mac OS X自带的OpenSSL(仍为0.9.8版本)具有特殊钩子,因此如果与OpenSSL本身提供的CA验证失败,则会回退到OS X钥匙串。但是,如果您使用自己的OpenSSL,则没有此回退。
这意味着,如果您在python2中使用内置的OpenSSL,它将成功验证站点,如果在OS X钥匙串中找到CA,则即使它不在requests本身提供的证书存储中也是如此。但是,如果您已经编译了针对自己的OpenSSL的python3,则只会使用requests本身提供的CA,并且不会回退到OS X钥匙串,因此如果CA不在requests密钥环中,则会验证失败。
有关Mac OS X的此“功能”及其引入的问题的详细信息,请参见https://hynek.me/articles/apple-openssl-verification-surprises/
不幸的是,这并不能解释为什么openssl可以成功验证requests库的默认证书,除非涉及另一个OpenSSL版本,即python3使用没有钥匙环回退的版本和命令行上的最新版本具有回退。

谢谢。你知道我如何检查每个Python版本使用的OpenSSL版本吗?在这一点上,进一步进行调试过程是必要的,但我希望有一个解决方案告诉我如何让Python3.4表现正常。 - pedmiston
1
要找出Python正在使用的OpenSSL版本,请执行此命令:python -c 'import ssl; print(ssl.OPENSSL_VERSION)' - Ian Stapleton Cordasco

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接