OpenSSL TLS、证书链与密钥验证工具手册
连接成功不等于证书可信
curl 报证书错误时,常见的第一反应是加 -k。这会让请求继续,却把最重要的判断一起抹掉:服务端究竟发了哪张证书,客户端是否拥有正确的信任锚,URL 中的主机名能否匹配 SAN。OpenSSL 适合做这类首诊,因为它能把 TLS 握手、证书对象和离线构链拆开观察。
Linux 实验使用 OpenSSL 3.x。Ubuntu/Debian 可执行 sudo apt-get update && sudo apt-get install openssl ca-certificates curl,RHEL 系使用 sudo dnf install openssl ca-certificates curl。安装需要管理员权限,后续生成材料与启动高端口服务均使用普通用户。先确认当前终端调用的是哪一个构建;相同命令名不代表相同配置目录和 provider。
command -v openssl
openssl version -a
openssl list -providersversion -a 会给出版本、平台、编译选项、OPENSSLDIR 和模块目录。OpenSSL 4.0 是常规发行线,3.5 是 LTS 发行线;项目应根据操作系统供应商、FIPS 要求和依赖兼容选择分支,而不是在脚本里假定所有机器都是最新版。官方发行生命周期和下载页是判断支持状态的入口。
在私有目录准备一套测试身份
在新目录执行下列命令。CA 只用于本次实验,不执行系统信任导入;私钥与 PKCS12 均不提交仓库。生产证书应由组织 CA 或受支持的 ACME 流程签发,完整签发和服务更新见 SSL 证书。
umask 077
mkdir tls-lab && cd tls-lab
mkdir certs .local
openssl req -x509 -newkey rsa:2048 -noenc -days 2 \
-subj '/CN=Temporary Lab Root' \
-addext 'basicConstraints=critical,CA:TRUE' \
-addext 'keyUsage=critical,keyCertSign,cRLSign' \
-keyout .local/root-key.pem -out certs/dev-root.pem
openssl req -new -newkey rsa:2048 -noenc \
-subj '/CN=dev.local.test' \
-keyout certs/dev.local.test-key.pem -out .local/server.csr
printf '%s\n' \
'basicConstraints=critical,CA:FALSE' \
'keyUsage=critical,digitalSignature,keyEncipherment' \
'extendedKeyUsage=serverAuth' \
'subjectAltName=DNS:dev.local.test,DNS:localhost,IP:127.0.0.1' \
>.local/server.ext
openssl x509 -req -in .local/server.csr \
-CA certs/dev-root.pem -CAkey .local/root-key.pem -set_serial 1 \
-days 1 -extfile .local/server.ext -out certs/dev.local.test.pem
openssl verify -CAfile certs/dev-root.pem -purpose sslserver \
-verify_hostname dev.local.test certs/dev.local.test.pem
openssl s_server -accept 127.0.0.1:8443 -www \
-cert certs/dev.local.test.pem -key certs/dev.local.test-key.pemverify 应输出 OK,最后一条命令保持前台监听。另开终端进入同一 tls-lab 目录再执行下一节;端口已占用时先停止并确认占用者,不覆盖已有服务。短期测试根直接签发叶子,所以此实验没有中间 CA;后文的 root-ca.pem 和 intermediate-ca.pem 只用于已有组织证书链的替换示例。req 与证书扩展语义见 OpenSSL req。
用严格握手同时验证 SNI、链和主机名
下面这条命令刻意同时给出 TCP 目标、SNI、信任锚和期望主机名:
openssl s_client \
-connect 127.0.0.1:8443 \
-servername dev.local.test \
-CAfile certs/dev-root.pem \
-verify_return_error \
-verify_hostname dev.local.test \
-brief </dev/null-connect 解决“连到哪里”,-servername 决定 ClientHello 中的 SNI,虚拟主机据此选择证书。-CAfile 明确信任锚,-verify_hostname 检查服务身份,-verify_return_error 让验证错误真正中止握手。没有最后一个参数时,s_client 会尽量继续,以便展示更多诊断信息;这对人工排查有用,却不适合作为自动门禁的成功条件。s_client 官方手册明确区分了这两种行为。
成功输出会出现验证通过和协商协议。Connection refused 发生在 TLS 之前,应该回到监听端口和路由;issuer 相关错误说明构链缺材料或信任锚不对;hostname mismatch 说明证书已经可以构链,但 SAN 不包含当前访问名。这三类故障不能靠重复导入同一张根证书互相修复。
-showcerts 只打印服务端实际发送的证书列表,而且保持服务端发送顺序。它不是经过验证的证书链。把这条命令的输出截图当“证书正常”是很常见的误判:服务端可能漏发中间证书,列表中也可能存在无关证书,客户端仍需从自己的信任锚构造有效路径。
openssl s_client \
-connect service.example.test:443 \
-servername service.example.test \
-showcerts </dev/null需要经过 HTTP CONNECT 代理时,显式增加 -proxy proxy.example.test:3128。代理认证如果使用 Basic,凭据在 TLS 隧道建立前只经过可逆编码;口令应通过 OpenSSL 支持的受控 password source 提供,不能直接写入共享命令历史。
x509 读取证书,verify 构造路径
openssl x509 处理的是单张证书。它适合确认 subject、issuer、序列号、SHA-256 指纹、SAN、用途和有效窗口,不负责证明服务端正在使用这张文件。
openssl x509 -in certs/dev.local.test.pem -noout \
-subject -issuer -serial -fingerprint -sha256 \
-ext subjectAltName,extendedKeyUsage -dates如果文件是 DER 编码,增加 -inform DER;不要仅凭 .cer、.crt 或 .pem 扩展名推断实际编码。证书更新时至少对比指纹、SAN、issuer、序列号和公钥,避免把“文件改了”误当成“身份按预期轮换”。
离线链验证由 openssl verify 完成。先对本次实验的根和叶子证书重做校验:
openssl verify -show_chain -purpose sslserver \
-CAfile certs/dev-root.pem -no-CApath -no-CAstore \
-verify_hostname dev.local.test certs/dev.local.test.pem应输出 certs/dev.local.test.pem: OK,链中只有叶子和本次测试根。已有组织证书若包含中间 CA,先把真实根、全部中间证书和叶子分别保存为以下三个文件,再运行对应命令;它们不是前一节生成的文件:
openssl verify -show_chain -purpose sslserver \
-CAfile certs/root-ca.pem \
-no-CApath -no-CAstore \
-untrusted certs/intermediate-ca.pem \
-verify_hostname service.example.test \
certs/server.pemverify 手册把 -untrusted 定义为只参与构链、并不自动成为信任锚的证书来源。删除它后若稳定出现 issuer 错误,就证明服务或证据包缺少中间证书。反过来,把中间 CA 直接塞进 -CAfile 可能让实验通过,却改变了组织原本要求的信任边界。
用 s_server 做一个可丢弃的正反实验
证书问题不必先接入完整框架。使用前面生成的材料;如果前面的 s_server 仍在运行,不要重复启动。已停止时,在同一目录重新运行:
openssl s_server -accept 127.0.0.1:8443 -www \
-cert certs/dev.local.test.pem \
-key certs/dev.local.test-key.pem另一个终端先执行严格握手,再把期望名称改成证书中不存在的 api.local.test:
openssl s_client -connect 127.0.0.1:8443 \
-servername dev.local.test \
-CAfile certs/dev-root.pem \
-verify_return_error \
-verify_hostname api.local.test </dev/null如果结果是 hostname mismatch,说明信任锚和签名路径没有被破坏,失败只来自服务身份。再生成另一张无关的测试根,用它做不受信反例;不依赖容器里是否恰好安装过开发根证书:
openssl req -x509 -newkey rsa:2048 -noenc -days 1 \
-subj '/CN=Other Lab Root' \
-addext 'basicConstraints=critical,CA:TRUE' \
-addext 'keyUsage=critical,keyCertSign,cRLSign' \
-keyout .local/other-root-key.pem -out certs/other-root.pem
openssl s_client -connect 127.0.0.1:8443 \
-servername dev.local.test \
-CAfile certs/other-root.pem -no-CApath -no-CAstore \
-verify_return_error -verify_hostname dev.local.test </dev/null第二次握手应因无法构造到受信根的路径而非零退出;如果仍然成功,检查是否修改过系统信任或误用了测试根。两个反例分别控制一个变量,比“浏览器能不能打开”更容易定位问题。
s_server 是测试工具,不是生产入口。它适合验证证书文件、TLS 参数和客户端行为,不承担热更新、访问日志、限流、进程守护或密钥托管。
证书和私钥必须证明是一对
文件名相同不构成密钥配对证据。可以分别导出叶子证书和私钥的公钥,转换成同一种 DER 表示后计算摘要:
openssl x509 -in certs/dev.local.test.pem -pubkey -noout \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256
openssl pkey -in certs/dev.local.test-key.pem -pubout -outform DER \
| openssl dgst -sha256
openssl pkey -in certs/dev.local.test-key.pem -check -noout前两个摘要必须一致,第三条检查私钥内部一致性。加密私钥默认让命令交互询问口令,或者使用权限受控的文件、环境或 secret provider;-passin pass:... 会把口令暴露在命令历史、进程参数和日志中。
Java、Windows 和部分代理软件常使用 PKCS12 身份包。OpenSSL 可以把私钥、叶子证书和中间链装进同一个 .p12,但根证书通常不应作为服务身份的一部分发送:
openssl pkcs12 -export \
-inkey certs/dev.local.test-key.pem \
-in certs/dev.local.test.pem \
-name server-tls \
-out .local/server.p12
openssl pkcs12 -in .local/server.p12 -info -noout交互输入导出口令后,第二条命令应能重新读取包的信息。本次叶子由测试根直接签发,没有中间证书;组织证书若有中间链,再用 -certfile 附加对应文件。-export 表示创建 PKCS12;没有它时,命令按解析输入包处理。pkcs12 手册还说明 -certfile 只是附加证书材料。旧系统如果只支持遗留算法,-legacy 可以帮助读取或导出,但这应被视为迁移信号,而不是给新制品的默认配置。
项目里保存验证方法,不保存密钥
仓库适合提交证书生成说明、测试域名、校验脚本和示例配置,不适合提交真实私钥、个人开发 CA 或生产证书包。脚本应从受控位置接收路径,并让失败保留非零退出码:
openssl x509 -in "$CERT_FILE" -noout -ext subjectAltName
openssl verify -CAfile "$CA_FILE" "$CERT_FILE"
openssl x509 -in "$CERT_FILE" -checkend "$RENEW_WINDOW_SECONDS" -noout
openssl pkey -in "$KEY_FILE" -check -noout续期窗口来自证书生命周期、变更审批和发布提前量,不能全公司共享一个没有容量依据的固定数字。对外服务还要从真实客户端网络执行严格 s_client,因为离线文件验证无法证明负载均衡器已经加载新证书,也无法发现某个节点仍在发送旧链。
本地开发证书可以交给 mkcert 管理;Java truststore、keystore 和 alias 生命周期应进入 keytool。OpenSSL 在这条链路中的职责是观察、转换和验证,不是替代组织 CA、自动续期平台或 Java 信任库管理器。
退出时清掉进程、文件和临时信任
先停止 s_server 和所有读取测试私钥的进程,再删除临时叶子证书、私钥、PKCS12 文件、导出的握手日志和证书列表。调试输出可能包含内部域名、证书主体、代理地址和会话信息,不能当普通构建日志长期保存。
如果实验曾把 CA 加进系统或运行时信任库,删除证书文件不等于撤销信任。必须回到真正持有状态的工具执行删除,并在新的客户端进程中重新握手,确认结果从成功变成明确的不受信错误。只有这个反证成立,清理才算完成。
权威资料与规范地址
安装、版本与产品能力
| 查阅内容 | 官方地址 |
|---|---|
| 发行生命周期 | https://openssl-library.org/roadmap/index.html |
| 下载页 | https://openssl-library.org/source/ |
