SQLx 测试 TLS 证书完全指南:生成、校验与 MySQL/PostgreSQL 集成实战
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 签发的证书。解决方案便是:
- 生成一个自签名根 CA(
ca.crt+ca.key); - 用该 CA 签发服务器证书与客户端证书;
- 测试时把
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 注入测试容器:
- tests/postgres/Dockerfile 将
server.crt、ca.crt、server.key拷贝到/var/lib/postgresql/并设置属主与0600权限; - tests/mysql/Dockerfile 将证书拷贝到
/etc/mysql/ssl/,并通过 tests/mysql/my.cnf 的ssl-ca/ssl-cert/ssl-key三项配置启用 MySQL 服务端 TLS。
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 依靠它完成主机名校验;
- 该主机名仅在启用身份验证模式时才会被检查:
- MySQL / MariaDB:
ssl-mode=verify_identity(见 sqlx-mysql/src/options/ssl_mode.rs 中MySqlSslMode::VerifyIdentity的定义); - PostgreSQL:
sslmode=verify-full(见 sqlx-postgres/src/options/ssl_mode.rs 中PgSslMode::VerifyFull的定义)。
- MySQL / MariaDB:
也就是说,如果测试环境希望验证"服务器证书与所连接主机名匹配",必须将 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 基础设施可以概括为三个环节:
- 证书供给:
tests/certs/下由同一个自签名 CA(ca.crt)签发的服务器证书与客户端证书,分别部署到测试数据库容器(见 Dockerfile)与测试客户端(见 x.py 的 URL 参数); - 连接校验:客户端通过
sslrootcert/ssl-ca加载信任根,按verify-ca/verify_ca或verify-full/verify_identity逐级校验服务器证书链与主机名(SAN); - 身份认证:PostgreSQL 的
hostssl ... cert规则(pg_hba.conf)要求客户端证书 CN 等于数据库用户名,而 MySQL 5.7 场景则依赖rsa/子目录下的 RSA 证书集绕过 Ed25519 兼容性问题。
当证书过期(2035 年)或需要自定义主机名、CN 时,按 README 给出的三条 OpenSSL 命令重新生成即可,唯一需要留意的两个"坑":客户端证书 CN 必须与数据库用户名一致、服务器证书必须携带 subjectAltName=DNS:... 扩展并开启对应的身份验证模式。