TLS 基础:握手、证书链与 SNI

HTTPS 握手、证书链与验证方法

HTTPS 不是「装个证书就完事」,它背后是一条完整的信任链:浏览器凭什么相信公钥属于这个域名、证书过期或链不全会发生什么、多个域名共用一台服务器怎么区分。理解这几件事,才能在出现「部分用户报证书错误、你的浏览器却正常」时快速定位。本章讲握手流程、证书链、SNI 与会话复用,并给一套 openssl s_client 验证手法。

要解决的问题

  • HTTPS 握手到底做了什么,为什么需要证书?
  • 「证书链不完整」是什么,为什么只有安卓/Java 客户端报错?
  • 一台服务器上跑多个 https 域名,Nginx 怎么知道该用哪张证书?
  • 为什么建议 TLSv1.2 TLSv1.3,而不是只留 1.3?
  • 会话复用怎么配,能省下多少握手开销?
  • 怎么不靠浏览器就把协议版本、证书链、有效期逐项验一遍?

TLS 握手流程

① ClientHello
   客户端发送:支持的 TLS 版本、密码套件列表、随机数、SNI=example.com
        ↓
② ServerHello + Certificate
   服务器选定版本与套件、发出随机数,并下发「站点证书 + 中间证书」组成的证书链
        ↓
③ 密钥协商与身份证明
   TLS1.3:双方交换密钥共享参数,服务器用证书私钥对握手内容签名
   TLS1.2:ECDHE 交换参数 + ServerKeyExchange 签名(RSA 密钥传输已淘汰)
        ↓
④ 客户端验证
   校验证书链能否追到受信任根、域名是否匹配(SAN)、是否在有效期内、是否被吊销
        ↓
⑤ Finished
   双方用协商出的主密钥派生会话密钥,握手结束,之后的 HTTP 数据全部加密

一次完整握手在 TLS1.2 下需要 2 个往返(2-RTT),TLS1.3 只要 1-RTT,会话恢复时甚至可以 0-RTT。这就是「会话复用」的价值:不重复做非对称运算,直接把之前的会话密钥拿出来用。

证书链与中间证书

浏览器和操作系统只内置了根证书;实际签发你证书的是中间 CA。服务器必须把「站点证书 → 中间证书」按顺序拼在一个文件里发给客户端,客户端才能把链补齐。少发中间证书时,桌面浏览器常常能靠缓存或 AIA 自动补链,而安卓老版本、Java 客户端、部分命令行工具会直接报错——这就是「我这儿好好的,用户说证书错误」的最常见原因。

文件内容配置位置
站点证书example.com.crt(叶子证书)拼接文件的第一个
中间证书ca-chain.crt(可能多张)紧跟在站点证书后面
完整链fullchain.pem = 站点 + 中间ssl_certificate
私钥example.com.keyssl_certificate_key,权限 600
server {
    listen 443 ssl;
    http2 on;                                   # nginx 1.25.1 起的新写法
    server_name example.com www.example.com;

    ssl_certificate     /etc/nginx/certs/example.com.fullchain.pem;   # 必须含中间证书
    ssl_certificate_key /etc/nginx/certs/example.com.key;

    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_session_cache   shared:SSL:10m;         # 10m 约存 4 万个会话
    ssl_session_timeout 1d;
    ssl_session_tickets off;                    # 票据机制有前向安全争议,可关

    location / { proxy_pass http://app_backend; }
}

SNI 与多域名

SNI 是客户端在握手第一步就明文告诉服务器「我要访问哪个域名」的扩展。没有它,服务器无法在一台机器上给不同域名返回不同证书;有了它,Nginx 才能按 server_name 选出对应的 server 块和证书。

方案配置适用代价
一张多域名证书(SAN)server_name a.com b.com;域名少、同一主体增删域名要重新签发
通配符证书server_name *.example.com;同域下无限子域只覆盖一级子域,a.b.example.com 不覆盖
SNI 分流每个域名独立 server 块 + 各自证书域名多、主体不同不支持 SNI 的老客户端会拿到默认证书

server_name 匹配不到的请求会落到该端口的 default_server;如果没显式指定,就是配置里第一个 listen 443 的 server。多域名共用 443 时,给一个明确的默认站点能避免「用 IP 访问时看到奇怪的证书」。

TLS1.2 与 TLS1.3 的差异

维度TLS1.2TLS1.3
握手往返2-RTT(恢复 1-RTT)1-RTT(恢复 0-RTT)
密钥交换RSA / ECDHE 均可只保留 (EC)DHE,前向安全
套件粒度密钥交换 + 认证 + 加密 + MAC 全组合只需选 AEAD 算法 + 哈希
已移除RSA 密钥传输、CBC、SHA-1、压缩、重协商
兼容性几乎所有客户端极老的系统(如 Android 4.x 自带浏览器)不支持

结论就是常用配置 ssl_protocols TLSv1.2 TLSv1.3;不要只留 1.3(会损失一小部分老客户端),也不要为了兼容放开 TLSv1.0/1.1(已被主流浏览器标记为不安全,还在 PCI-DSS 等合规要求的禁用名单里)。

验证方法

# 1) 完整握手详情:协议版本、套件、协商结果
openssl s_client -connect example.com:443 -servername example.com </dev/null

# 2) 只想快速看握手的响应行与证书概要
curl -vI https://example.com/ 2>&1 | grep -i -E 'SSL connection|subject:|issuer:'

# 2) 只看协议与套件
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null 2>/dev/null \
  | grep -E 'Protocol|Cipher'

# 3) 证书主体、签发者、有效期、SAN
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

# 4) 链条完整性 + 域名与链校验(任一项失败会以非 0 退出)
openssl s_client -connect example.com:443 -servername example.com \
  -verify_return_error -verify_hostname example.com </dev/null

# 5) 剩余有效期(-checkend 604800 表示检查 7 天内是否到期)
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 604800 && echo '7 天内不会过期'

第 4 条是最容易被忽略、也最有价值的一条:它同时验证「链能不能追到根」和「域名是否匹配」,比只看浏览器有没有红字严谨得多。

常见坑

现象做法
ssl_certificate 只配站点证书部分安卓/Java 客户端报证书错误fullchain.pem(站点 + 中间证书)
用 IP 或错误域名访问证书不匹配告警确保域名与证书 SAN 一致,配 default_server 兜底
私钥权限过宽私钥可被任意用户读取chmod 600、属主 root
只留 TLSv1.3老设备无法访问保留 TLSv1.2 TLSv1.3
关掉会话复用高并发下 CPU 花在握手而不是业务ssl_session_cache shared:SSL:10m
用自签证书对外所有用户看到安全警告自签只用于内网和测试
以为「没登录就不用 HTTPS」表单被中间人篡改、被抓包只要有登录、表单、Cookie,就必须上 HTTPS

关于最后一条要说得更直白:只要涉及登录、支付、表单、Cookie,就必须上 HTTPS。现在浏览器对 http 页面会直接标注「不安全」,输入框还会弹警告,用户信任成本比证书成本高得多。mTLS(双向证书验证,ssl_client_certificate + ssl_verify_client on)是另一回事,用于服务间调用或内部后台,它是「验证客户端」而不是「加密传输」,日常对外的站点不需要。

小结:一次握手就是「协商版本与套件 → 下发证书链 → 用私钥证明身份 → 客户端验链 → 派生会话密钥」,TLS1.3 把它压到 1-RTT 并只保留前向安全算法;证书文件必须是「站点证书 + 中间证书」的完整链,否则部分客户端会失败而桌面浏览器看不出来;多域名靠 SAN、通配符和 SNI 三种手段解决;协议固定为 TLSv1.2 TLSv1.3 并开启会话复用;验证永远用 openssl s_client 逐项核对,而不是用自己那台已经缓存过证书的浏览器。

笔记加载中…