V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hxndg
V2EX  ›  程序员

如何高效安全地实现 TLS 自动机?

  •  1
     
  •   hxndg · 2021-02-22 15:43:11 +08:00 · 1069 次点击
    这是一个创建于 1374 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 这块还有啥不明白的直接告诉我就成了

    第 1 条附言  ·  2021-03-15 23:57:15 +08:00

    5 再谈安全开发

    提到安全开发头头是道,真到开发的时候抓瞎了。身为一个从预研到开发的程序工程师,我来简单说说我所认识到的安全开发。由于我是单纯从开发干起,很多东西都是(我自己总结的)从开发的角度来谈的,因此难免有局限性。

    5.1 安全开发流程初期

    安全开发名字高大上,但是实际上是个苦差事,因为安全并不是水桶,而是一个气球:哪里扎破一个洞,立刻整个就崩了。因此初期把握好安全开发就很重要,我比较喜欢从下面几个方面来分析:

    • 目的是提供一款什么样子的安全产品?是提供端到端的加密?认证服务不可抵赖?秘钥管理?权限管理?安全监测?每种不同的属性对应着不同的功能。project leader还常常需要进行产品的横向对比,从而能够保证功能的灵活性和创造性。
    • 需要保护哪些敏感信息,等级分为几种?比方说身份信息,权限信息,支付信息,爱好信息等等方面都可以被划归为敏感信息。
    • 可能遭受哪些攻击?相比上两种,这个问题是最具体的攻击,常见的攻击很多种:供应链攻击,SQL注入
    • 可以复用哪些已有的安全工具和代码块?不要老去造轮子,要学会组合而不是创造。

    上面几个问题实际上是安全开发当中大的目标,分析好上面几个问题之后,最大的问题就来了,怎么落地?说起来也不难:

    1. 第一步 培训:培训的目的包含三个方面:使得每个参与者(不限于开发者,测试者,经理,审计....)了解产品提供的安全属性,安全准则和具体功能,尤其是要对产品经理培训使得其明白安全不是个绝对概念,是有选择倾向的,总要做出抉择;对每个参与进来的开发者培训,使得其清楚常见的安全知识,比方说对称加密的安全性由秘钥保证,hash不是加密,什么方法提供认证功能等基础知识;对核心开发者明确产品的核心逻辑和模块的设计(不一定是高速开发者,而是让核心开发者设计),必须达成对功能和安全的共识!我们在做TLS自动机的时候核心就三个人,每个人都可以相互替换对面的功能,TLS1.3新功能REUSE和0-RTT的核心逻辑也是我设计完了,争论了几次才确定的,我们的测试完全参与不到我们的培训里,因为听不懂。。。
    2. 第二步 需求和功能划分:确定需求是个很有趣,却不困难的工作。因为核心逻辑是围绕关键信息的,所以基本确定了敏感信息和核心逻辑之后,需求划分就比较简单了。但功能划分是个很麻烦的事情,一方面核心逻辑可能并不复杂,但是边界错误和细枝末节可能会出现大量问题,因此每个细微的工程都需要有人把关;另一方面,安全开发产品有很强的连续性,安全经常是成环的,因此要确保具体的代码开发者是了解开发的模块的输入/输出/隐喻/保障的,因此需要保证开发者是真的懂怎么回事,而不是似是而非模棱两可。这里注意,划分功能意味着划分责任,有的开发爱甩锅,有的QA爱甩锅,这时候就看project leader的功力了。
    3. 第三步 颁发安全准则:培训了,需求确定了,功能划分了,到了写代码的时候新的问题就来了,怎么保证代码是安全的呢?针对TLS可以看我写过TLS三理念,更具体的一般是:第三方安全性,很多安全问题是由第三方安全保证的,开发工程师精力不够,找QA啊;函数安全;工具安全。

    多赘述一点东西:安全属性不是隔离的,端到端加密是很多功能的保证,但是奥卡姆的剃刀原则务必不要忘记,如无必要勿增实体,不要把其他层的东西放到本层来做!

    5.2 安全开发流程中

    安全开发流程中期实际上是个工程师埋头干活,抬头开会报进度的流程,这阶段的关注点在于project leader上。project leader不但要对项目进度做好把握还要把握好需求变更的问题。project leader还需要检查行为变化,细化安全规范。这就需要project leader能够明确安全的行为,并且能够根据具体的功能黑盒灵活的改变行为。

    坦白讲,做TLS自动机研发的时候,虽然复用和0-RTT是我负责,但是关于排期这事我没咋催,主力是我,小功能分出去的时候我会提前培训开发的同事;每天问问进度和问题;明确行为规范。所以没啥排期的变更,行为这块一开始三个核心争了很久,也很确定,所以没遇到啥大问题。

    第 2 条附言  ·  2021-03-15 23:57:34 +08:00

    5.3 安全开发流程后期

    安全开发流程后期实际上主要的关注点不再开发工程师上了,在于QA和运营人员:运营人员要执行静态检测和动态监测;QA要执行模糊测试,输入测试等多种功能,这都不是问题。问题在于:project leader和开发者要在开发前期和开发后期以高度透明的角度,参与QA的测试CASE制定!换言之QA和开发必须都审核过测试CASE,从而避免出现什么“现实不可能发生,测试不全面”的扯皮。

    此外,需要记录开发过程当中遇到的实际问题和BUG,并且将这些问题归类,加入到安全规范中。一方面能够给工程师提供具体的案例分析,另一方面能够很明确的细化规范和经验。实际上关于0-RTT的DDOS攻击我在分析OPENSSL的时候虽然想到了但是没明确,是后来QA参与进来我们才定性规定这是个问题的。

    最后,最蛋疼的收尾工作来了:

    • 如果出现问题,如何应对?project leader需要根据具体的威胁制定针对补足方案,开发需要明确出现某种问题时,谁第一阶段参与,谁来参与追责。
    • 如果在具体的安全规范出现问题,依据什么原则,听谁的?
    • 开发和测试当中的资料归档在哪里?值得商榷的行为和安全保证的细节必须高度透明的写出来,并且能够被明确归档。
    • 可复用的工具,代码和属性是哪些?哪些应当成为标准组件?这些(第三方,第一方)工具,代码安全吗?实际上这里我最直接想到的就是HKDF-EXTRACT & HKDF-EXPAND,一个安全并且规范化的灵活伪随机码产生工具。实际上这个工作是需要一线开发工程师一起参与的。当然TLS研发完了,也就我自己想了想这块。。。。。

    5.4 遇到的安全开发中的问题

    简单说说我们在开发过程当中遇到的一些实际问题:

    • 人力不充足:人员短缺的问题很蛋疼,核心三人组是又订规范,又商量行为,光研究RFC就花了很久,更别提给其他人培训了。虽然能力增加了不少,但是确实很蛋疼。
    • 需求的变更:我对需求变更实在是太烦了,小的需求比方说拓展的支持还好说,解析的时候多花点功夫即可。如果大的属性变了,那排期就得大变。
    • 项目周期紧:这个对我们来说还好,虽然我们设计阶段功夫很多,但是设计好了我们的开发还是很快的。
    第 3 条附言  ·  2021-03-18 13:21:02 +08:00

    6 再提安全运营

    我只所以又加了一段这个有两个原因,一个是因为昨天晚上11点,我们的SE忽然给我打电话,说了一个卡顿的问题,然后说是必须明天10点给回复,然后我去看了下BUGZILLA对应的页面,前几天我在里面回复的要查询测试的点一个都没做/然后说好的统计数据和日志也没有上传。另一个事情是看FREEBUF上安全甲方群的讨论,里面的几个回复有所感慨,还是以开发的角度来聊聊技术支持和安全运营。

    从我的观点来看,安全运营实际上分为两个方向:对内--针对开发和测试,对外---对客户。这两点说完了,再写一点对SE的建议。

    6.1 对客户

    对客户而言,SE是问题的解决者,在解决问题的时候应当:

    • 正视漏洞(问题)存在的客观性,不要去抵赖或者无视问题,但是要明白安全是有所选择的。
    • 能够明白具体问题是什么,出在哪个层面,不要乱承诺。
    • 和服务商进行沟通,确保有业务保障的应急方案。应急方案是必须得有的,怎么能没有应急方案呢?应对策略
    • 和运维团队(如果可能的话)一起评估风险,并制定紧急干预措施(如果可行)
    • 事后复盘,评估服务商的综合能力。
    • 保证高效的解决方案的能力,不能磨蹭,尤其是不能在低效的沟通上磨蹭时间。

    除此之外,由于我们是TOB的,漏洞的发现往往会早于客户环境遇到,因此应该:

    • 积极关注客户的方案和环境,如果产品出现了问题或漏洞,要第一时间通知客户

    6.2 对内

    对内,或者说对开发和测试工程师而言,SE是寻求帮助的人,SE应当:

    • 了解产品,了解基本的技术。至少和开发/测试够通的时候能够说明白具体问题出在哪个组件上。
    • 能够清晰的够通具体问题和针对现象,对开发/测试给出的探查方向和原因要能搞明白,要做一道桥,而不是做一堵墙。
    • 解决问题,给出测试等流程要块,务必不要11号发现,18号截至,然后17号忽然电话打过来说赶紧给解决方案,然后一看啥分析数据都没有。

    6.3 SE的自我修养

    我不是SE,但是我想所有的问题都是一致的。

    • SE要去学习技术,不说精通,至少得熟悉模块/功能/划分
    • SE应当提炼工作手段,将可利用的部分组件化,而不是每次都抓瞎
    • SE应当适当的利用自动化技术,不要老是手动操作,尝试安全高效的解决问题。
    iloveayu
        1
    iloveayu  
       2022-07-28 15:28:19 +08:00
    感谢分享,OCSP STAPLING 搜过来的。
    hxndg
        2
    hxndg  
    OP
       2022-08-28 19:57:57 +08:00
    @iloveayu
    Nmmmm ,没想到会有人看这个,不过说起来可以问问你是做啥的 /为啥会搜索 ocsp ?还有就是安全开发现在咋样呢?最近觉得做持续集成太无聊了。。。。
    iloveayu
        3
    iloveayu  
       2022-08-30 22:59:10 +08:00
    @hxndg infra 运维、devops 都做过,目前转开发写业务代码,当时在对比不同浏览器对 ocsp 这块的实现处理是不是有区别,Google 过来的。
    哈哈,啥方向做时间长了都会无聊,说明该换换口味了 ; )
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   6064 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 02:26 · PVG 10:26 · LAX 18:26 · JFK 21:26
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.