Python SSL 证书设置:cafile, capath, cadata 详解
2025-03-07 12:49:23
如何正确设置 Python 中 SSL Context 的 cafile, capath, cadata 参数?
遇到 SSL 证书验证问题? 很可能与 ssl.create_default_context()
函数中 cafile
、capath
和 cadata
参数的设置有关。下面就聊聊这三个参数,并给出一套可靠的解决方案。
问题
看段代码:
import ssl
ssl_context = ssl.create_default_context(cafile=..., capath=..., cadata=...)
同时,有一个 Docker 容器,证书文件位于:
$ pwd
/app/cert
$ ls
ca.pem cert.pem key.pem
问:cafile
、capath
、cadata
的值分别是什么?
尝试过 cafile="/app/cert/ca.pem"
并且 capath="/app/cert"
,但报错:
[SSL: SSLV3_ALERT_BAD_CERTIFICATE] sslv3 alert bad certificate (_ssl.c:1002)
问题原因分析
ssl.create_default_context()
用于创建默认的 SSL 上下文,其中 cafile
、capath
和 cadata
参数用于指定受信任的证书颁发机构 (CA) 证书。 报错 SSLV3_ALERT_BAD_CERTIFICATE
表明客户端或服务器收到了一个坏的或无法验证的证书。
问题通常出在以下几个方面:
- CA 证书不正确或不完整:
ca.pem
可能不是正确的 CA 证书,或者它缺少了必要的中间证书。 - 路径问题: 即使
cafile
指定的文件存在,Python 进程也可能因为权限问题或路径错误无法访问。 Docker 容器内部路径和宿主机路径可能会有区别。 - 证书链问题: 服务器发送的证书链可能不完整,缺少中间 CA 证书。客户端需要能够通过提供的 CA 证书验证整个证书链。
capath
使用的坑 :使用capath
的时候,目录中的证书需要经过特殊处理。
解决方案
针对上述原因,提供以下几种解决方案:
1. 使用 cafile
指定 CA 证书 (推荐)
这是最直接的方法。 将 cafile
参数设置为 CA 证书文件的绝对路径。
-
原理:
cafile
参数直接指定包含一个或多个 PEM 格式 CA 证书的文件。Python 会加载这些证书,并用它们来验证对端证书。 -
代码示例:
import ssl ssl_context = ssl.create_default_context(cafile="/app/cert/ca.pem")
-
注意: 确保 Python 进程有权限读取该文件。 Docker 容器内部可以使用 volume 挂载,保证路径正确。
2. 使用 capath
指定 CA 证书目录
如果有很多 CA 证书,可以把它们放在一个目录下,用 capath
指定这个目录。
-
原理:
capath
参数指定一个目录,该目录包含多个 PEM 格式的 CA 证书文件。 关键在于:这个目录必须经过openssl rehash
命令(或者等效的 OpenSSL API 调用)处理,建立一个符号链接的哈希表。
这个哈希值允许OpenSSL按需查找和加载证书. -
操作步骤 (重点):
- 准备目录: 将所有 CA 证书(PEM 格式)放入
/app/cert
目录。 - 创建哈希链接: 在 Docker 容器内部,进入
/app/cert
目录,运行:
该指令为每个证书文件根据其哈希值创建一个符号链接。openssl rehash .
- 准备目录: 将所有 CA 证书(PEM 格式)放入
-
代码示例:
import ssl ssl_context = ssl.create_default_context(capath="/app/cert")
-
安全提示:
保证只有可信的CA证书放到此目录, 如果可能, 将此目录设置为只读。
3. 使用 cadata
直接提供 CA 证书数据
cadata
可以直接接收证书数据,而不是指定文件或目录。
-
原理:
cadata
参数可以直接接受字符串或字节对象形式的 PEM 编码的 CA 证书数据。 方便在代码中嵌入证书数据,或从其他来源(如配置中心、环境变量)获取证书数据。 -
代码示例 (字符串形式):
import ssl ca_data = """ -----BEGIN CERTIFICATE----- ... (CA certificate content in PEM format) ... -----END CERTIFICATE----- """ ssl_context = ssl.create_default_context(cadata=ca_data)
-
代码示例 (字节对象形式):
import ssl with open("/app/cert/ca.pem", "rb") as f: ca_data = f.read() ssl_context = ssl.create_default_context(cadata=ca_data)
读取
ca.pem
的内容,将读取到的内容直接传递。
4. 验证并合并 CA 证书 (重要)
很多时候,错误并非来自单个证书文件,而是证书链不完整。 可以尝试将服务器证书、中间证书以及根证书合并到一个 PEM 文件中,再提供给 cafile
。
-
原理 :合并成完整的证书链, 让客户端可以沿着链逐级验证.
-
操作步骤:
-
获取证书链: 可以通过浏览器、
openssl s_client
等工具获取服务器发送的完整证书链。 -
合并证书: 将证书按照从服务器证书到根证书的顺序(服务器证书 -> 中间证书1 -> 中间证书2 -> ... -> 根证书)依次复制粘贴到一个新的 PEM 文件中。 每个证书之间应该用
-----BEGIN CERTIFICATE-----
和-----END CERTIFICATE-----
分隔。 -
使用合并后的文件。
import ssl ssl_context = ssl.create_default_context(cafile="/app/cert/combined_ca.pem")
假设你的服务器证书, 中间CA证书,根CA证书,已经拿到(通过浏览器或者
openssl s_client
),分别保存为server.pem
,intermediate.pem
,root.pem
。
在/app/cert
下合并证书:cat server.pem intermediate.pem root.pem > combined_ca.pem
然后将
cafile
指向combined_ca.pem
即可。 -
5. 检查服务器证书
确保服务器证书本身有效且未过期。
-
使用 OpenSSL 检查:
openssl x509 -in /app/cert/cert.pem -text -noout
查看输出的有效期和颁发者等信息。
进阶:加载系统默认 CA 证书
有些情况下, 你可以不需要手动指定任何cafile
, capath
, cadata
,而是让系统自动选择已经信任的CA.
-
原理 : ssl 模块会自动尝试加载系统的默认的 CA 证书. 不同的操作系统, 其位置不太一样.
-
使用 :
直接调用ssl.create_default_context()
, 不传任何ca*
相关的参数。import ssl ssl_context = ssl.create_default_context() # 不提供 cafile, capath, 或 cadata
这种方法不适合需要明确控制受信任CA的情况, 仅适合不那么敏感的验证场景。
总结
遇到 SSLV3_ALERT_BAD_CERTIFICATE
错误时, 首先要检查 CA 证书是否正确、完整,证书链是否完整,以及文件路径是否正确。然后按需选择 cafile
、capath
或 cadata
来提供 CA 证书。 建议优先使用 cafile
参数,因为简单直接。 使用 capath
的时候,别忘了对证书目录执行 openssl rehash
。 如果证书链不完整,手动合并。 通过这些细致的操作, 就能解决大多数的 SSL 证书验证问题。