返回

Python SSL 证书设置:cafile, capath, cadata 详解

python

如何正确设置 Python 中 SSL Context 的 cafile, capath, cadata 参数?

遇到 SSL 证书验证问题? 很可能与 ssl.create_default_context() 函数中 cafilecapathcadata 参数的设置有关。下面就聊聊这三个参数,并给出一套可靠的解决方案。

问题

看段代码:

import ssl

ssl_context = ssl.create_default_context(cafile=..., capath=..., cadata=...)

同时,有一个 Docker 容器,证书文件位于:

$ pwd
/app/cert
$ ls
ca.pem  cert.pem  key.pem

问:cafilecapathcadata 的值分别是什么?

尝试过 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 上下文,其中 cafilecapathcadata 参数用于指定受信任的证书颁发机构 (CA) 证书。 报错 SSLV3_ALERT_BAD_CERTIFICATE 表明客户端或服务器收到了一个坏的或无法验证的证书。

问题通常出在以下几个方面:

  1. CA 证书不正确或不完整: ca.pem 可能不是正确的 CA 证书,或者它缺少了必要的中间证书。
  2. 路径问题: 即使 cafile 指定的文件存在,Python 进程也可能因为权限问题或路径错误无法访问。 Docker 容器内部路径和宿主机路径可能会有区别。
  3. 证书链问题: 服务器发送的证书链可能不完整,缺少中间 CA 证书。客户端需要能够通过提供的 CA 证书验证整个证书链。
  4. 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按需查找和加载证书.

  • 操作步骤 (重点):

    1. 准备目录: 将所有 CA 证书(PEM 格式)放入 /app/cert 目录。
    2. 创建哈希链接: 在 Docker 容器内部,进入 /app/cert 目录,运行:
      openssl rehash .
      
      该指令为每个证书文件根据其哈希值创建一个符号链接。
  • 代码示例:

    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

  • 原理 :合并成完整的证书链, 让客户端可以沿着链逐级验证.

  • 操作步骤:

    1. 获取证书链: 可以通过浏览器、openssl s_client 等工具获取服务器发送的完整证书链。

    2. 合并证书: 将证书按照从服务器证书到根证书的顺序(服务器证书 -> 中间证书1 -> 中间证书2 -> ... -> 根证书)依次复制粘贴到一个新的 PEM 文件中。 每个证书之间应该用 -----BEGIN CERTIFICATE----------END CERTIFICATE----- 分隔。

    3. 使用合并后的文件。

      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 证书是否正确、完整,证书链是否完整,以及文件路径是否正确。然后按需选择 cafilecapathcadata 来提供 CA 证书。 建议优先使用 cafile 参数,因为简单直接。 使用 capath 的时候,别忘了对证书目录执行 openssl rehash。 如果证书链不完整,手动合并。 通过这些细致的操作, 就能解决大多数的 SSL 证书验证问题。