切换深浅色
第 13 章 为什么网络通信需要加密
13.1 先说明攻击者能做什么
前几章已经说明怎样把请求完整地送到另一台机器。但“完整送达”中的完整,首先是运输服务的含义:TCP向应用提供有序字节流,HTTP解析器识别消息边界。它们本身没有回答另一个问题:收到的内容是否确实出自预期的人,有没有被第三方改写,路上的观察者能看懂多少。
设浏览器C通过代理P访问网站W。现在先不讨论算法,只设想C与P之间的一台中间设备A。若A可以读取经过它的字节,却不改变传输,这叫被动攻击。它可以记下明文目标、身份凭据和正文,也可以只记录连接的时间、方向和长度。即使当前没有立即利用这些信息,保存下来以后分析仍属于这一问题。
若A还可以丢弃、延迟、插入、替换或重新发送通信内容,就进入主动攻击的范围。主动攻击不要求A先解密全部内容。把一份看不懂的旧请求重新发送,或直接切断连接,都已经改变了通信行为。抓取凭据可能是被动阶段,拿着凭据建立新连接则是主动阶段;同一场攻击可以先后使用两种能力。
另一条划分轴是位置。路径上的攻击者能够观察相关通信经过的位置;路径外攻击者没有这种观察能力,但仍可能主动向服务器发包。因而“主动”不能简单等同于“位于链路中间”,“路径外”也不能简单等同于“无害”。例如,服务器面向互联网开放的认证入口,可以被一台普通外部主机连接和试探,不需要先控制受害者的路由。
| 分析维度 | 要问的问题 | 对代理通信的例子 |
|---|---|---|
| 观察 | 能看到哪些链路、哪些时间段 | 只能看到C—P,还是同时看到P—W |
| 操纵 | 能否修改、延迟、重放和中断 | 替换一次握手,或只丢弃部分包 |
| 计算 | 能进行多少猜测与运算 | 在线有限次尝试,还是离线批量猜测 |
| 端点控制 | 能否读取进程、磁盘和密钥 | 仅监听网络,还是已控制P |
| 凭据 | 已掌握哪些共享秘密 | 只有公开地址,还是拥有一个合法账号 |
这些维度共同构成威胁模型,也就是一项安全分析明确承认的攻击者能力。模型不是攻击者的道德评价,也不是某一种软件的名称。两个人都说“防监听”,一个指防本地网络旁观者,另一个指防已经控制代理服务器的人,两人的要求其实不同。
本书采用的通常起点是:通信端点实现正确、必要秘密尚未泄漏,但网络不值得信任。于是协议不能因为包来自某个源IP、TCP已经握手成功、HTTP状态为200,就认定对方具有合法身份。这个起点接近RFC 3552 §3给出的互联网威胁分析方法。该文的历史网络实例不能直接当成今日所有局域网的事实;这里采用的是能力分析方法,而不是“接入同一交换网络就必然看见全部流量”的旧式概括。
端点失陷需要另行分析。P若必须把某层密文解开才能转发这一层明文,那么控制P的攻击者通常也能接触解开的内容。把C—P链路加密得更强,无法自动消除这个解密位置。后文分析多层加密时,将逐层指出谁持有密钥、谁得到明文,而不是把“有加密”当作不受任何人观察的保证。
13.2 保密、完整性、认证和授权分别回答什么
设代理收到一条请求:“连接example.com的443端口”。我们可以对它提出四个不同问题。
第一,第三方是否能读出目标。这是保密性。加密通常以一把密钥为条件,把明文转换为没有该密钥就难以恢复内容的密文。但长度、发送时间和参与地址可能仍然可见;保密的对象必须明确到字段或内容。
第二,请求有没有被未经允许地改变。这是完整性。在有攻击者的环境里,不能只加一个公开的校验和:第三方修改目标后,也可以重新计算校验和。TCP校验和解决传输差错检测的一部分问题,不能作为持有秘密的证明。后文的认证标签会把验证能力与密钥关联,使不知道密钥的人难以制造一份会被接受的新内容。
第三,正在和谁通信。这是对端认证。客户端需要确认连接的是它预期的代理;代理可能需要确认请求来自允许使用服务的凭据持有者。这两个方向可以使用不同机制,也可以只实现一个方向。服务器向浏览器证明了网站身份,并不意味着浏览器已经以某个网站账号登录;反过来,客户端发出密码,也不意味着它在发送前确认了接收者身份。
第四,这个已经通过认证的主体能做什么。这是授权。某个凭据可以使用代理,不代表它必然有权连接服务器的回环地址、云平台管理接口或任意目标端口。目标限制、用户额度与出站规则都可能参与授权决定。认证将通信关联到某个被认可的身份或凭据,授权才把它映射到允许的行为。二者不能因为配置文件只写了一行用户信息就合并为一个概念。RFC 3552 §4.4。
这里的“身份”往往比日常姓名狭窄。使用某个共享秘密完成认证,直接证明的是对方掌握或能使用这个秘密,并不自动证明操作者的现实姓名、设备唯一性或没有转借账号。如果十个人共享一个凭据,服务器通常不能仅凭这项认证区分十个人。若凭据被偷,攻击者可能在协议层得到与合法持有者相同的待遇。
| 目标 | 典型失败 | 单独实现它仍不能解决的事 |
|---|---|---|
| 保密 | 路人读出正文或凭据 | 对方是否真实、密文是否被改动 |
| 完整性 | 目标或正文被替换后仍接受 | 原封不动的旧消息是否应再次执行 |
| 对端认证 | 接入冒充服务器的端点 | 账号允许访问哪些资源 |
| 授权 | 合法账号访问被禁止的目标 | 网络是否持续可用 |
| 可用性 | 资源耗尽、链路被阻断 | 已交付内容是否保密 |
重放进一步显示了这些目标的区别。一份请求内容没有改变,认证标签也可能仍然正确,但“把昨天有效的动作今天再执行一遍”可能违反业务要求。因此协议还要决定消息是否足够新鲜,是否属于当前会话,是否已经处理过。时间戳、计数、一次性随机挑战以及服务器保存的状态都可能参与这件事;只列出字段名称还不够,必须继续说明接受窗口、状态保存和失败处理。
可用性也不能由加密直接承诺。即使攻击者不能伪造一字节有效内容,仍可能让所有有效包无法到达。密码学可以帮助尽早拒绝伪造请求、限制某些放大行为,却不能迫使一条被切断的线路继续传输。后文谈“安全”时,都会说明具体是哪几项性质,而不会给所有目标打一个总分。
13.3 中间人为什么不需要破解加密算法
先考虑一种直觉方案:C连接P,双方通过网络商量出一个只有彼此知道的秘密,然后用它加密。这解决了“事先没有共同密钥怎么办”的问题,却还留下“和我商量的人是谁”。密钥交换的数学细节在第15章展开;这里暂时只需要理解它是一段建立共享秘密的交互。
如果交换过程没有把对端身份与本次交互可靠地绑定,路径上的A可能截住C发给P的信息,自己和C完成一次交换,同时冒充C和P完成另一次交换。C掌握的秘密只与A相同,P掌握的秘密也只与A相同,两侧秘密并不需要相同。A可以解开左侧数据,理解或修改它,再用右侧秘密重新加密。
正在绘制图表…
查看图表原文
sequenceDiagram
participant C as 客户端C
participant A as 中间人A
participant P as 预期代理P
C->>A: 发起未认证的密钥交换,原本要发给P
A->>P: 以自己的交换信息发起另一会话
P-->>A: 交换响应
A-->>C: 以自己的交换信息回应C
Note over C,A: C与A得到秘密K1
Note over A,P: A与P得到另一秘密K2
C->>A: 使用K1保护的请求
A->>P: 使用K2保护的请求,可被改写
这张图不是某个真实协议报文的抓包,而是未认证交换的逻辑反例。算法可能在每一侧都正确工作,密钥也足够长,中间人仍然成功。它绕开的正是身份绑定,而非硬算出一个未知密钥。只观察“网上没有出现明文”无法排除这种情况。RFC 3552 §3.3.5讨论了这种两端分别被冒充的结构。
阻止它需要某种攻击者无法随意替换的可信起点。例如,客户端事先可靠获得P的公钥,随后检查本次握手确实由对应秘密的持有者参与;或者客户端依据可信机构及预期服务名检查服务器证书,再验证服务器在本次握手中的证明;也可能双方原先已可靠共享一个秘密,用它认证交换过程。公钥、证书、签名的具体计算会在第15、16章定义,这里先确定它们承担的角色。
公钥不是因为“公开”而不可信,也不是因为“看起来随机”而可信。问题是客户端凭什么相信这把公钥属于预期的P。如果公钥和需要保护的数据一起经过同一条可被随意替换的未认证通道,攻击者可能连公钥一起换掉。可信分发、系统信任根、预先固定的密钥,都是在回答信任从哪里开始。
认证还必须绑定当前交互,而不只是展示一份可以复制的证件。假设A复制了P公开展示的证书,它不应仅凭证书副本就能冒充P;协议还要让A证明自己拥有相关秘密,并将该证明与本次协商内容关联。否则,把另一场会话中的证明剪贴过来,或替换协商选项后继续转发,都可能破坏预期保证。这也是后文反复分析握手记录、签名覆盖范围和完成验证消息的原因。
客户端关闭证书验证的影响由此可以准确描述:它可能仍在使用加密算法,但失去了原本用来确认预期服务的某些检查。具体还能依靠什么,要看是否另有正确的认证机制,不能简单推断“完全没有加密”,也不能据此宣称“既然能连上就同样安全”。
代理本身则是被明确安排在数据路径中的参与者。C主动把某层数据交给P解开,并不自动构成中间人攻击;攻击的关键在于身份和权限是否违背通信者的预期。对C—P的认证保护也不会自动延伸成对P—W的认证。每一层会话都需要找出它实际认证的对象。
13.4 密文之外还有元数据
把正文替换成密文以后,通信仍需要找到下一跳。C向P发送IP包,C—P路径上的设备仍需处理该包的外层地址;包的大小、到达时间和方向也不会因为负载加密而自然消失。这些关于通信本身的信息称为元数据。某个字段是不是元数据,取决于分析的层次:网站域名对代理请求来说是内容,对正在判断用户访问对象的分析者来说又可能是关键信息。
考虑一个明确的条件场景:C与P有经过认证的加密通道,C通过它请求连接W;P转发浏览器与W之间独立建立的HTTPS会话;没有其他侧漏,也暂不假定多个观察者共享记录。由封装关系可以得到以下可见性,而不必破解任何算法。
| 参与位置 | 可以直接观察的部分 | 不能仅凭所在位置直接得到的部分 |
|---|---|---|
| C—P的网络观察者 | C和P的外层地址、流量时序与尺寸 | 已正确保护的代理目标字段、网站HTTPS正文 |
| 代理P | 与自己连接的来源、代理请求目标、转发节奏 | 浏览器—W的HTTPS应用明文,前提是P不是该TLS端点且验证正确 |
| P—W的网络观察者 | P和W的外层地址、该段时序与尺寸 | 已正确保护的HTTPS正文 |
| 网站W | P的连接来源、解密后的网站请求 | 不能仅靠该TCP来源地址直接读出C原来的地址 |
表中最后一格不是匿名保证。用户若登录自己的账号、带上可关联的会话标识,W仍可能知道用户是谁;应用也可能显式发送源地址信息。这里严格区分“这个网络地址没有直接出现”与“任何信息都不能关联到用户”。RFC 6973 §5.2把关联、识别和信息的后续使用作为不同的隐私问题讨论。
DNS也有自己的观察位置。若C在通道外向一个解析器查询W,它可能泄露目标,即使随后代理目标字段被加密。若P解析W,解析位置就移动到P及其使用的解析器。采用加密DNS可以改变网络沿途能直接读出的内容,但接收并处理查询的解析服务仍然参与信任边界。不能把“DNS加密”写成“没有任何一方知道查询”。
TLS同样不能被概括为“所有握手字段都不可见”。普通TLS1.3握手的初始ClientHello包含一些未被该TLS会话自身加密的协商信息;是否另有机制保护这些内容,需要具体分析。QUIC Initial已有包保护,第12章却说明其初始材料可由公开信息导出,因此那一阶段的保护目的不能等同于抵抗被动观察者的内容保密。不同阶段、不同字段必须分别判断。
流量分析则会利用没有直接表示目标的线索。例如,短上行后紧接长下行可能符合许多网页加载,也可能符合其他下载行为。这样的形状是可观察事实,“它一定在访问某个网站”却是推断。判断方法如果没有数据集、观察条件、误报率和适用版本,就不能写成确定识别能力。反过来,也不能因为一段密文看起来随机,就断言无法分类。
填充可以让若干不同长度的内容呈现相同外部尺寸,但通常增加流量;为了改变时间特征而缓存、定时发送或发送额外数据,又会影响延迟和带宽。单次填充不会自动隐藏连接总时长、包数和全部方向关系。代理链也可能改变各位置获得的信息,却不天然保证能够抵抗同时观察入口与出口的关联分析。本书后文解释Padding、Vision与XHTTP时,将把具体改变的可见特征与没有改变的特征一起列出。
13.5 信任边界取决于谁能解开哪一层
安全通道保护的是两个端点之间的某段交互。它在发送端加密,在接收端解密;一旦接收端把明文交给上层或另一条连接,原通道的保护范围就到此为止。数据随后能否继续受到保护,取决于下一层、下一段自己的机制。
例如,C把HTTPS网站流量装进一条到P的加密代理通道,P只负责转发网站TLS字节。两个安全范围可以表示为:
text
网站TLS: C ==================================== W
代理通道: C ================= P
网络连接: C ----------------- P --------------- W
C发出: 外层保护[代理控制信息 | 网站TLS记录]
P得到: 代理控制信息 | 网站TLS记录
W得到: 网站HTTP内容这里的等号只表示保护端点,不代表物理线路直连。P解除外层后仍面对网站TLS记录,因此拥有代理通道密钥不等于拥有网站TLS密钥。网站TLS的认证若正确,即使P改变其中的受保护内容,W或C也应检测失败;P仍可以阻断、选择连接目标,或记录它直接知道的代理目的地。
若C访问的是明文HTTP,图中的内层保护就不存在。P解开外层以后能读出HTTP内容,P—W沿途也可能读到它。不能把C—P加密画成一个自动延伸到W的盾牌。对于只要求保护本地接入链路的场景,它仍然有意义,但意义必须说准。
再加入一个能终止外层TLS的CDN节点D。若结构是“C与D建立TLS,D把解密的HTTP请求转给P”,TLS保护的接收端是D。即使D到P又建立另一条TLS连接,D仍处理过两段之间的内容。这叫分段保护,不能在图上跳过D,画成只有C和P能解密的一条通道。
不过,D解开外层HTTP以后看到的负载也可能仍是内层密文。关键是这一层有没有独立于C—D TLS的保护,以及秘密由谁持有:若负载另由C和P共享的密钥保护,D可以处理HTTP路由却未必能读出内部代理内容;若没有,D就可能直接看到代理请求。XHTTP作为承载方式能否经过CDN,与经过后哪些字段保密,是两个需要分别证明的问题。
这个思路也适用于代理链。增加一台转发机器,可能让入口只看到下一跳、出口只看到前一跳;但如果它们由同一个操作者控制、共用日志,或者链路上仍存在未保护的控制字段,实际信任范围不会仅因图上多了一个方框就缩小。反而新增端点可能增加密钥分发、权限控制和故障处理的工作。
信任边界还包括机器内部。网络协议可以保护传输中的字节,但不能自动保护终端截图、应用日志、崩溃转储和磁盘上的凭据。通道接收者把内容写入日志后,该日志已经成为另一份需要管理的数据。对象级保护则直接施加于某个数据对象,可能跨过多个运输通道继续存在;其端点和权限不一定与传输端点重合。RFC 3552 §4.7指出了对象保护与通道保护的这种区别。
因此,分析一个具体配置应沿着“输入是什么、这一层验证什么、在哪里解密、输出交给谁”的顺序逐段走。只看配置里出现了几次TLS、多少个随机字符串,无法推导端到端的安全边界。
13.6 同样是字节变化,协议需要识别不同失败
下面构造一个只用于分析安全目标的明文请求。它不是VLESS,也不是新的部署协议:
text
01 00 2a 03 01 bb 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d| 偏移 | 字节数 | 本例约定 |
|---|---|---|
| 0 | 1 | 操作01,表示请求连接 |
| 1—2 | 2 | 请求编号002a,即42,大端 |
| 3 | 1 | 地址类型03,表示域名 |
| 4—5 | 2 | 目标端口01bb,即443,大端 |
| 6 | 1 | 域名长度0b,即11 |
| 7—17 | 11 | ASCII字符串example.com |
总长度是18字节。构造中的字段长度与取值均由这里明示,没有暗示它是任何真实代理协议的规范。它能展示三种不会由“正确解析”自动解决的问题。
第一,改写。路径上的攻击者若把偏移4—5从01 bb改成00 50,解析器仍能得到一条形式正确的连接请求,只是端口变成80。给完整18字节计算一个公开散列并附在后面也不足以认证发送者,因为攻击者可以对修改后的请求重算散列。需要的是与秘密关联且覆盖适当字段的验证机制。
第二,重放。攻击者原封不动地再次发送18字节,内容没有改变。若接收方只检查格式并立刻建立连接,就可能重复执行。请求编号42只有在协议说明了它所属的用户、会话、有效期以及接收方如何识别重复时,才可能参与防重放。一个从1开始、每次重连清零的编号,不能脱离会话上下文充当跨连接唯一凭据。加密后的旧请求是否仍被接受,也需要同样分析。
第三,截断与状态误判。攻击者只让前10字节到达,随后中断连接。解析器已看见长度11,却只得到域名的前三字节65 78 61。此时是未完成请求,不能把exa当成完整域名继续执行。即使每个已经收到的安全记录都验证通过,若完整应用对象还没结束,也不能据此认定整个对象完整。安全记录边界、运输结束与应用完成状态是三件不同的事。
同理,若协议保护了正文,却没有把“这份正文属于哪个目标或请求”可靠地绑定,攻击者可能尝试把一个上下文里的有效内容搬到另一个上下文。后文会用附加认证数据、方向独立的密钥、会话派生和隐含序号说明怎样限制这类混用。这里先确定要保护的不只是某一段字节值,还包括它的解释位置。
错误处理本身也属于状态机。字段超出长度上限时先分配巨量缓存、认证失败后仍将暂存明文交给业务、收到不完整消息却按正常完成结算,都可能破坏安全设计。通道必须规定什么时候允许使用数据,什么时候停止处理;这比笼统写一句“失败则忽略”更精确。
最后,安全失败不一定表现为一份明显不同的报文。错误响应的尺寸、时延、连接是否保留,都可能向主动试探者透露不同处理分支。这些表现是否构成实际可利用的信息,需要结合机制和观察条件判断。本书不会把所有差异都宣称为漏洞,但分析REALITY等认证与回落行为时,会把外部能观察的响应列为实际机制的一部分。
13.7 把安全声明读成有条件的命题
“该协议安全”缺少可检查的对象。更完整的说法应当是:在某些端点、秘密分发和实现条件下,面对具备某类能力的攻击者,这套机制为某一段数据提供哪些性质。条件越明确,读者越能判断结论是否适合自己的配置。
例如,下面是一条可以分析的声明:“C正确验证P的身份,以未泄漏的会话秘密使用规定的认证加密,并遵守每个密钥的用量限制,则C—P沿途的攻击者难以读取受保护正文或伪造会被接受的新记录。”它没有承诺P看不到正文,没有承诺隐藏P的IP,也没有承诺攻击者不能切断网络。第14—16章会把其中每个条件具体化。
再看另一句话:“客户端用一个UUID登录代理,所以链路已加密。”前半句只描述某种凭据,无法推出后半句。必须追问该值是否明文传输、如何验证、是否受外层保护、服务器身份如何认证、后续数据有没有独立保护。凭据的字符外观不是加密算法,更不提供整个会话的自动安全证明。
读规范时还要分清三层证据。第一层是设计规定:报文应有哪些字段,发送方和接收方必须怎样处理。第二层是实现事实:某个固定版本实际采用哪种算法、校验了什么、在什么条件下走另一条路径。第三层是部署事实:当前机器真的运行哪个版本,密钥和证书如何设置,路径上又有哪些终止节点。一个源码分支存在,不代表某个实际配置一定执行它;一个设计要求写在文档里,也不代表所有历史版本已经实现。
安全结论同样有证据强弱。规范明确要求、源码可以追踪、公开分析支持、仅由现象推断,是不同状态。若没有追踪到某个加密记录的nonce形成方式,本书不能因为配置字符串含有某算法名就补出字段;如果没有运行部署,也不能把源码兼容条件写成“已在VPS验证”。来源不足时应保留不确定性,继续寻找依据。
性能和隐蔽性也应使用同一标准。“少一轮握手”要说明是在新连接、恢复连接还是已经复用的连接上,从哪个事件算到哪个事件;“无法识别”要说明观察位置、识别任务和误判条件。算法名称、包看起来随机、端口号与常见业务一致,都不足以直接推出完整结论。
这一方法不是给每段正文加上笼统免责声明,而是找到结论实际依赖的机制。接下来先解决最常见的数据处理问题:已经有了共同秘密以后,怎样保护一串任意长度的字节,怎样发现改写,以及为什么同一把密钥下重复使用某些输入会造成严重后果。
本章安全术语以RFC 4949作为查阅词汇的资料,威胁模型方法参考RFC 3552,隐私分析参考RFC 6973。关于代理端点与18字节请求的例子为本书自行构造的推演,不是这些RFC给出的代理协议。