SQLx 测试 TLS 证书完全指南:生成、校验与 MySQL/PostgreSQL 集成实战

原创2026-10-02 21:58:051,250 阅读
文章标签:数据库后端

SQLx 测试 TLS 证书完全指南:生成、校验与 MySQL/PostgreSQL 集成实战

本指南以 SQLx 仓库中 tests/certs/README.md 为骨架,系统讲解 SQLx 测试套件中 TLS 证书的组织方式、用途边界与重新生成方法,并结合仓库源码(测试驱动脚本、Dockerfile、连接选项实现)揭示这些证书如何被真实用于 PostgreSQL / MySQL 的 SSL 与客户端证书认证测试。读完本文,你将掌握:SQLx 测试证书的目录结构与安全约束、OpenSSL 生成 CA/客户端/服务器证书的完整命令行流程、x509 v3 扩展(Subject Alternative Name)与 verify_identity / verify-full 主机名校验之间的关系,以及 MySQL 5.7 为何必须使用独立的 RSA 证书集。

证书文件与目录结构

SQLx 将测试用 TLS 证书集中存放在仓库根目录下的 tests/certs 目录中,完整布局如下:

tests/certs/
├── README.md          # 本指南对应的说明文档
├── ca.crt             # 自签名的根 CA 证书(用于签发 client.crt 与 server.crt)
├── client.crt         # 由 ca.crt 签发的客户端证书
├── server.crt         # 由 ca.crt 签发的服务器证书
├── keys/              # 对应私钥(公开可读,仅限测试环境)
│   ├── ca.key         # CA 私钥
│   ├── client.key     # 客户端私钥
│   └── server.key     # 服务器私钥
└── rsa/               # MySQL 5.7 专用 RSA 证书集(同样含 ca/client/server 三件套与 keys/)
    ├── ca.crt
    ├── client.crt
    ├── server.crt
    └── keys/
        ├── ca.key
        ├── client.key
        └── server.key

三个核心证书的职责划分如下:

文件 角色 说明
ca.crt 证书颁发机构(CA) 自签名根证书,测试中作为信任根(trust root)用于校验对端证书链
client.crt 客户端证书 由 ca.crt 签发,用于客户端证书认证(cert 认证)场景
server.crt 服务器证书 由 ca.crt 签发,部署到 PostgreSQL / MySQL 服务端启用 TLS

keys/ 目录存放三个证书对应的私钥。按照 README.md 的明确说明:这些证书仅限在 SQLx 测试中使用,不得用于测试之外的任何生产或开发环境,因为私钥已公开存放在仓库中,任何持有私钥的人都能伪造相应的证书身份。

证书有效期截至 2035 年,因此在未来一段时间内,重新生成证书的需求主要出现在:开发者本地证书丢失、需要自定义主机名 / CN 字段、或需要适配特定数据库版本时(见下文 MySQL 5.7 一节)。

为什么测试 TLS 需要"自造"证书

SQLx 的集成测试需要验证真实的 TLS 握手、证书链校验与客户端证书认证行为,但测试环境无法依赖公共 CA 签发的证书。解决方案便是:

  1. 生成一个自签名根 CA(ca.crt + ca.key);
  2. 用该 CA 签发服务器证书与客户端证书;
  3. 测试时把 ca.crt 作为信任根传入客户端连接选项,把 server.crt 部署到测试数据库容器。

从仓库的 tests/x.py 可以清楚看到这一套路的实际使用:测试脚本在启动数据库服务后,把包含证书参数的连接 URL 写入 DATABASE_URL 环境变量再运行 cargo test。例如 PostgreSQL 的服务器证书验证测试使用:

sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt

而客户端证书认证(cert 认证)测试则追加客户端私钥与证书:

sslmode=verify-ca&sslrootcert=.%2Ftests%2Fcerts%2Fca.crt
&sslkey=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&sslcert=.%2Ftests%2Fcerts%2Fclient.crt

MySQL 侧同样在 tests/x.py 中拼接 ssl-mode=required(普通 TLS 加密)以及 sslmode=verify_ca&ssl-ca=...&ssl-key=...&ssl-cert=...(客户端证书认证)两类 URL。注意 URL 中路径必须做百分号编码(如 / 写作 %2F、. 写作 %2E 或直接以 . 形式给出),这与 sqlx-postgres/src/options/doc.md 中关于 URL 参数与 libpq 兼容性的说明一致。

服务端一侧,证书通过 Dockerfile 注入测试容器:

PostgreSQL 的客户端证书认证还依赖 tests/postgres/pg_hba.conf 中的认证规则:

local   all all     trust
hostssl all all all cert

hostssl ... cert 表示仅对 SSL 连接启用"客户端证书即身份"的认证方式——这正是测试 client.crt / client.key 的场景。从源码结构可以推断,这条规则仅用于证书认证测试,并刻意省略了非 SSL 的 host 行,以避免回退到非证书认证。

x509 v3:RusTLS 与 OpenSSL 的硬性要求

README 特别强调了一条环境约束:

RusTLS requires TLS certificates to be x509v3. OpenSSL 3.2 and up create v3 certificates by default.

SQLx 的 TLS 后端之一 RusTLS(对应 tls-rustls 系列特性)要求证书符合 x509 v3 格式;而 OpenSSL 从 3.2 版本起默认生成的证书即为 v3,因此使用 3.2 及以上版本的 OpenSSL 执行下文命令即可满足要求。若使用更早版本的 OpenSSL,则需要在生成时显式指定相关 v3 扩展(尤其是 Subject Alternative Name,见下文服务器证书一节)。

MySQL 5.7 的特殊性:默认证书集采用 Ed25519 签名算法,MySQL 5.7 无法加载。因此仓库在 tests/certs/rsa/ 下单独维护了一套 RSA 签名的 CA / 客户端 / 服务器证书,仅供 MySQL 5.7 的 client-SSL 测试目标使用。这一点在 tests/x.py 中有明确体现——当被测版本为 5_7 时,测试脚本会把 URL 中的证书路径整体切换到 rsa/ 子目录:

ssl-ca=.%2Ftests%2Fcerts%2Frsa%2Fca.crt
ssl-key=.%2Ftests%2Fcerts%2Frsa%2Fkeys%2Fclient.key
ssl-cert=.%2Ftests%2Fcerts%2Frsa%2Fclient.crt

同时,由于 MySQL 5.7 的 yaSSL 仅支持 TLSv1.1,脚本还会在 rustls 后端下跳过该版本的 SSL 测试组合(注释中引用了 docker-library/mysql 的相关 issue)。

重新生成证书:OpenSSL 完整流程

README 给出了三套 OpenSSL 命令,用于在需要时重新生成整组证书。执行这些命令前,请先确认工作目录为 tests/certs(下文命令中的相对路径均基于该目录)。

OpenSSL 交互提示字段

运行 openssl req 时,OpenSSL 会逐个询问证书主题(DN,Distinguished Name)字段:

Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:

README 指出:这些字段纯属信息性内容,通常可以留空或用任意值填充,唯一的例外是客户端证书的 Common Name(详见下文)。若想跳过全部字段,可以输入 .(点号)表示留空。

1. 生成 CA 证书(自签名根证书)

openssl req -x509 -sha256 -days 3650 -key keys/ca.key -out ca.crt
  • -x509:直接输出自签名 X.509 证书,而不是 CSR;
  • -sha256:摘要算法使用 SHA-256;
  • -days 3650:有效期 3650 天(约 10 年,与 README 中"有效期至 2035 年"的约定一致);
  • -key keys/ca.key:使用仓库 keys/ 目录下已存在的 CA 私钥完成签名。

生成后的 ca.crt 在测试中作为信任根传入客户端(即上文 x.py 中 sslrootcert / ssl-ca 参数指向的文件)。

2. 生成客户端证书(CSR + CA 签名)

关键约束:PostgreSQL 的证书认证(cert 认证)要求客户端证书的 Common Name(CN)字段与数据库用户名一致。 README 引用了 PostgreSQL 官方文档中 auth-cert.html 一节的说明,在 SQLx 的测试环境中,数据库用户名固定为 postgres,因此在提示 Common Name (e.g. server FQDN or YOUR name) []: 时必须输入 postgres。

先创建证书签名请求(CSR):

openssl req -key keys/client.key -new -out client.csr

再用 CA 私钥和 CSR 签发证书:

openssl x509 -req -CA ca.crt -CAkey keys/ca.key -in client.csr -out client.crt -days 3650 -CAcreateserial
  • -CA ca.crt -CAkey keys/ca.key:指定签发方 CA 及其私钥;
  • -CAcreateserial:为 CA 生成并维护序列号文件(ca.srl),确保后续签发的证书序列号不重复;
  • 生成的 client.crt 由 ca.crt 背书,测试中配合 keys/client.key 使用。

3. 生成服务器证书(CSR + SAN 扩展)

服务器证书同样先创建 CSR,但必须携带 subjectAltName 扩展:

openssl req -key keys/server.key -new -out server.csr -addext subjectAltName=DNS:sqlx.rs

然后由 CA 签发:

openssl x509 -req -CA ca.crt -CAkey keys/ca.key -in server.csr -out server.crt -days 3650 -CAcreateserial

subjectAltName=DNS:sqlx.rs 是关键所在:

  • 它是 x509 v3 扩展,RusTLS 依靠它完成主机名校验;
  • 该主机名仅在启用身份验证模式时才会被检查:

也就是说,如果测试环境希望验证"服务器证书与所连接主机名匹配",必须将 SAN 中的 DNS 名与实际连接的主机对齐(sqlx.rs 是仓库约定的测试主机名),并在连接 URL 中开启对应的身份校验模式。

4. 清理临时文件

生成过程中会产生 client.csr、server.csr 以及 ca.srl 等中间文件。这些临时产物不属于最终证书集,可在确认证书生成无误后按需清理(仓库中并不包含 CSR 文件)。

SSL 验证模式与证书的对应关系

为了让读者理解证书在连接选项中的角色,这里结合 SQLx 源码补充说明两端数据库的 SSL 模式枚举(这也是 README 中 verify_identity / verify-full 说法的落点)。

PostgreSQL(PgSslMode):

模式 行为
disable 只尝试非 SSL 连接
allow 先尝试非 SSL,失败再尝试 SSL
prefer 先尝试 SSL,失败回退非 SSL(默认值)
require 只尝试 SSL;若提供根 CA 文件则按 verify-ca 方式校验
verify-ca 只尝试 SSL,且校验服务器证书由受信任 CA 签发(信任根来自 sslrootcert)
verify-full 在 verify-ca 基础上,额外校验连接的主机名与证书 SAN 匹配

MySQL / MariaDB(MySqlSslMode):

模式 行为
disabled 建立未加密连接
preferred 服务端支持加密则加密,否则回退明文(默认值)
required 必须加密,否则连接失败
verify_ca 在 required 基础上校验服务器 CA 证书与配置的 CA 匹配
verify_identity 在 verify_ca 基础上,额外校验连接主机名与证书身份匹配

两者在 URL 解析上的对应实现分别位于 sqlx-postgres/src/options/parse.rs(接受 sslmode、sslrootcert、sslcert、sslkey,也兼容 ssl-ca / ssl-cert / ssl-key 拼写)与 sqlx-mysql/src/options/parse.rs(接受 ssl-mode、ssl-ca、ssl-cert、ssl-key)。这些参数在测试中的典型组合,正是 README 所述证书集与 tests/x.py 中 DATABASE_URL 参数拼接的完整闭环。

小结:证书、命令与验证链路的完整对应

回顾整条链路,SQLx 测试 TLS 基础设施可以概括为三个环节:

  1. 证书供给:tests/certs/ 下由同一个自签名 CA(ca.crt)签发的服务器证书与客户端证书,分别部署到测试数据库容器(见 Dockerfile)与测试客户端(见 x.py 的 URL 参数);
  2. 连接校验:客户端通过 sslrootcert / ssl-ca 加载信任根,按 verify-ca / verify_ca 或 verify-full / verify_identity 逐级校验服务器证书链与主机名(SAN);
  3. 身份认证:PostgreSQL 的 hostssl ... cert 规则(pg_hba.conf)要求客户端证书 CN 等于数据库用户名,而 MySQL 5.7 场景则依赖 rsa/ 子目录下的 RSA 证书集绕过 Ed25519 兼容性问题。

当证书过期(2035 年)或需要自定义主机名、CN 时,按 README 给出的三条 OpenSSL 命令重新生成即可,唯一需要留意的两个"坑":客户端证书 CN 必须与数据库用户名一致、服务器证书必须携带 subjectAltName=DNS:... 扩展并开启对应的身份验证模式。

登录后查看全文
sqlx