gitbook/代码精进之路/docs/89226.md
2022-09-03 22:05:03 +08:00

13 KiB
Raw Permalink Blame History

41 | 预案,代码的主动风险管理

上一次,我们聊了保持代码长治久安的基础——代码规范。这一次,我们接着聊第二个方面,代码的风险预案。

有些问题,并没有适用于各种场景的解决办法;有些设计,并不能适用于所有的用户;有些实现,并不能经受过去、现在和未来的检验。在你的日常工作中,有没有这样的情况出现?

做好预案,是我们管理风险的一个很重要的手段。代码的安全管理,也需要预案。

评审案例

让我们一起来看一段节选的Java代码变更。

     public static String[] getDefaultCipherSuites() {
-        int ssl_ctx = SSL_CTX_new();
-        String[] supportedCiphers = SSL_CTX_get_ciphers(ssl_ctx);
-        SSL_CTX_free(ssl_ctx);
-        return supportedCiphers;
+        return new String[] {
+                "SSL_RSA_WITH_RC4_128_MD5",
+                "SSL_RSA_WITH_RC4_128_SHA",
+                "TLS_RSA_WITH_AES_128_CBC_SHA",
                 // snipped
+            };
     }

对于这段代码我先做一些说明。其中“Cipher Suites”指的是我们在前面一篇文章中提到的TLS协议的密码算法族 “SSL_RSA_WITH_RC4_128_MD5”是一种基于RC4加密技术的算法族“TLS_RSA_WITH_AES_128_CBC_SHA”是一种基于CBCCipher Block Chaining链式加密模式的算法族。

getDefaultCipherSuites()这个方法返回值的顺序就是TLS协议使用这些算法的优先级别。比如变更后的代码“SSL_RSA_WITH_RC4_128_MD5”算法族具有最高的优先级。相应地 “SSL_RSA_WITH_RC4_128_SHA”具有第二优先级。在安全传输的连接中优先级靠前的算法会获得优先考虑。 一旦优先的算法被采用,其他的算法就不会被使用了。

这段代码是Andriod系统的一部分。这个修改发生在2010年5月份这样做是为了使用Android偏爱的RC4加密算法。有了这样的变更Android就能对算法的选择有更好的安排与控制。

想想上一篇文章中我们说到的BEAST攻击这个修改是不是很有前瞻性 BEAST攻击技术是在2011年9月份公布的有缺陷的算法是基于CBC模式的算法。Android提早了一年把涉及问题的CBC模式设为次优选择。Chrome浏览器可能更早做了类似的修改。所以当BEAST攻击技术公开后Google可以很自豪地说“我们很早就优先使用更安全的RC4算法啦。”

可是,这个变更,还是有一点小问题的。

案例分析

要想看清楚这个问题,我们还需要讲述一段小插曲。

在1999年设计TLS 1.0的时侯,有两种常用的加密算法类型。一个是分组加密技术把原数据分成若干的小块然后一小块一小块地分组加密。3DES是二十世纪九十年代最流行的分组加密算法。另一个是流加密技术这种加密方式是把原数据一位一位地运算。RC4是二十世纪九十年代最流行的流加密算法。对这两种算法TLS 1.0都是支持的。其中的分组加密算法TLS 1.0采用的是链式加密模式。

2011年9月25日BEAST攻击技术公开发表。通过上一篇的介绍我们都知道BEAST攻击技术针对的就是链式加密模式链式加密模式不再安全了。你有没有惊喜地发现TLS 1.0的设计真是周到居然还有一个流加密技术可以使用而且RC4算法被广泛支持。这真是一个可以救命的设计。

如果你回看2011年、2012年的安全分析文章很多业界的专家都会推荐使用RC4来替代链式加密模式很多产品也开始变更为优先使用RC4算法。毕竟BEAST攻击是一个不可忽视的安全问题而针对BEAST攻击的补救措施并不是一个完美的解决方案。在业界寻找链式加密模式的替代算法的同时优先使用RC4算法似乎可以让大家喘口气。

这的确是一个救命的设计,但是,这是一个巧合的设计吗? 如果身处1999年我还没有足够的经验来判断这样的设计是有意为之还是仅仅是一个巧合。但是20年后的今天 如果我们的产品只支持一种模式的安全算法,我一定如坐针毡。因为我知道,短则一两天,长则三五年,一个算法的理论模型或者实现方式几乎一定会被破解。战战兢兢地等待着这个算法被破解,然后再去寻找补救的措施,显然不是一个可以让工程师心情愉悦、身心放松的好选择。

虽然优先使用RC4可以让业界稍作喘息但是好景并不长。2013年3月13日一个研究小组公开了一个关于RC4算法的严重的安全漏洞。不同寻常的是这一次并没有合适的修改RC4算法的补救措施。该研究小组建议停止使用RC4TLS 1.0和1.1版本的用户应该转化到CBC模式的加密算法。这算是一个不小的玩笑很多应用刚从CBC模式切换到RC4算法不久就要重新调整再切换回去。

这就类似于两个病例。CBC模式虽然是一场大病可是有成熟的救治方案。虽然那里或者这里或许会留个疤可是手术一旦实施成功CBC模式照样活蹦乱跳。 这就好比以前的100米需要跑9.8秒,手术后也可以跑个十一二秒的。虽然离巅峰阶段有点差距,但是问题不算大。

而RC4的问题就像是医生诊断后直接重症监护并时刻准备后事了。冷酷而又无奈2013年3月13日RC4算法宣告重病缠身重症监护。

随后业界开始重新转换回CBC模式很多应用开始禁用RC4算法。2013年8月IETF提出了在TLS协议中禁用RC4算法的议案。2015年2月该议案获得通过。 RC4算法这个因高效、安全而著名的算法从2013年3月开始慢慢淡出人们的视野。

有了上面的小插曲,你知道上面案例代码的问题了吗? 这段代码写死了TLS协议算法的缺省优先级别。除非更改代码,否则这个缺省优先级别是无法更改的。一旦优先的算法出了问题,代码修改虽然简单,但是已部署产品的升级,有时候就是一件很复杂的事情。

世事无常,一个好的设计,需要有双引擎和降落伞

双引擎,长远之计

现代的客机,一般采用双引擎甚至多引擎设计。如果其中一个引擎失灵,依靠其他的引擎依然可以延程飞行。 有人戏称延程飞行是一个“要么多引擎要么去游泳”的设计理念。但是延程飞行时间也是有约束的比如不得超过90分钟。为什么呢 因为延程飞行时,就只有一个发动机在工作了。单引擎运转,总是有更大的安全隐患,这实在是让人不安!

需要注意的是,双引擎不是备份计划不是应急计划不是Plan B两个引擎日常都要使用。如果其中一个引擎闲置,那么当真正需要它的时候,我们就不知道它的状态如何,是否可以承担重任。

想一想为什么CBC模式出事的时候业界可以切换到RC4算法 RC4算法出事的时候业界可以切换回CBC模式 其中有很重要的两点值得考虑。

  1. 无论是CBC模式还是RC4算法都是实际投入使用的算法。

  2. 无论是CBC模式还是RC4算法都是大部分应用同时支持的算法。

这两条对于CBC模式和RC4算法之间的成功的切换都是必不可少的隐性条件。

如果我们理一理TLS协议发展的脉络就随时可以看到双引擎设计的理念的运用。

1999年TLS 1.0提供了CBC模式和RC4算法两种加密算法。随后2003年发现了CBC模式的安全问题。 2006年发布的TLS 1.1在协议设计层面修复了CBC模式的潜在问题提供了CBC模式和RC4算法两种加密算法。2008年发布的TLS 1.2添加了AEAD加密算法加上已被修复的CBC模式和RC4算法这样就有三种加密算法可供选择。2018年8月发布的TLS 1.3废弃了CBC模式和RC4算法只保留了AEAD算法但是AEAD算法有两个推荐选项分组密码的GCM模式和流密码的Chacha20/Ploy1305模式。到2018年8月TLS协议在这二十年里逐步废弃了二十年前最流行的算法。但是在整个过程中一直保持多算法并存的设计。

如果你熟悉JDK的安全规范和实现可能会注意到对于每一个类型的算法我们总是尽可能地提供多种选择。如果一个算法面临问题我们总是尽快地替换旧算法并且补充新的算法。这样尽快地结束单算法的延程飞行状态。所以提供多种选择不仅仅是为了提升丰富性也是为了在面临关键风险的时候有风险控制的办法。

对于生死攸关的风险点,我们要有双引擎设计的意识。 然而,也有双引擎解决不了的问题。即便是多引擎飞机,也需要备用降落伞。

降落伞,权宜之计

在上述案例的代码中,算法的缺省优先级别是固定的。 一旦优先的算法出了问题,该怎么办? 如果等到出了问题、蒙受了损失,再去寻找解决方案,就太晚了。一般情况下,一个好的软件应该备好降落伞,提前设计部署好这些意外风险的应急办法。我们永远不希望使用降落伞,但是如果有意外发生,降落伞的存在就非常必要了。随时需要,随时就可以拿来使用。

以JDK为例对于TLS协议的密码算法一旦一个算法出现问题修改源代码替换掉出问题的算法是JDK提供的常规解决方案。另外JDK还提供了多样的应急方案

  1. 修改JVM系统的安全参数Security Property降低出问题算法的优先级

  2. 修改JVM系统的安全参数Security Property废弃出问题算法

  3. 修改JVM系统的安全参数Security Property升级到没有问题TLS版本

  4. 修改应用的系统属性System Property, 使用指定的算法;

  5. 修改应用的系统属性System Property, 升级到没有问题的TLS版本。

JVM系统的安全参数可以控制运营在JVM上的所有应用程序而应用的系统属性一般只影响使用它的应用程序。

在JDK中可以通过修改<java-home>/conf/security/java.security文件设置JVM系统的安全参数。比如“jdk.tls.legacyAlgorithms”是一个设置TLS历史遗留算法的安全参数。一旦一个算法被设置为历史遗留算法这个算法就不会被优先使用除非不存在其他可替换的算法。如果我们把RC4算法设置为历史遗留算法它的优先级就被降到最低即使它的缺省优先级别是最高的。

jdk.tls.legacyAlgorithms = RC4_128

一旦在java.security文件中设置了这个参数所有使用这个JDK配置的应用程序都会受到影响。

一个应用程序运行时,可以指定系统属性,比如:

$ java -Djdk.tls.client.protocols="TLSv1.3" myApp

那么这个应用程序就使用TLS 1.3版本的客户端。 另外一个运行的程序也可以使用TLS 1.2。 两个运行程序的设置互不影响。

$ java -Djdk.tls.client.protocols="TLSv1.2" myApp

通过上面的例子,你可以看到,这些应急方案采用了配置参数的方式,使用非常简单,不需要运营代码的更改。简单、易用、快速上手,这是我们设计应急降落伞的一个思路。

一旦一个系统采纳了双引擎和降落伞的设计,系统的可靠性和抗风险能力往往会有大幅度提高。 可是,这并不是白白得来的。它同时也意味着软件研发的巨大投入,和软件复杂度的显著提升。

我们总是尽最大的可能使得软件程序简化、简化再简化。可是对于生死攸关的风险点,我们有时需要选择相反的方向,强化、强化再强化。不是所有的复杂都是必要的,也不是所有的复杂都是不必要的。软件的设计,是一个需要反复权衡、反复妥协的艺术

小结

通过对这个评审案例的讨论,我想和你分享下面两点个人看法。

  1. 尽管我们无法预料未来可能出现的风险,但是软件的设计和实现依然要考虑抗风险的能力

  2. 对于生死攸关的风险点,我们既要有长期的双引擎设计的意识,也要有权宜的应急预案

如果深入到软件的架构和设计里,双引擎和降落伞的使用随处可见,你愿意分享你见到过的双引擎和降落伞的案例吗?欢迎在留言区留言。

一起来动手

这不算是一个练习,而是一个请求。如果你有时间,你能够研究下你使用的语言、架构或者应用,找找其中的风险防范设计吗? 代码安全和风险控制,是一个需要超大范围合作的技术领域。我们也需要共同创作这一话题,共同学习其中的经验。

比如说,我有个疑问就是,很多业务需要手机验证码,当我的手机不能使用时,我还有没有办法操作我的银行账户?

如果你觉得这篇文章有所帮助,欢迎点击“请朋友读”,把它分享给你的朋友或者同事。