0 前言
这篇文章是介绍 TLS 自动机设计和实现理念的,是我写在博客上的文章,转过来发一下,博客上的 CVE 的分析就比这里的多几句话,看这个就对一般协议实现者够用了。
这里的 TLS 自动机设计和总结不会设计加到线程安全,因为那个纯属于实现级别。
1 TLS 自动机设计理念
1.1 理念
设计时,我们的理念就三种:
这三条理念说起来都很短的,但是实际上的理念是高度集成的,我们一条一条分开说:
1.1.1 简单
简单的概念包含三种:
- 自动机的设计要足够简单,没有用或者说能节省掉的流程,在保证安全的同时,一定要省略掉。这里千万不要忽视掉“保证安全”四个字,openssh 曾经有个 CVE(CVE-2018-15473)就是因为简单了,但是没考虑安全导致的。复杂的步骤除了带来计算的复杂度,也带来了安全的风险;而简单的状态实现一方面减少了实现的难度和麻烦,另一方面带来了可读性和吞吐性能的改善。这里需要注意的是,自动机的设计并不等同于 RFC 里面的状态机,这点是需要自动机设计者搞明白的。
- 自动机的状态转换,语义和可读性要足够简单,好理解。:任何涉及到大 /小的状态转换,标记变化必须在明处写明白,能用一种变量表明的状态不要使用两种来做。就以 TLS1.3 的 EARLYDATA 的状态而言,这个状态 EARLY_DATA_NONE 表示没有 0-RTT 请求,不要用 EARLY_DATA_NONE 来表示拒绝了 0-RTT 请求。这一条“可读,语义简单”并不一定完全执行,有的时候处于内存申请的考虑是需要
- 最后一个简单的意思就是要减少复杂的操作,这里复杂的操作主要指就是系统调用:我司系统里面就是减少了 malloc 的申请(毕竟又是系统调用,又是缺页中断)能用 uma_zone/slab 管理器的地方,绝对不要用 malloc 来做,虽然对于 TLS 握手流程而言,非对称密钥的计算才是时间消耗的大头,但是内存的申请利用这块也要减少!
1.1.2 分层
相比较简单而言,分层的概念简单多了,就两种:
- 协议本身的分层是“分层理念”的基础,这点最直接就提现到了 TLS 的协议分层:外层为 RECORD LAYER,内层为 HANDSHAKE LAYER,内外层尽量减少干涉。但这种分层实际上并没有提现到 OPENSSL 的自动机里,或者说体现的不够明显,这也是我对 OPENSSL 自动机的主要不满之处。
- 实现层面的分层,举个操作系统的简单例子,分配内存时候只是分配虚拟内存,物理内存未必可以分配出来,真正需要的时候再进行缺页中断。这种方式主要是为了提高吞吐量。
1.1.3 安全
TLS 状态机设计里面实际上安全是最难实现的,安全的原则说简单也简单,说难也难,抽象起来就两种:
- 不要对下层协议释放过多的信息。这个设计到的攻击主要是针对信息泄露,任何时刻都需要对输入输出做边界检查(心脏滴血漏洞),对不同的行为但是相同的后果不要做出不同的回应( CVE(CVE-2018-15473))。
- 上层协议的资源不要和下层保持同样的生命周期。这种设计理念主要是针对拒绝服务攻击,常见的拒绝服务攻击记大多都是由于资源错误释放时间。
2 TLS 自动机实现时候的困难和问题
下面针对性的给出我们做自动机的时候遇到的困难和问题,并简单描述我们给出的解决方案。这里需要注意的是里面一部分问题是理念导致的,一部分是实现导致的,在写代码的时候要区分开抽象和具体的区别。
2.1 简单理念
- 问题 1,冗余的状态:TLS1.3 里在客户端复用的流程下的 psk ==> binder_key ==> hmac_key 状态切换冗余而且麻烦,会降低复用的性能。出于简单理念,我将状态机的从 psk ==> binder_key ==> hmac_key 的状态切分压缩到一步,并且将 hmac_key 计算流程放在了服务端,使得客户端可以直接利用 hmac_key(加密存储在 ticket identity 里)
- 问题 2,TLS1.2 自动机里隐晦的状态转换:我在写 TLS1.3 自动机之前,参与了我司对 TLS1.2 的状态机优化,除了代码层面的优化,也修改了 TLS1.2 里面一部分隐晦状态的改变问题,TLS1.2 的验签藏在一个非常深的函数里,因为验签涉及到跨进程通信,因此这个地方的修改非常不显眼,及其容易引起误解!我改了两次:第一次只是加了注释,第二次将跨进程通信的 flag 打在了明处。
- 问题 3,session identity 的 lazy delete,当一个 session identity 失效的时候我们不会直接调用释放函数(在 CPP 里面是析构函数),相反我们只是打上一个 flag 表示无效,当 new 不出来的时候就遍历一遍链表查找失效的 identity 然后复用。这种方法很简单,效率高。但是带来了问题即多线程的争用,读写和无效化,解决方式很多,不多赘述。
2.2 分层理念
分层理念带来了逻辑的简单,但也带来了一些比较麻烦的问题。
-
问题 1,Record Layer 层的饿死:和 handshake layer 不同,record layer 实际上是无状态的,因此如果有报文加密又有报文解密的时候 record layer 应当向上驱动还是向下驱动呢? OPENSSL 将 record layer 包含在 handshakel layer 里解决这个问题,我的解决方法是如果上次是解密,此时有加密就先做加密。
-
问题 2,上下层的通信:通常来说层与层之间只有依赖,而没有通信,但 TLS1.3 里的 0-RTT 导致 1record layer 和 handshake layer 需要通信决定 lazy alloc enc/dec ctx 2 0-RTT 加密 /解密失败时候需要丢弃数据,而不是丢弃自动机,这两个情况就需要设计两个层的通信。我目前没有给出这个问题的规整解决,只是看 EARLY_DATA_STATUE,如果复杂化了再建立通信机制。
-
问题 3,0-RTT 的自驱动:TLS1.3 之前,自动机都是由数据驱动的,因此都是 RECORD LAYER 驱动了 HANDSHAKE LAYER,但是 0-RTT 导致了一个自驱动。正是这个自驱动导致 TLS 自动机的 RECORD LAYER 和 HANDSHAKE LAYER 分层复杂化,解决方法也不难,在 handshake layer 添加一个自己触发自己的状态,该状态会频繁触发 record layer 的自驱动(这里面隐藏着一个问题 1 )。
2.3 安全理念
- 问题 1,错误的 NST 颁发:NST(Session Identity)的颁发需要握手完成,但是握手完成不代表对端可信,必须是单转双之后才能发送 NST 。我最早的逻辑是只要发送了 finish 报文就发送 NST,后来我发现这个安全(逻辑)漏洞。
- 问题 2,错误的证书授权者:这个问题实际上是个协议+实现导致的问题,比方说我是一个黑客,我可以在某个不靠谱的 CA 申请了 GITHUB 的网站证书,然后我就可以假冒 GITHUB 了。关于这点我们提供了严格的 CA 校验,就是对于特定的访问只能通过特定的 CA,以此来对抗此类攻击。
- 问题 3,错误的证书验证(黑白名单功能):这个问题和实现没关系,是设计的缺陷。CRL 记录的是吊销证书,也就是黑名单,如果在 crl 里面找不到的证书会验证通过。这个缺陷我最后实现的时候改成了白名单模式,如果没找到就直接拒绝连接。
3 不遵守三种理念导致的问题
下面我会列出一些 CVE,这些 CVE 就是由于不遵守相应的理念导致的问题,作为一个安全协议的实现人员(分析人员),一定不要忽视小小的问题。有一点需要注意,我并不只列出 OPENSSL 的 cve,其他软件也会有,不过基本都是我修过 /探究过的。除此之外,我写过很多好玩的东西,比方说假 SNI 做代理,但这些都不属于标准行为,不具备任何参考价值。
- 臭名昭著的心脏滴血漏洞( CVE-2014-0160 ),该问题就是由于安全理念考虑不到位,对拷贝边界 /信息校验没做到位。因此在实现的时候,我们对边界坐了严格的校验。实际上我们在实现的时候有一部分可以上送 /下移的功能出于安全考虑的角度也没做,因为多做实际上就意味着可攻击。
- 因为区分存在用户名和不存在用户名导致的攻击( CVE-2018-15473 ),该问题是由于将存在的用户名校验流程和不存在的用户名校验流程的步骤可区分导致的信息泄漏,我当时修复就是把两个流程改一致。在平时的代码实现的时候,这个问题可能是不可避免的,也就是说,可能存在的用户名和不存在的用户名处理流程不同是客户想要的,尤其以银行为首的一部分企业。
- 弱 hash 算法( CVE-2004-2761 ),这个问题没啥好说的,现代计算机算力增强了,木的办法。再赘述一句,我国很多企业(还是以银行为首)使用的包括 ATM,内部网关,内部数据的通信还是以弱 TLS 为主,这是一个很可怕的事情。某些基本不存在任何安全可言的 CIPHER 还在使用。
- 因为校验没到位导致的( CVE-2015-4458 )和不安全重协商攻击,对消息的 mac 没包含足量信息导致。这里考虑的东西实际上是一种信任关系的拓展和层与层之间的关联,加密过的信息必须是经过上一步加密过的信息保证保密性和关联。有一个很有意思(很蛋疼)的东西,这种关联在设计不合理的时候反而会成为被攻击点,就好比 HKDF 应该包含 EXPAND+EXTRACT,如果只有一个那么可能反而是不安全的。
- 因为校验没做到位( Zombie POODLE ),这个实际上也可以说是因为区分了不同行为导致的,具体的不多赘述了。
- 证书链校验攻击( Microsoft CryptoAPI 错误等多种),这种问题在代码实现时很难被发现,因为人们对证书链校验的不完备导致。因此我们实现的时候改了逻辑,做了最严格的校验,发现一层无效,链式就无效。
- 随机数范围过小攻击,这个问题实际上是因为 random 种子需要随机导致的,我们只是规定了必须有种子,没详细规定种子的来源。
- BEAST 攻击,我当时看这个攻击看的是一愣一愣的,感觉就是俩字,牛逼,真正的协议攻击。该问题就是因为 IV 可见了。TLS1.0 之后在协议层面避开了这个问题
4 针对三种理念做的优化
这里的优化并不是单纯针对三种理念的,是一种杂糅的优化策略,有的优化访问速度,有的优化安全性能。
- 不要将证书过于绑定,安全方面的角度,不需要过多解释。
- 部署 OCSP STAPLING,这个东西实际上是减少访问时延的措施,比较有效。
- 性能转移,将一部分的计算操作放到客户端让复用减少性能消耗是一种非常常见的方法,这个不展开说了,需要具体情况而定。
结尾的闲言碎语
厂商大多不关心安全,只关心效益,是目光短浅吗?
写到这里差不多就可以结束了,就不多说了。TLS 这块还有啥不明白的直接告诉我就成了