HTTPS 实战:TLS 握手成本、自制 CA 与证书、三大常见故障

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_sslhttp_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_appconnectTLS 握手完成 耗时(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)上真实执行,输出为原文截取。

相关推荐
江华森1 小时前
HTTP 再邂逅:报文结构、请求方法、状态码与 Cookie/Session
前端
szephyr1 小时前
前端错误监控实战:用 Sentry 把线上报错从“用户反馈“变成“主动发现“
前端·sentry·稳定性·前端监控·错误监控
拖孩1 小时前
给小程序加「一键发公众号贴图」,我把 wx.shareToOfficialAccount 的三个坑趟了一遍
前端·后端·微信小程序
ONLYOFFICE1 小时前
团队协作软件指南:主要类型、核心功能、选型建议等
前端·onlyoffice·office
志尊宝2 小时前
Vue3 零基础每日笔记(048):嵌套路由与命名视图——后台管理系统布局雏形
前端·javascript·vue.js·笔记·html5
Mr.Meng_952 小时前
彻底搞懂Promise.allSettled:用法、场景、源码手写、面试题详解
前端
心易行者2 小时前
Python在线运行+SQLite数据库实战:0成本搭个人数据后台,5个场景直接套用
前端·网络·人工智能·python
@tangguo1233 小时前
npm 和 yarn 配置说明
前端·javascript·npm·node.js·yarn