HTTPS 实战:TLS 握手成本、自制 CA 与证书、三大常见故障
摘要 :HTTPS 是 HTTP 加上 TLS 加密,但"加"这一层究竟要付出多少代价?为什么会看到"证书不安全"的告警?本文在真实 ECS 上完成三件事:用
curl -w量化 TLS 握手耗时、从零搭建一条「根 CA → 服务器证书」的完整信任链并用 nginx 启用 HTTPS、逐一复现自签名/证书链不完整/证书过期三类经典故障。全部命令与输出均为原文采集。
一、背景:HTTPS 到底是什么,贵在哪
HTTPS = HTTP + TLS(传输层安全)。TLS 在 TCP 与应用层之间加了一层加密,带来三个能力:机密性 (内容加密)、完整性 (防篡改)、身份认证(证书证明"你连的是谁")。
但加密不是免费的:TLS 需要在传输数据之前完成一次握手------协商版本与密码套件、交换密钥、校验证书。这一篇就从"贵在哪"开始量起。
实验环境 :root@113.44.176.3(Ubuntu 24.04.4 LTS,x86_64,8C16G),依赖已就绪:
text
$ openssl version
OpenSSL 3.0.13 30 Jan 2024
$ curl --version | head -1
curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13 ... nghttp2/1.59.0 ...
$ nginx -v
nginx version: nginx/1.24.0 (Ubuntu)
$ nginx -V 2>&1 | tr " " "\n" | grep -E "http_v2|http_ssl"
--with-http_ssl_module
--with-http_v2_module
nginx 1.24.0 自带
http_ssl与http_v2模块,天然支持 TLS + ALPN 协商。
二、分步骤实验
实验 1:量化 TLS 握手耗时------HTTPS 到底"贵"多少
对公网 https://www.baidu.com 测量 DNS、TCP 连接、TLS 握手各阶段耗时:
bash
curl -s -o /dev/null -w 'code=%{http_code} dns=%{time_namelookup} connect=%{time_connect} appconnect=%{time_appconnect} total=%{time_total}\n' https://www.baidu.com/
text
code=200 dns=0.001137 connect=0.013080 appconnect=0.043866 total=0.056988
各字段含义(curl 手册):
time_namelookup:DNS 解析耗时time_connect:TCP 三次握手完成耗时time_appconnect:TLS 握手完成 耗时(appconnect - connect即为纯 TLS 握手开销)time_total:整体耗时
本次:TCP 三次握手约 13ms,TLS 握手使其累积到 43.9ms,即纯 TLS 握手约 31ms,TLS 是端到端延迟的大头。
为了剥离网络抖动、放大"加密与握手"的纯 CPU 成本,改到本机回环(RTT≈0)对比 HTTP 明文与 HTTPS:
bash
# HTTP/1.1 明文(80)
for i in 1 2 3 4 5; do curl -s -o /dev/null -w "connect=%{time_connect} appconnect=%{time_appconnect} total=%{time_total}\n" http://localhost/; done
# HTTPS(443)
for i in 1 2 3 4 5; do curl -s -o /dev/null -w "connect=%{time_connect} appconnect=%{time_appconnect} total=%{time_total}\n" https://localhost/; done
text
--- HTTP/1.1 (80, 明文) ---
connect=0.000095 appconnect=0.000000 total=0.000194
connect=0.000091 appconnect=0.000000 total=0.000180
connect=0.000084 appconnect=0.000000 total=0.000168
(均值 total ≈ 0.0002s = 0.2ms)
--- HTTPS (443, TLS) ---
connect=0.000067 appconnect=0.018021 total=0.018236
connect=0.000064 appconnect=0.017930 total=0.018140
connect=0.000063 appconnect=0.017827 total=0.017968
(均值 appconnect ≈ 0.0178s = 17.8ms)
讲解 :回环下明文 HTTP 总耗时约 0.2ms,而 HTTPS 的 TLS 握手(RSA-2048 密钥交换 + 证书校验)就要约 17.8ms,一次握手成本约为百倍 。这直观说明"HTTPS 更贵"------但贵在握手阶段,一旦连接建立,对称加密(AES-GCM)的附加开销几乎可忽略。因此实践中用"会话复用 / TLS 1.3 一次 RTT / ECDHE"来摊薄握手成本。
实验 2:openssl 查看证书链
真实公网站点(百度)的证书链:
bash
echo | openssl s_client -connect www.baidu.com:443 -servername www.baidu.com -showcerts 2>/dev/null \
| grep -iE "subject=|issuer=|Protocol|Cipher|Verify return"
text
subject=C = CN, ST = Beijing, L = Beijing, O = "Beijing Baidu Netcom Science Technology Co., Ltd.", CN = baidu.com
issuer=C = BE, O = GlobalSign nv-sa, CN = GlobalSign RSA OV SSL CA 2018
Protocol : TLSv1.2
Cipher : ECDHE-RSA-AES128-GCM-SHA256
Verify return code: 0 (ok)
能看到「叶子证书 subject=baidu.com → 上级签发者 issuer=GlobalSign RSA OV SSL CA 2018」这条信任链,Verify return code: 0 (ok) 表示系统信任链校验通过。
实验 3:从零搭建完整 PKI(自制根 CA → 签发服务器证书 → nginx 启用 HTTPS)
接下来完整模拟一个真实 PKI:根 CA 自签 → 用根 CA 签发服务器证书 → nginx 加载 → 客户端校验信任链通过。
① 生成根 CA:
bash
mkdir -p /root/lab/ca /root/lab/server && cd /root/lab
# 1. 根 CA 私钥
openssl genrsa -out ca/ca.key 2048
# 2. 根 CA 自签名证书(有效期 10 年)
openssl req -x509 -new -nodes -key ca/ca.key -sha256 -days 3650 \
-subj "/C=CN/O=MyLab/CN=MyLab Root CA" -out ca/ca.crt
# 3. 查看
openssl x509 -in ca/ca.crt -noout -subject -issuer -dates
text
subject=C = CN, O = MyLab, CN = MyLab Root CA
issuer=C = CN, O = MyLab, CN = MyLab Root CA # 自签名:issuer == subject
notBefore=Sep 19 05:13:10 2026 GMT
notAfter=Sep 16 05:13:10 2036 GMT
② 生成带 SAN 的服务器证书:
现代浏览器/客户端强制校验 subjectAltName,所以必须签发 SAN(含 localhost 与 ECS 公网/内网 IP):
bash
cat > server/san.cnf <<'EOF'
[req]
distinguished_name = dn
req_extensions = req_ext
prompt = no
[dn]
C = CN
O = MyLab
CN = localhost
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = www.mylab.test
IP.1 = 113.44.176.3
IP.2 = 192.168.0.229
EOF
# 4. 服务器私钥
openssl genrsa -out server/server.key 2048
# 5. CSR
openssl req -new -key server/server.key -config server/san.cnf -out server/server.csr
# 6. 校验 SAN 已写入
openssl req -in server/server.csr -noout -text | grep -A1 "Subject Alternative Name"
text
X509v3 Subject Alternative Name:
DNS:localhost, DNS:www.mylab.test, IP Address:113.44.176.3, IP Address:192.168.0.229
③ 根 CA 签发服务器证书 + 校验信任链:
bash
# 7. 用根 CA 签发(带 SAN 扩展,有效期 1 年)
openssl x509 -req -in server/server.csr -CA ca/ca.crt -CAkey ca/ca.key -CAcreateserial \
-out server/server.crt -days 365 -sha256 -extfile server/ext.cnf
# 8. 查看签出的证书
openssl x509 -in server/server.crt -noout -subject -issuer -dates
# 9. 信任链校验
openssl verify -CAfile ca/ca.crt server/server.crt
text
subject=C = CN, O = MyLab, CN = localhost
issuer=C = CN, O = MyLab, CN = MyLab Root CA
notBefore=Sep 19 05:13:27 2026 GMT
notAfter=Sep 19 05:13:27 2027 GMT
server/server.crt: OK
④ 客户端建立对根 CA 的信任 + nginx 配置 HTTPS:
bash
# 把根 CA 装入系统信任库(curl/openssl 随即不再告警)
cp /root/lab/ca/ca.crt /usr/local/share/ca-certificates/mylab-root-ca.crt
update-ca-certificates
nginx 配置(/etc/nginx/conf.d/lab.conf,节选):
nginx
server {
listen 443 ssl http2; # 关键:http2 通过 listen 参数启用(ON by ALPN)
listen [::]:443 ssl http2;
server_name _;
root /var/www/lab;
index index.html;
ssl_certificate /root/lab/server/server.crt;
ssl_certificate_key /root/lab/server/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / { }
}
启动并验证:
bash
rm -f /etc/nginx/sites-enabled/default && nginx -t && systemctl restart nginx
ss -tlnp | grep -E ":(80|443|8443)\b"
text
LISTEN 0 511 0.0.0.0:80 ... nginx
LISTEN 0 511 0.0.0.0:443 ... nginx
LISTEN 0 511 [::]:443 ... nginx
⑤ 信任链验证(信任后访问成功、ALPN 协商 h2):
bash
curl -s -o /dev/null -w "HTTP=%{http_version} code=%{http_code}\n" https://localhost/
curl -sv https://localhost/ -o /dev/null 2>&1 | grep -iE "ALPN|SSL connection|subject|issuer"
text
HTTP=2 code=200
* ALPN: curl offers h2,http/1.1
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
* ALPN: server accepted h2
* subject: C=CN; O=MyLab; CN=localhost
* issuer: C=CN; O=MyLab; CN=MyLab Root CA
讲解 :从「根 CA 自签 → 签发服务器证书 → 客户端装信任 → nginx 加载 → ALPN 协商 h2」跑通了一整条 HTTPS 信任链。Verify return code: 0 (ok) 与 ALPN: server accepted h2 是关键证据。注意信任链的核心逻辑:客户端信任的是根 CA,只要服务器证书能回溯到被信任的根,校验即通过------这解释了为什么"装一个根证书"就能让自制证书变得可信。
实验 4:HTTPS 三大常见故障现场
在 nginx 上分别用三个端口制造问题证书:
| 端口 | 证书类型 | 期望现象 |
|---|---|---|
| 8444 | 自签名证书(未入信任库) | self-signed certificate |
| 8445 | 叶子证书(中间 CA 未下发) | unable to get local issuer certificate |
| 8446 | 已过期证书 | certificate has expired |
① 自签名证书告警:
bash
# 生成自签名证书(issuer == subject)
openssl req -x509 -newkey rsa:2048 -nodes -days 365 -subj "/CN=localhost" \
-keyout server/selfsigned.key -out server/selfsigned.crt \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:113.44.176.3"
# nginx 8444 端口加载它,然后:
curl https://localhost:8444/ -o /dev/null; echo "exit=$?"
text
curl: (60) SSL certificate problem: self-signed certificate
curl failed to verify the legitimacy of the server ...
exit=60
② 证书链不完整(根 CA → 中间 CA → 叶子 三级链,但 nginx 只下发叶子证书):
bash
# 中间 CA:根 CA 签发
openssl genrsa -out inter/inter.key 2048
openssl req -new -key inter/inter.key -subj "/C=CN/O=MyLab/CN=MyLab Intermediate CA" -out inter/inter.csr
openssl x509 -req -in inter/inter.csr -CA ca/ca.crt -CAkey ca/ca.key -CAcreateserial \
-out inter/inter.crt -days 3650 -sha256 \
-extfile <(printf "basicConstraints=CA:TRUE\nkeyUsage=keyCertSign,cRLSign\n")
# 叶子证书:中间 CA 签发
openssl x509 -req -in server/leaf.csr -CA inter/inter.crt -CAkey inter/inter.key -CAcreateserial \
-out server/leaf.crt -days 365 -sha256 -extfile server/leaf-ext.cnf
# nginx 8445 只配 ssl_certificate server/leaf.crt,然后:
curl https://localhost:8445/ -o /dev/null; echo "exit=$?"
text
curl: (60) SSL certificate problem: unable to get local issuer certificate
exit=60
用 openssl verify 精确定位"缺中间证书"和"补上即通过":
bash
openssl verify -CAfile ca/ca.crt server/leaf.crt # 缺中间 CA
openssl verify -CAfile ca/ca.crt -untrusted inter/inter.crt server/leaf.crt # 补上中间 CA
text
error 20 at 0 depth lookup: unable to get local issuer certificate
error server/leaf.crt: verification failed
server/leaf.crt: OK
③ 证书过期(签发一个有效期落在过去的证书):
bash
# 签发一个 2025 年 1 月就已到期的证书
openssl ca -config caconf.cnf -in server/expired.csr -out server/expired.crt \
-startdate 20250101000000Z -enddate 20250201000000Z -notext -batch
openssl x509 -in server/expired.crt -noout -subject -issuer -dates
text
subject=CN = localhost
issuer=C = CN, O = MyLab, CN = MyLab Root CA
notBefore=Jan 1 00:00:00 2025 GMT
notAfter=Feb 1 00:00:00 2025 GMT # 早已过期的有效期
$ curl https://localhost:8446/ -o /dev/null; echo "exit=$?"
curl: (60) SSL certificate problem: certificate has expired
exit=60
讲解 :三类经典 HTTPS 故障都复现成功,curl 统一返回退出码 60(证书校验失败),但错误文本分别指向三种根因------self-signed certificate / unable to get local issuer certificate / certificate has expired。这既解释了浏览器里"证书不安全"告警的来源,也印证了"信任链 + 有效期"是 TLS 认证的核心。
三、原理解析:信任链与一次 HTTPS 握手的时序
信任链(Chain of Trust):
swift
根 CA(自签名,内置在系统信任库)
└── 中间 CA(由根 CA 签发)
└── 叶子证书(由中间 CA 签发,CN/SAN 匹配站点)
客户端校验时,从叶子一路向上回溯,直到找到一个自己信任的锚点(根 CA)。任何一个环节断裂------缺中间证书、根不被信任、过期------都会失败。
一次 HTTPS 请求的时序:
TCP 三次握手 → 建立传输通道
TLS 握手 → 协商版本/密码套件、交换密钥、校验证书(成本大头)
加密数据传输 → 对称加密,开销可忽略
这就是实验 1 数据(0.2ms vs 17.8ms)背后的原理:昂贵的一次性握手,换来此后每个字节的廉价加密。
四、最佳实践与避坑
- 内网/测试站也要签 SAN :现代客户端强制校验
subjectAltName,只写 CN 的老式证书会被拒绝;SAN 应包含所有会用到的域名与 IP。 - 服务器要下发完整证书链 :nginx 的
ssl_certificate应把"叶子 + 中间"拼接在一起,否则用户端会报unable to get local issuer certificate。 - 自签名证书 ≠ 无证书 :自签名证书有加密能力,但没有可信身份,只能用于内网测试,绝不能对公网用户使用。
- 证书有效期要监控 :过期是生产事故高发源,用
openssl x509 -enddate或证书监控平台提前告警。 - 用
curl -k只是"跳过校验",不是"修复":它便于调试,但会掩盖真正的证书问题,排查时务必先看错误文本再决定是否信任。 - 握手成本靠"复用"摊薄:生产优先 TLS 1.3 + 会话复用(session ticket / 0-RTT),把重复握手降到最低。
五、小结
本文在真实 ECS 上完整走通了 HTTPS 的"成本---搭建---排障"三部曲:
- 用
curl -w量化:回环下 TLS 握手约 17.8ms,是明文 HTTP 总耗时的约百倍,成本集中在握手而非加密本身; - 从根 CA 自签、签发带 SAN 的服务器证书、客户端装信任、到 nginx 启用 443 并 ALPN 协商 h2,搭起完整 PKI;
- 复现并定位三类经典故障:自签名、证书链不完整、证书过期,统一退出码 60、根因各异。
下一篇《HTTP/2 与 WebSocket:多路复用、二进帧与全双工》将解答:HTTPS 之上的 HTTP/2 如何解决队头阻塞,WebSocket 又如何实现实时双向通信。
本文所有命令均在 root@113.44.176.3(Ubuntu 24.04.4 LTS)上真实执行,输出为原文截取。