如何编写更好的软件?– 与 Brian Warner 的访谈系列,第二部分

这是 Mozilla Hacks 新的 访谈系列 的第二部分。

“我们作为开发者,如何编写更加出色的软件?”

这是一个简单的问题,却没有简单的答案。编写优质代码很难,即使是拥有多年经验的开发者也是如此。幸运的是,Mozilla 社区由业界最优秀的开发、质量保证和安全人员组成。

这是我作为学徒向 Mozilla 最优秀的成员学习的系列访谈的第二部分。

介绍 Brian Warner

当我与我的经理首次讨论这个项目时,Brian 是我第一个想采访的人。Brian 可能没有意识到这一点,但自从我加入 Mozilla 以来,他一直是我非官方的导师之一。他是一位杰出的老师,并且拥有让安全变得易于理解的独特能力。

在 Mozilla,Brian 设计了“旧版”Firefox 同步的配对协议,设计了“新版”Firefox 同步的协议,并且在确保 Persona 的安全方面发挥了重要作用。在 Mozilla 之外,Brian 与他人共同创办了 Tahoe-LAFS 项目,并创建了 Buildbot

你在 Mozilla 做什么工作?

我的职位是云服务部门的员工安全工程师。我分析和开发用于安全管理密码和帐户数据的协议,并在不同的方式中实现这些协议。我还审查其他人的代码,研究外部项目以确定是否适合纳入,并努力跟上 0 天漏洞等安全漏洞以及可能影响我们以及我们可以使用的工具和算法的野外部问题。

用户体验与安全:这是一个错误的二分法吗?有些人认为,为了让安全性能良好,使用起来必须很困难。

有时我认为这是一个三方面的权衡。与其说是 x 轴、y 轴和一条不接触零点的对角线,不如说这是一个三方面的事情,其中另一个轴是你想要投入多少工作、你有多聪明、你愿意进行多少用户研究和实验。工程师通常不关注这些,但 UX 和心理学家会关注。我相信,也许更像是一种希望而不是信念,如果你为此投入足够多的努力,那么你实际上可以找到既安全又易用的东西,但你需要做更多工作。

诀窍是弄清楚人们想要做什么,并找到一种方法将他们不得不做出的任何安全决策融入他们的正常工作流程。就像你把家里的钥匙借给邻居,让他们在你度假时浇灌你的植物一样,你对他们掌握了多少权力有一个很好的了解。

围绕着它有一些社会结构,例如“我认为你不会复制那把钥匙,所以当我从你那里拿回来时,你就不再拥有我授予你的权力”。在正常的生活中,对于正常的非计算机行为和物体,我们发展了一些社会实践,我认为部分技巧在于使用它,并假设人们会期待类似的东西,然后找到一种方法让计算机的东西更像那样。

部分问题在于,我们最终要求人们做一些非常不自然的事情,因为很难想象或很难构建更好的东西。以密码为例。密码是一种糟糕的身份验证技术,原因有很多。其中一个原因是在大多数情况下,为了行使权力,你必须将权力交给你要证明身份的人。就像,“让我向你证明我知道一个秘密”……“好的,告诉我那个秘密”。这就引入了许多问题,比如如何正确识别你正在与谁交谈,以及确保没有人窃听。

除此之外,最好的密码将由计算机随机生成,并且相对较长。完全可以记住这样的东西,但这需要一定程度的练习和实践,这比任何一个程序都值得付出更多努力。

但是,如果你只有一个这样的密码,而且你唯一的用途是你的手机,那么你的手机现在就成了你的中介,为你管理所有这些东西,然后要求用户花更多精力管理那个密码可能是合理的。而且很明显,你的手机是你的延伸,更善于记住东西,而你在整个系统中需要的唯一密码就是引导程序。

所以,类似的事情,以及其他类似在罕见情况下增加努力的事情。在许多情况下,你每天做的事情可以非常容易和轻量级,只有当你丢失手机时,你才不得不回到更加复杂的事情。就像你只在钱包里放少量现金一样,偶尔你不得不去银行取更多钱。

我认为这样的事情完全可以做到,但陷入糟糕的模式却非常容易,例如责怪用户或将许多决策推给用户,而他们实际上没有足够的信息做出明智的选择,而且你给他们的许多选择都没有太大意义。

你认为许多用户不理解他们被要求做出的决策和权衡吗?

我认为这是非常真实的,而且我认为大多数情况下,这都是一个不合适的问题。这有点不公平。走近某个人并将他们置于这种尴尬的境地——你喜欢 X 还是你喜欢 Y——有点残忍。

另一个让我想到的事情是权限对话框,尤其是在 Windows 机器上。它们经常出现,即使只是为了执行一些非常基本的操作。这不像你试图做一些奇特或疯狂的事情。这些对话框声称要向用户征求许可,但它们没有解释上下文、原因或后果,不足以使它成为一个真正的问题。它们更像是一种要求或最后通牒。如果你说“不”,你就无法完成你的工作,但如果你说“是”,系统会告诉你,坏事会发生,而这将是你的责任。

它的目的是让用户做出明智的选择,但它是一种责怪用户的模式,一种责怪受害者的模式,就像“发生了不好的事情,但你点击了“确定”按钮,你已经承担了责任”。用户没有足够的信息做某事,而且系统设计得不好,以至于他们无法在不受伤害的情况下做他们想做的事情。

在“新版”同步正式发布的几个月前,该协议在公开论坛上进行了非常热烈和公开的讨论。这与通过模糊性实现安全恰恰相反。你希望通过这种方式实现什么目标?

我对那次讨论抱有一些不同的希望。我推动所有这些东西公开描述和讨论,因为这是正确的做法,这是我们开发软件的方式,你知道,这是开源的方式。所以我真的无法想象用其他方式来做。

我对公开发布这些东西的具体希望是,试图征求反馈并让人们寻找基本的设计缺陷。我希望让人们对安全属性感到满意,尤其是因为新版同步改变了其中的一些属性。我们正在从配对切换到基于密码的东西。我希望人们有时间感受到他们理解这些变化是什么以及我们为什么要做出这些变化。我们提出了设计标准和约束,这样人们就能看到我们必须切换到密码才能满足所有其他目标,以及在基于密码的安全性的前提下,我们可以做些什么。

然后,另一部分是,进行这种公开讨论并让尽可能多的经验丰富的人参与进来,这是我知道的唯一一种方法,可以让我们相信我们正在构建的是正确的东西,而不是错误的东西。

所以,这真的是更多眼球……

在协议或 API 设计人员坐下来编写规范或代码行之前,他们应该考虑些什么?

我想说,考虑一下你的用户需要什么。将他们试图完成的事情归结为最小的、最基本的东西。确定你可以提供的最少的代码、最少的权力,以满足这些需求。

这就像开发协议的敏捷版本。

是的。极简主义绝对有用。一旦你拥有了能够完成所需工作的基本 API,就开始考虑所有可能通过该 API 做的坏事。尝试弄清楚如何阻止它们,或者让它们变得过于昂贵而不值得去做。

安全方面的一个大问题是,有时你会问“问题 X 发生的可能性有多大?”。如果你设计了一些东西,并且有 1/1000 的可能性会发生某件事,即特定的输入集会导致这个特定问题发生。如果它确实是随机的,那么 1/1000 可能是可以接受的,1/1M 也可能是可以接受的,但是如果是在攻击者可以控制输入的情况下,那么它就不再是 1/1000,而是 1 除以攻击者选择让它变为 1 的次数。

这是一个看谁更聪明、谁更彻底的游戏。不得不进行这种案例分析,以找出可能发生的所有事情,它可能进入的每个状态,这很令人沮丧,但如果其他人决心找到一个漏洞,那么他们就会进行这种分析。如果他们比你更彻底,那么他们会发现你没有覆盖的问题。

这就是威胁建模的意思吗?

是的,不同的人用不同的方式使用这个词,我认为当你设计系统时,你是在设定游戏规则。你是在说会有这个游戏。在这个游戏中,爱丽丝会选择一个密码,而鲍勃试图猜出她的密码,等等。

你正在定义游戏规则是什么。所以有时规则会说……攻击者不能在防御系统上运行,他们唯一的访问方式是通过这个 API 调用,而这也是你为所有好玩家提供的 API 调用,但你无法区分好人与坏人,所以他们会使用相同的 API。

所以接下来你要弄清楚如果坏人只能做 API 调用,那么安全属性是什么,所以也许这意味着他们正在猜密码,或者这意味着他们试图通过给你一些你没有预期的输入来溢出缓冲区。

然后你退一步说“好吧,你在这里做了哪些假设,这些假设真的有效吗?”你在数据库中存储密码,假设攻击者永远无法看到数据库,然后系统的其他部分发生故障,哎呀,现在他们可以看到数据库了。好吧,撤回那个假设,现在你假设大多数攻击者无法看到数据库,但有时他们可以,你如何尽可能地保护数据库中的东西?

其他事情,比如“你打算防御哪些不同类型的威胁?”有时你会划一条界线,说“我们愿意尝试防御所有达到这个级别的威胁,但除此之外你就完蛋了。”有时这是一个非常实际的区别,比如“我们可以尝试防御它,但这会让我们花费 5 倍的成本。”

有时人们会尝试估计攻击者对攻击的价值与用户的成本,这有点像用预期价值进行保险建模。攻击者进行某项操作需要花费 X,而他们根据可能被抓获的风险获得了 Y 的预期收益。

有时可以重新安排系统,使激励措施鼓励他们做好事而不是坏事。比特币在这个领域经过了深思熟虑,在这个领域中,有一些明确的点,坏人,有人可能会尝试进行双重支付,尝试做一些违反系统的事情,但对于每个人,包括攻击者来说,都很清楚,他们的努力最好用在主流好事上。他们做好事显然会比做坏事赚更多的钱。所以,任何理性的攻击者都不会再是攻击者了,他们会成为一个好参与者。

系统设计人员如何最大限度地提高开发一个合理安全的系统的可能性?

我想说最大的准则是最小特权原则。POLA 有时是用来表达它的方式。任何组件都应该只有执行其特定工作所需的最小权限。这有很多含义,其中之一是你的系统应该由独立的组件构成,这些组件应该真正被隔离,以便如果其中一个组件出现故障或被攻击,或者只是行为不端,存在 bug,那么它至少造成的损害是有限的。

我喜欢用的例子是一个解压缩例程。比如 gzip,你从网络上接收字节,并尝试在进行其他处理之前对其进行扩展。作为软件组件,它应该是一个独立的 2 线捆绑。一边应该有一条线,带有压缩字节,另一边应该有解压缩数据输出。它必须分配内存并执行各种格式处理和查找表等等,但是,这个盒子无论如何,无论输入有多奇怪,或者这个盒子有多恶意,都不能做除了将字节从另一边吐出来之外的任何事情。

这有点像 Unix 进程隔离,除了一个进程可以执行系统调用来破坏你的整个磁盘,进行网络通信并执行各种操作。这只是一个管道输入和一个管道输出,别无其他。编写代码的方式并不总是容易,但通常是更好的。这是一个非常好的工程实践,因为它意味着当你试图弄清楚可能影响一段代码的因素时,你只需要查看那一段代码。这就是我们不鼓励使用全局变量的原因,这就是我们喜欢面向对象的代码设计的原因,在面向对象的代码设计中,类实例可以保护其内部状态,或者至少存在一种强烈的约定,即你不会到处戳其他对象的内部状态。拥有私有状态的能力就像拥有私有财产的能力,这意味着你可以计划你的行动,而不会受到无法预测的事物潜在的干扰。因此,如果事物被隔离,分析你的软件的可追踪性就会大大提高。这也意味着你需要一种内存安全的语言……

在非内存安全的语言中,大型单片程序真的很难让人对它充满信心。这就是我选择更高级的语言,这些语言对它们有内存安全性,即使这意味着它们的速度并不快。大多数时候你并不真正需要那种速度。如果你需要,通常可以将你需要的部分隔离到一个单独的进程中。

你在网上看到了哪些违反这些原则的常见问题?

好吧,特别是网络是一个有趣的地方。我们倾向于使用内存安全的语言来接收。

你说的是像 Python 和 JavaScript 这样的语言。

是的,而且我们倾向于使用更多面向对象的代码,更多隔离。我在网上看到的大问题是,未能验证和清理你的输入。或者,未能转义诸如注入攻击之类的东西。

你有丰富的经验审查已经编写的实现,Persona 就是一个例子。你在前端和后端分别看到了哪些常见问题?

这往往是转义东西,或者对数据来自哪里做出假设,以及攻击者如果发现有缺陷,他们能控制多少。

这就是你主张让追踪数据如何在系统中流动变得容易的原因吗?

是的,绝对的,如果你能从代码中缩小范围,看到一堆连接在一起的小组件,它们之间有线条,并说,“好吧,这个模块是如何得到这个名字字符串的?哦,它来自这个模块。它从哪里来的?然后追踪到,在这里,名字字符串实际上来自用户提交的参数。这是来自浏览器的,而浏览器正在将其作为 postMessage 的发送域生成。好的,攻击者对其中一个的控制力有多大?他们能做哪些让我们感到意外的事情?然后,在任何给定点找出类型,看看类型从一个到另一个的转换点在哪里,并注意是否有任何点你没有做到这一点,这种转换,或者你对类型感到困惑。毫无疑问,简单性、可见性和可追踪性分析是关键。

人们可以做些什么来简化数据流审计?

我认为,最大限度地减少不同代码段之间的交互是一个非常重要的事情。将行为隔离到特定的较小区域。尝试将整体功能分解成有意义的片段。

什么是纵深防御,开发者如何在系统中使用它?

“皮带和吊带”是经典的说法。如果有一件事出错,另一件事就会保护你。如果你既戴着皮带又戴着吊带,你会显得滑稽,因为它们是两种独立的工具,可以帮助你把裤子穿好,但有时皮带会断,有时吊带也会断。它们一起保护你免受裤子掉下来的尴尬。所以纵深防御通常意味着不要依赖边界安全。

这是否意味着你应该在整个系统中检查数据?

关于性能成本或复杂成本总是会有一个判断。如果你的代码充满了健全性检查,那么这可能会分散阅读你代码的人的注意力,让他们看不到正在进行的实际功能。这会限制他们理解你代码的能力,而理解你代码对于正确使用它并满足它的需求至关重要。所以,这始终是这种判断和权衡,是在过于冗长和不够冗长之间,或者是在检查太多和检查太少之间。

关于边界安全的想法,很容易陷入这种陷阱,在你程序的外面画一条虚线,说“坏人在外面,而里面的人都是好人”,然后在那条边界上实现你要做的防御措施,而里面不再做任何其他事情。我曾经与一些人交谈,他们认为,这有进化生物学和社会学的原因。人类在这些部落中发展,在这些部落中,基本上每个人都与部落中的其他人有亲缘关系,并且只有大约 100 个人,而你住的地方距离下一个部落很远。规则基本上是,如果你与某人有亲缘关系,那么你信任他们,如果你没有亲缘关系,那么你就会当场杀死他们。

这种方式有效了一段时间,但你无法构建超过 100 人的社会结构。在处理计算机时,我们仍然以这种方式思考。我们认为存在“坏人”和“好人”,而我只需要防范坏人。但是,我们无法在互联网上区分两者,而且好人也会犯错。因此,最小权限原则以及拥有彼此独立且访问权限非常有限的独立软件组件的想法意味着,如果一个组件由于有人破坏了它,或者有人欺骗它表现出与你预期不同的行为,或者它只是有 bug,那么它能造成的损害是有限的,因为下一个组件不愿意为它做太多事情。

你是否有来自你或其他人写的代码片段,你认为它特别优雅,其他人可以从中学习?

我想展示的一件事是为 Tahoe-LAFS 编写的 核心共享下载 循环。

在 Tahoe 中,文件被上传到多个部分冗余的“共享”中,这些共享被分发到多个服务器。稍后,当你想要下载文件时,你只需要获取一部分共享,这样你就可以容忍一定数量的服务器故障。

这些共享包含许多用于保护完整性的 Merkle 哈希树,这些哈希树有助于验证你正在下载的数据。这些哈希的位置并不总是事先知道的(我们没有精确地指定布局,因此替代实现可能会以不同的方式排列它们)。但我们希望快速下载并减少往返次数,因此我们猜测它们的位置并推测性地获取它们:如果事实证明我们错了,我们必须进行第二次传递并获取更多数据。

这段代码非常努力地尝试获取最少的数据。它使用一组压缩位图来记录我们要获取哪些字节(希望它们是正确的字节),哪些字节是我们真正需要的,以及哪些字节已经检索到,并只针对正确的字节发送请求。

让我对这个过于聪明的模块感到好笑的是,整个算法都是围绕着滚石乐队的歌词设计的。我想我从“你不能总是得到你想要的东西,但有时......你会得到你需要的”,然后从那里倒退。

关于这个算法的另一个教育意义是它太聪明了:在我们发布它之后,我们发现它实际上比它所替换的不太复杂的代码要慢。事实证明,读取一些大块(即使你获取了比你需要更多的數據)比读取大量小块(网络和磁盘 IO 负载)要快。我不得不运行一组大型性能测试来描述这个问题,并决定下次我会找到方法来衡量新算法的速度,然后再选择围绕哪首歌曲的歌词来设计它。:)。

你希望鼓励人们参与哪些开源项目?

就我个人而言,我非常关注安全的通信工具,因此我鼓励大家(特别是设计师和 UI/UX 人员)研究像 PondTextSecure 和我自己的 Petmail 这样的工具。我还对像 GNU FreedomBox 这样的各种“在家运行自己的服务器”系统感到兴奋。

人们如何才能跟上你的工作?

关注我在 https://github.com/warner 上的提交可能是一种不错的方式,因为我发布的几乎所有东西都最终在那里。

谢谢布莱恩。

记录

布莱恩和我涵盖了比我可以在一篇帖子中包含的更多内容。完整的记录 可以在 GitHub 上找到,它还涵盖了内存安全语言、在处理 HTML 时隐式类型转换以及布莱恩常用的 Python 工具。

下一个!

伊万·博伊利和彼得·德哈安都在下一篇文章中介绍。伊万领导云服务安全保障团队,并继续讨论安全主题,讨论他团队的安全审计方法以及开发人员可以用来自我审计其网站以发现常见问题的方法。

彼得是 Mozilla 令人难以置信的质量保证工程师之一,负责确保 Firefox 帐户不会崩溃。彼得谈论了他用来评估项目的预警信号、流程和工具,以及如何在让大家笑的同时给予打击。

关于 罗伯特·奈曼 [名誉编辑]

Mozilla Hacks 的技术布道者和编辑。就 HTML5、JavaScript 和开放网络发表演讲和博客。罗伯特是 HTML5 和开放网络的坚定支持者,自 1999 年起便一直从事网络前端开发工作,在瑞典和纽约市都有工作经历。他经常在 http://robertnyman.com 上写博客,喜欢旅行和结识新朋友。

更多罗伯特·奈曼 [名誉编辑] 的文章...