HTTP 没有「你是谁」这一问。只要能把流量引到自己这儿,任何人都能冒充任何站点。 证书要同时过两关:信任链能追到受信根、证书名覆盖请求的主机名。
上一节是「被听见」,这一节是「被冒充」。两者经常一起发生——中间人既要能读,也要能替你答话。
一、伪装有多容易
Demo 里用 openssl 造了一张自签证书,名字写 wrong.example,起一个真的 TLS 服务端,
然后让客户端用四种方式去连。
第一种:关掉校验。结果连上了,收到 98 字节——服务端是什么来路完全不重要,只要握手能成。
这就是伪装的本质:TLS 的加密部分本身是可靠的,但「你在跟谁加密」这件事,得靠校验来完成。
不校验,等于把加密用在了一个陌生对象上。
二、证书要过的两关
第二关先过第一关。
Demo 里第二种:把这证书手动加进受信根,再请求 right.example,
结果被拒——Hostname mismatch, certificate is not valid for 'right.example'。
信任链过了,名字没过。
第三种:同样当受信根,但请求 wrong.example,连上了。两关都过,于是放行。
这说明「受信」是个相对概念——它只表示「这台机器的信任库里认它」。
第四种:不当受信根、名字写对,又被拒——self-signed certificate。名字过了,信任链没过。
四行连起来就是完整的验证逻辑:链要能追到受信根,名要覆盖主机名,两条同时成立才放行。
缺一条,中间人就能得手。
三、为什么 DoS 不算在这个问题里
有一点要分清:中途切断连接、拖慢响应这类破坏,明文和加密都挡不住——攻击者直接掐线就行。 证书解决的问题只有「冒充」:它让你能确认对面是谁。至于对面能不能回话、回得多快,是另一层的事。 所以看到「HTTPS 也防不住 DDoS」这类说法时不必惊讶,它本来就不负责这个。 安全的目标从来不是「什么都能防」,而是「每一条威胁都有对应的手段」。
四、动手 Demo:四种连法,四种结果
用 openssl 现场生成一张 CN 和 SAN 都是 wrong.example 的自签证书,起一个真 TLS 服务端;
客户端分别用「不校验」「当受信根但名字不对」「两关都对」「名字对但不受信」四种配置去连。
openssl req -x509 -newkey rsa:2048 -keyout wrong.key -out wrong.pem \
-days 2 -nodes -subj '/CN=wrong.example' \
-addext 'subjectAltName=DNS:wrong.example'
def attempt(label, hostname, verify, trust=False):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
if verify:
ctx.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
if trust:
ctx.load_verify_locations(cert) # 手动信任这张自签证书
else:
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE # 什么都不查
with ctx.wrap_socket(sock, server_hostname=hostname) as t:
...
实跑输出:
--------------------------------------------------------------------
证书对不上:能连上,不等于对面是对的
--------------------------------------------------------------------
先造一张自签证书,它的名字是 wrong.example:
subject=CN=wrong.example
X509v3 Subject Alternative Name:
DNS:wrong.example
同一个服务端,客户端用四种方式去连:
① 不校验 连上了,收到 98 字节
② 当受信根,名字对不上 拒绝:Hostname mismatch, certificate is not valid for 'right.example'.
③ 当受信根,名字也对 连上了,收到 98 字节
④ 不当受信根,名字对 拒绝:self-signed certificate
第一行最能说明问题:**关掉校验就一定能连上**。
中间人只要把流量引到自己这儿,用一张随便什么证书,就能冒充任何站点。
客户端这边毫无察觉——它以为自己在跟 right.example 说话。
第二行只错了一件事:证书里的名字是 wrong.example,请求的是 right.example。
信任链这一关过了(因为我们手动画了它当受信根),名字这一关没过。
第三行两关都过,于是连上了——注意这时候服务端根本不是什么正规站点,
只是一张我们自己用 openssl 造的自签证书。它「合法」是因为客户端被手动配置成信任它。
第四行又回到拒绝:名字对了,但客户端没把它当受信根。
四条连起来看,就是一整套身份验证:
「你是不是你」——证书链要能追到一个受信的根;
「你是哪个你」——证书里的名字要覆盖你请求的主机名。
两条同时成立才放行。缺任何一条,中间人都能冒充。
① 不校验:连上,收到 98 字节。关掉检查就一定通,这是伪装的入口。
② 当受信根、请求 right.example:拒绝,
Hostname mismatch, certificate is not valid for 'right.example'。
③ 当受信根、请求 wrong.example:连上。两关都过。
④ 不当受信根、请求 wrong.example:拒绝,self-signed certificate。
四条结果把「验证」拆成了两件独立的事。这也是排查证书问题时最该先问的:是链的问题,还是名字的问题?
两条的报错完全不同。
30 秒实验
- 给证书加一个 SAN 为
*.example.com,试a.example.com与a.b.example.com能不能过。 - 把
-days 2改成-days 0,看报错是不是变成「证书过期」。 - 想一想:为什么浏览器不允许用户「跳过」证书错误在生产环境随意使用?
常见误区
- 「关掉校验只是测试方便」——它同时关掉了唯一能防伪装的那道门。
- 「HTTPS 就自动验证身份」——握手成功不等于对面是它自称的那个。
- 「证书过期和名字不对是一回事」——报错不同,处理方式也不同。
自测三问(点开对答案)
1. 证书验证要过哪两关? —— 信任链能追到受信根;证书名覆盖请求的主机名。 2. 关掉 `check_hostname` 会发生什么? —— 任何证书都能通过,中间人可以冒充任意站点。 3. `Hostname mismatch` 和 `self-signed certificate` 有什么区别? —— 前者是名字不对(链可能是好的),后者是链追不到受信根。被读、被冒充都说完了。第三种更隐蔽:报文被悄悄改了一两个字,你根本看不出来。
- ☐ (本节完,下一节 7.1.3 无法证明报文完整性,可能已遭篡改)