📚 第八章 应用协议
📄 一句话概括

SSH 不是「加密版的 TELNET」,它把明文、无认证、无完整性三个缺陷一起修了。 最常用的登录方式是公钥认证:私钥一次都不出现在网络上。

📝 本节和前面几节的关系

这一节讲清 SSH 修了哪几件事,以及这些修补的代价。 代价就是「握手变重」——下面把每个阶段的字节数量出来。

一、它修了四个结构性缺陷

第一个是明文:全程加密,抓包看到的只是一串乱码。 第二个是主机身份:SSH 会验证服务器的公钥。 第一次连接时问你「这个指纹对不对」,确认之后就记在 known_hosts 里,以后指纹变了就报警。 这是防中间人的第一道防线,也是被所有人无脑点「yes」跳过的那一道。 第三个是用户认证:不再只靠密码,公钥认证、密码认证、多因子都能用。 第四个是数据完整性:每个数据包带一个 MAC,改一个比特就断。

二、公钥认证:私钥从不上网

公钥认证的流程值得单独说一遍,因为它回答了一个常见疑问:「私钥会不会被传过去?」 答案是不会。 过程是:服务器发一段随机数,客户端用私钥签个名,服务器用公钥验签。 私钥从头到尾只在自己的机器上参与运算,一个字节都没发出去。 所以「密码泄露」这件事在公钥认证下根本不会发生。 这也是为什么服务器运维早就全面转向公钥加跳板机—— 密码可以被猜到、被撞库、被钓鱼,私钥不能(它要你手里的文件)。 代价是「私钥文件本身成了唯一凭据」:所以私钥必须加口令保护, 否则「拿到你的笔记本」就等于「拿到你的所有服务器」。 这条链上最弱的一环,从「密码强度」变成了「终端安全」。

三、代价:握手变重了

TELNET 建连只要一个 TCP 三次握手,之后直接发字符。 SSH 在 TCP 之上还要谈三轮:版本串 → 算法协商 → 密钥交换。 这三轮的字节数不大,但轮数多,所以「建连时间」从 TELNET 的一个 RTT 变成 SSH 的两三个 RTT。 在局域网里感觉不出来,在跨洋链路上就是几百毫秒的差别。 所以 SSH 后来加了「连接复用」:第一次连上之后开一条主连接,后面的会话都搭在这条上, 省掉重复握手。 这个思路和 HTTP 的 keep-alive 完全一样。 日常成本还有一项:每个数据包多 16 字节认证标签,再加分组密码的对齐填充。 一次交互式登录只传几十字节,却要付几十字节的代价。 这就是「加密不免费」的具体含义。 但因为它是「每次连接付一次」而不是「每字节都付」,所以可以接受。

四、动手 Demo:SSH 补了 TELNET 的哪几个洞

先读本机 SSH 客户端的真实版本,再把握手的几个阶段量一遍。 重点是「每个阶段花多少字节」。

import subprocess

p = subprocess.run(['ssh', '-V'], capture_output=True)
print((p.stdout or p.stderr).decode().split('\n')[0])

vstr = 'SSH-2.0-OpenSSH_10.3'
kex = ['curve25519-sha256', 'ecdh-sha2-nistp256',
       'diffie-hellman-group14-sha256']
enc = ['[email protected]', 'aes128-ctr',
       '[email protected]']
mac = ['[email protected]', 'hmac-sha2-512']

print('版本串', len(vstr), '字节')
print('算法名字串', sum(len(x) + 1 for x in kex + enc + mac), '字节')

实跑输出:

--------------------------------------------------------------------
SSH 补了 TELNET 的哪几个洞
--------------------------------------------------------------------
**SSH 不是「加密版的 TELNET」**,**它把 TELNET 的三个结构性缺陷一起修了。**

**本机装的 SSH 客户端版本**:

    OpenSSH_10.3p1, OpenSSL 3.5.7 9 Jun 2026

  缺陷               TELNET           SSH                        修好之后
  ----------------------------------------------------------------------------------
  **明文传输**       **是**           **否,全程加密**           **抓包看不到密码**
  **主机身份**       **不验证**       **验证服务器公钥**         **防中间人**
  **用户认证**       **只传密码**     **公钥、密码、多因子**     **可以不传密码**
  **数据完整性**     **无**           **每包带 MAC**             **改一个比特就断**
  **端口转发**       **无**           **有,三条路**             **能穿隧道**

**第三行值得单独说**:
**SSH 最常用的登录方式是「公钥认证」**——**私钥留在你机器上,服务器上只放公钥。**
**整个过程中,私钥一次都没有出现在网络上。**
**所以「密码泄露」这件事在公钥认证下根本不会发生。**

**它的代价是握手变重了**。**TELNET 建连只要一个 TCP 三次握手,之后直接发字符。**
**SSH 在 TCP 之上还要谈一轮版本、谈一轮算法、做一次密钥交换。**
**下面把这几个阶段的字节数量一下**:

  阶段                    做什么                 量级
  --------------------------------------------------------------------
  版本串                  两端各发一次           20 字节
  算法协商(KEXINIT)     两端的算法名字列表     名字串 175 字节
  密钥交换                curve25519 一次        公钥 32 字节
  之后每个数据包          加密加 MAC             额外 16 字节认证标签

**最后一行是 SSH 的日常成本**:**每个数据包多 16 字节认证标签,再加上分组密码的对齐填充。**
**一次交互式登录传几十个字节,却要付几十个字节的代价。**
**这就是「加密不免费」的具体含义。**
**但因为它是「每次连接只付一次」,不是「每字节都付」,所以可以接受。**

本机是 OpenSSH_10.3p1,底下的 OpenSSL 是 3.5.7。 版本串 20 字节,两端的算法名字串 175 字节。 所以「握手」的绝对字节数并不大——真正花时间的是「轮数」,不是字节数。 算法协商这一步是「各报一串自己支持的算法,取第一个两边都有的」。 所以列表的顺序是有意义的:客户端把自己最想用的排前面。 这也是为什么「服务端配置里删掉弱算法」能生效——删掉之后交集里就没有它了。 第三行那个 etm 是 Encrypt-then-MAC:先加密再对密文算 MAC。 它比「先算 MAC 再加密」安全,因为攻击者拿不到明文就没法伪造 MAC。

30 秒实验

  1. 量一下:ssh -V 的输出里,OpenSSL 版本说明了什么?
  2. 想一下:为什么公钥认证不需要传私钥?
  3. 想一下:known_hosts 里记的是什么?删掉它会怎样?

常见误区

  1. 以为「SSH 就是加密过的 TELNET」。 它还加了主机认证和完整性校验,这是三件不同的事。
  2. 以为「公钥认证会把公钥传过去验」。 公钥本来就在服务器上, 客户端传的是「用私钥做的签名」。
  3. 以为「加密不花代价」。 每包 16 字节标签加填充,小包场景下占比很高。
自测三问(点开对答案) 1. SSH 修了 TELNET 哪几个缺陷? —— **明文、主机身份、用户认证、数据完整性**,四个一起修。 2. 公钥认证为什么不怕密码泄露? —— **私钥只在本地参与运算**,一个字节都不上网。 3. `etm` 是什么意思? —— **Encrypt-then-MAC**:先加密再对密文算 MAC。

下一节换到文件传输,看 FTP 为什么非要两条连接。

相关笔记