Python 请求 - 如何使用系统 ca 证书(debian/ubuntu)?
- 2025-03-04 08:24:00
- admin 原创
- 104
问题描述:
我已经在 Debian 中安装了自签名的根 CA 证书/usr/share/ca-certificates/local
,并使用 进行安装sudo dpkg-reconfigure ca-certificates
。此时,true | gnutls-cli mysite.local
一切都很顺利,一切true | openssl s_client -connect mysite.local:443
都很顺利,但 python2 和 python3 请求模块坚持认为它对证书不满意。
python2:
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/local/lib/python2.7/site-packages/requests/api.py", line 70, in get
return request('get', url, params=params, **kwargs)
File "/usr/local/lib/python2.7/site-packages/requests/api.py", line 56, in request
return session.request(method=method, url=url, **kwargs)
File "/usr/local/lib/python2.7/site-packages/requests/sessions.py", line 488, in request
resp = self.send(prep, **send_kwargs)
File "/usr/local/lib/python2.7/site-packages/requests/sessions.py", line 609, in send
r = adapter.send(request, **kwargs)
File "/usr/local/lib/python2.7/site-packages/requests/adapters.py", line 497, in send
raise SSLError(e, request=request)
requests.exceptions.SSLError: ("bad handshake: Error([('SSL routines', 'ssl3_get_server_certificate', 'certificate verify failed')],)",)
python3
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/local/bin/python3.5/site-packages/requests/api.py", line 70, in get
return request('get', url, params=params, **kwargs)
File "/usr/local/bin/python3.5/site-packages/requests/api.py", line 56, in request
return session.request(method=method, url=url, **kwargs)
File "/usr/local/bin/python3.5/site-packages/requests/sessions.py", line 488, in request
resp = self.send(prep, **send_kwargs)
File "/usr/local/bin/python3.5/site-packages/requests/sessions.py", line 609, in send
r = adapter.send(request, **kwargs)
File "/usr/local/bin/python3.5/site-packages/requests/adapters.py", line 497, in send
raise SSLError(e, request=request)
requests.exceptions.SSLError: ("bad handshake: Error([('SSL routines', 'ssl3_get_server_certificate', 'certificate verify failed')],)",)
为什么 python 忽略系统 ca-certificates 包,以及我该如何集成它?
解决方案 1:
来自https://stackoverflow.com/a/33717517/1695680
要使 Python 请求使用系统 ca 证书包,需要告知它使用它而不是其自己的嵌入式包
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Requests 在此处嵌入了它的包,以供参考:
/usr/local/lib/python2.7/site-packages/requests/cacert.pem
/usr/lib/python3/dist-packages/requests/cacert.pem
或者在较新版本中使用附加包从以下位置获取证书:
https://github.com/certifi/python-certifi
要验证从哪个文件加载了证书,您可以尝试:
Python 3.8.5 (default, Jul 28 2020, 12:59:40)
>>> import certifi
>>> certifi.where()
'/etc/ssl/certs/ca-certificates.crt'
解决方案 2:
最近我为此苦苦挣扎了大约一周。我终于找到了用 Python 验证自签名或私人签名证书的方法。您需要创建自己的证书包文件。每次更新库时,无需更新晦涩难懂的证书包,也无需向系统证书存储区添加任何内容。
首先运行之前运行过的 openssl 命令,但要添加 -showcerts。openssl s_client -connect mysite.local:443 -showcerts
这将为您提供一个长输出,在顶部您将看到整个证书链。通常,这意味着三个证书,即网站证书、中间证书和根证书,按此顺序排列。我们只需将根证书和中间证书以相反的顺序放入下一个文件中。
将最后一个证书(根证书)复制到新的文本文件中。只抓取和之间的内容,包括:
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
将中间证书(又称中间证书)复制到根证书下的新文本文件中。再次获取开始和结束证书行以及其间的所有内容。
将此文本文件保存到 Python 脚本所在的目录中。我建议将其命名为CertBundle.pem
。(如果您为其指定了其他名称,或将其放在文件夹结构中的其他位置,请确保 verify 行反映了这一点。)更新脚本以引用新的证书包:
response = requests.post("https://www.example.com/", headers=headerContents, json=bodyContents, verify="CertBundle.pem")
就是这样。如果您只有根证书或中间证书,那么 Python 就无法验证整个证书链。但是,如果您将两个证书都包含在您创建的证书包中,那么 Python 可以验证中间证书是否由根证书签名,然后当它访问网站时,它可以验证网站的证书是否由中间证书签名。
编辑:修复了证书包的文件扩展名。此外,还修复了几个语法错误。
解决方案 3:
我的看法:
感谢这个其他答案,它让我检查了实际的请求代码,我发现你不必使用 env 变量,而只需在请求中设置“验证”参数:
requests.get("https://whatever", verify="/my/path/to/cacert.crt", ...)
它也有文档记录,尽管我只能在发现之后才能找到文档(并且 pypi 项目指向文档的无效链接):D
解决方案 4:
requests
使用certifi
作为默认根证书包,内置了很多好的 CA 但无法修改。
Debian(和 Ubuntu)维护者改变了certifi
与默认行为不同的行为:
def where():
return "/etc/ssl/certs/ca-certificates.crt"
所以如果你使用 apt-installedrequests
就certifi
没有问题。
但是 pip3 在虚拟环境中安装的 certifi 使用内置 CA。因此无法使用机制。除了在应用程序代码中手动指定根证书(如果通过第三方接口间接调用则update-ca-certificates
可能无法实现)外,它还可以使用环境变量覆盖以模拟 Debianized 行为。request
`REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt`
解决方案 5:
尝试了所有方法后,我发现这个方法在 Ubuntu 上很管用
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
certifi
尽管显示的是相同的路径,我也必须这样做。
解决方案 6:
我不想使用静态文件或我不了解的附加 pip 包来解决完全相同的问题。幸运的是,标准ssl
包(尤其是load_default_certs()
函数)可以解决这个问题:
import ssl
import requests
from requests.adapters import HTTPAdapter
class LocalSSLContext(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
context = ssl.create_default_context()
context.load_default_certs()
kwargs['ssl_context'] = context
return super(LocalSSLContext, self).init_poolmanager(*args, **kwargs)
session = requests.Session()
sslContext = LocalSSLContext()
session.mount('https://www.example.com/', sslContext)
response = session.get(url='https://www.example.com/')
print(response.status_code)
在 Windows 和 Linux 环境中为我工作。
解决方案 7:
@fryads 的答案对我来说最有效。由于使用了具有 https 拦截功能的公司 vpn,我在使用本地 python 脚本时也遇到了类似的问题。以下脚本(在正确指导后借助 gpt 的帮助完成 :) - 可以完成这项工作。
#!/bin/bash
# Description:
# This script fetches the entire certificate chain for a given domain using openssl.
# It then removes the server's certificate and reverses the order of the remaining certificates.
# The output is saved to a file named `certbundle.pem`.
#
# Usage:
# ./scriptname.sh <domain_name>
#
# Example:
# ./scriptname.sh example.com
# Check if a URL has been provided as an argument
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <domain_name>"
exit 1
fi
# Extract the domain name from the argument
DOMAIN="$1"
# Fetch only the certificate chain using openssl.
# We use sed to filter out everything except the certificates.
openssl s_client -connect "$DOMAIN":443 -showcerts | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > certbundle.pem
# Remove the first certificate (from the server itself) from the certbundle.pem file.
# In macOS, the -i option for sed requires an extension, but we're providing an empty string to modify the file in-place without creating a backup.
sed -i '' '1,/-END CERTIFICATE-/d' certbundle.pem
# Reverse the order of the certificates and save it back to certbundle.pem.
awk 'BEGIN {last = 0} /-----BEGIN CERTIFICATE-----/ { if(last) {print lastcert} last=1 } { lastcert = (lastcert $0 RS) } END {print lastcert}' certbundle.pem > reversed_certbundle.pem && mv reversed_certbundle.pem certbundle.pem
echo "Certificate chain has been saved in certbundle.pem."
在 python 中只需简单地执行:
import request
response = requests.get("your url goes here", verify='certbundle.pem')
解决方案 8:
出现错误的原因可能还有其他。即,在 中添加了两个太相似的(例如自签名)证书/usr/share/ca-certificates/local
。
在这种情况下,requests
发现其中一个没有问题,但是当尝试连接到另一个主机时,会出现错误。
然后 SAN(主体备用名称)和/或名称可能会引起混淆(我仍然不确定是哪一个)。例如,名称可能相同或 SAN 主机相交。当使用
openssl x509 -in my.crt -text -noout
扫码咨询,免费领取项目管理大礼包!