Python请求不处理仅来自一台机器的丢失中间证书

Rob*_*tts 7 ssl-certificate python-2.7 python-requests

我正在开发一个运行 CentOS (Linux) 的机器,当尝试访问特定子域进行工作时遇到以下错误:

Traceback (most recent call last):
  ... # My code, relevant call is requests.get(url)
  File "/usr/local/lib/python2.7/site-packages/requests/api.py", line 60, in get
    return request('get', url, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/requests/api.py", line 49, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/requests/sessions.py", line 457, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/local/lib/python2.7/site-packages/requests/sessions.py", line 569, in send
    r = adapter.send(request, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/requests/adapters.py", line 420, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: [Errno 1] _ssl.c:504: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
Run Code Online (Sandbox Code Playgroud)

根据https://www.digicert.com/help/,子域“没有发送所需的中间证书”(这是 DigiCert 发现的唯一问题)。然而,当我从 Mac 笔记本电脑上运行我的代码时,它可以毫无问题地处理这个问题,Chrome 和 Safari 也是如此。我在我的笔记本电脑和 Linux 机器上运行 Python 2.7.5。我在 Linux 机器上运行 requests 1.2.0,在笔记本电脑上运行 requests 2.2.1,但我将两者都升级到 2.4.3,但它们仍然没有相同的行为。

也可能相关 - 相同的证书正在与发送中间证书的其他一些子域一起使用,并且我的笔记本电脑和 Linux 盒子都没有任何问题,所以我的笔记本电脑不应该有一个根 CA linux盒子没有。

有谁知道为什么它在我的 linux 盒子上不起作用以及如何修复它?

Mar*_*epp 14

我花了一天的时间来完全理解并解决这个问题,所以我认为与大家分享我的发现会很高兴:-)!这是我的结果:

SSL 服务器配置中的一个常见缺陷是提供不完整的证书链,通常会忽略中间证书。例如,我正在使用的网站在服务器响应中不包含常见的 DigiCert“中间”证书“DigiCert TLS RSA SHA256 2020 CA1”。

由于此配置缺陷很常见,因此大多数(但并非所有)现代浏览器都实现了一种称为“AIA Fetching”的技术来即时修复此问题(请参阅https://www.thesslstore.com/blog/aia-fetching/)。

Python 的 SSL 支持不支持 AIA Fetching,依赖于服务器的完整证书链;否则它会抛出异常,就像这样

SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1124)')))
Run Code Online (Sandbox Code Playgroud)

关于是否应将 AIA Fetching 添加到 Python 中的讨论正在进行中,例如在此线程中:https ://bugs.python.org/issue18617#msg293894 。

我的印象是,在可预见的未来,这仍然是一个悬而未决的问题。

现在,我们该如何解决这个问题?

  1. certifi如果您还没有安装,请安装,或者更新它

pip install certifi

或者

pip install certifi --upgrade

许多(但不是全部)Python 模块可以使用来自certifiMozilla CA 证书计划( https://wiki.mozilla.org/CA )的证书。基本上,certifi 从 Mozilla 站点创建一个干净的 *.pem 文件,并提供用于访问该文件的轻量级 Python 接口。certifi

  1. 将丢失的证书下载为 PEM 语法的文件,例如从https://www.digicert.com/kb/digicert-root-certificates.htm或从受信任的浏览器下载。

  2. 找到 certifi *.PEM 证书文件

     import certifi
     print(certifi.where())
    
    Run Code Online (Sandbox Code Playgroud)

注意:我建议首先激活您想要使用证书的虚拟环境(例如conda activate <envname>) 。文件路径会有所不同。如果您将此应用到您的基础环境,任何潜在的有缺陷的证书都会使您所有代码的整个 SSL 机制面临风险。

示例: /Users/用户名/anaconda3/envs/环境名称/lib/python3.8/site-packages/certifi/cacert.pem

使用一个简单的文本编辑器,打开该文件,然后在标题后面的开头插入缺少的证书,如下所示

##
## CA 根证书捆绑包
##
...
-----开始证书-----
+I2tIJLYrVJmuzHZ9bjPvXj1hJeRPG/cUJ9WIQDgLGB
Afr5yjK7tI4nhyfFK3TUqNaX3sNK+CROU6J
---> 这是附加证书。
+I2tIJLYrVJmuzHZ9bjPvXj1hJeRPG/cUJ9WIQDgLGB
Afr5yjK7tI4nhyfFK3TUqNaX3sNK+CROU6J
-----证书结束-----

包含开始和结束标记很重要。

保存文件,一切就都准备好了!

您可以使用以下几行测试它是否有效:

#Python 3
导入 urllib.request 导入证书导入请求

 
URL = 'https://www.the_url_that_caused_the_trouble.org'
print('正在尝试 urllib.request.urlopen().')
r = urllib.request.urlopen(URL)
print(f'urllib.request.urlopen\n================\n {r.read()[:80]}')
print('尝试 requests.get().')
r = requests.get(URL)
print(f'requests.get()\n================\n {r.text[:80]}')

注意:一般的 SSL 证书(例如 openssl 的证书)可能位于其他地方,因此您可能必须在那里尝试相同的方法:

/用户/用户名/anaconda3/envs/环境名称/ssl

瞧!

笔记:

  1. 当您更新certifi或创建新的虚拟环境时,更改可能会丢失,但我认为这实际上是很好的设计,因为它不会对整个系统进行临时安全调整。
  2. 当然,下载证书的过程存在潜在的安全风险 - 如果该下载受到损害,您的整个 SSL 链也可能受到损害。
  3. 证书的维护certifi滞后于 Mozilla 的发布。如果您想将最新版本的 Mozilla CA 捆绑包与 一起使用certifi,您可以使用 https://github.com/mfhepp/update_certifi_certificates中的我的脚本。


Rob*_*tts 2

我仍然不明白为什么它在一个地方起作用而在另一个地方不起作用,但我确实找到了一种可以接受的解决方法,它比关闭证书验证要好得多。

根据requests 库文档,如果系统上安装了certifi ,它将使用certifi 。所以我安装了证书

sudo pip install certifi
Run Code Online (Sandbox Code Playgroud)

然后修改它使用的 .pem 文件。您可以使用以下命令找到文件位置certifi.where()

>>> import certifi
>>> certifi.where()
'/usr/local/lib/python2.7/site-packages/certifi/cacert.pem'
Run Code Online (Sandbox Code Playgroud)

我将中间密钥添加到该 .pem 文件中,现在它可以工作了。仅供参考,.pem 文件期望证书显示如下

-----BEGIN CERTIFICATE-----
<certificate here>
-----END CERTIFICATE-----
Run Code Online (Sandbox Code Playgroud)

警告:这并不是真正的解决方案,只是一种解决方法。从安全角度来看,告诉您的系统信任证书可能很危险。如果您不了解证书,则不要使用此解决方法,除非您的其他选择是完全关闭证书验证。

另外,从请求文档中:

为了安全起见,我们建议经常升级证书!

我假设当您升级 certifi 时,您必须重做对该文件所做的任何更改。我还没有仔细研究过如何进行更改,以免在 certifi 更新时被覆盖。