📘 第七章 确保Web安全的HTTPS
📄 一句话概括

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 秒实验

  1. 给证书加一个 SAN 为 *.example.com,试 a.example.com 与 a.b.example.com 能不能过。
  2. 把 -days 2 改成 -days 0,看报错是不是变成「证书过期」。
  3. 想一想:为什么浏览器不允许用户「跳过」证书错误在生产环境随意使用?

常见误区

  1. 「关掉校验只是测试方便」——它同时关掉了唯一能防伪装的那道门。
  2. 「HTTPS 就自动验证身份」——握手成功不等于对面是它自称的那个。
  3. 「证书过期和名字不对是一回事」——报错不同,处理方式也不同。
自测三问(点开对答案) 1. 证书验证要过哪两关? —— 信任链能追到受信根;证书名覆盖请求的主机名。 2. 关掉 `check_hostname` 会发生什么? —— 任何证书都能通过,中间人可以冒充任意站点。 3. `Hostname mismatch` 和 `self-signed certificate` 有什么区别? —— 前者是名字不对(链可能是好的),后者是链追不到受信根。

被读、被冒充都说完了。第三种更隐蔽:报文被悄悄改了一两个字,你根本看不出来。

相关笔记