Python 请求 - 如何使用系统 ca 证书(debian/ubuntu)?

问题描述 投票:0回答:7

我已将自签名根 ca 证书安装到 debian 的

/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 证书捆绑包,如何集成它?

python ssl debian python-requests debian-based
7个回答
233
投票

来自https://stackoverflow.com/a/33717517/1695680

要使 python 请求使用系统 ca-certificates 捆绑包,需要告诉它在自己的嵌入式捆绑包上使用它

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'

37
投票

最近我为此苦苦挣扎了一周左右。我终于找到了在 Python 中验证自签名或私签名证书的方法。您需要创建自己的证书捆绑文件。每次更新库或向系统证书存储添加任何内容时,无需更新晦涩的证书包。

首先运行之前运行的 openssl 命令,但添加 -showcerts。

openssl s_client -connect mysite.local:443 -showcerts
这将为您提供很长的输出,在顶部您将看到整个证书链。通常,这意味着三个证书,依次为网站证书、中间证书和根证书。我们只需将根证书和中间证书以相反的顺序放入下一个文件中。

将最后一个证书(根证书)复制到新的文本文件。只抓住之间的东西,包括:

-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

将中间证书(又名中间证书)复制到根证书下的新文本文件中。再次,抓住开始和结束证书行以及之间的所有内容。

将此文本文件保存到 Python 脚本所在的目录中。我的建议是称其为

CertBundle.pem
。 (如果您给它一个不同的名称,或者将其放在文件夹结构中的其他位置,请确保验证行反映了这一点。)更新您的脚本以引用新的证书包:

response = requests.post("https://www.example.com/", headers=headerContents, json=bodyContents, verify="CertBundle.pem")

就是这样。如果您只有根证书或只有中间证书,那么 Python 无法验证整个证书链。但是,如果您将两个证书都包含在您创建的证书包中,则 Python 可以验证中间证书是否由根证书签名,然后当它访问网站时,它可以验证网站的证书是否由中间证书签名.

编辑:修复了证书包的文件扩展名。此外,还修复了一些语法错误。


22
投票

我的两分钱:

感谢另一个answer,它让我检查了实际的请求代码,我发现你不必使用环境变量,而只需在请求中设置“验证”参数即可:

requests.get("https://whatever", verify="/my/path/to/cacert.crt", ...)

它也是有记录的,尽管我只能在发现之后才能找到文档(并且 pypi 项目指向文档的死链接):D


18
投票

requests
使用
certifi
作为默认根证书包,内置了很多好的CA,但无法修改。

Debian(和 Ubuntu)维护者更改了

certifi
的行为与默认行为不同:

def where():
    return "/etc/ssl/certs/ca-certificates.crt"

所以如果你使用 apt-installed

requests
certifi
就没有问题了。

但是 pip3 在虚拟环境中安装了 certifi,使用内置 CA。所以无法使用

update-ca-certificates
机制。除了在应用程序代码中手动指定根证书(如果通过第 3 方接口间接调用
request
可能无法实现)之外,它还可以使用环境变量
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
覆盖以模拟 Debian 化行为。


4
投票

尝试了一切之后,我发现这在 Ubuntu 上对我有用

export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt

即使

certifi
显示了相同的路径,我也必须这样做。


2
投票

@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')

0
投票

我不想使用静态文件或额外的我不理解的 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 环境中为我工作。

© www.soinside.com 2019 - 2024. All rights reserved.