Midori博客系列翻译(2)——对象即安全权能

在上一篇博客中,我们已经看到Midori是如何建立在类型,内存和并发安全的基础之上的。 在本文中,我们将看到它们又是使一些新颖的方法来实现安全变得可能,也就是说,这些方法让我们的系统消除了环境权限和访问控制问题,有利于编织到系统及其代码的结构中的权能之上。 与我们的许多其他原则一样,这种保证是通过编程语言及其类型系统“从构造”处提供的。

权能

首要的问题是,权能究竟是什么?

在我们大多数人都知道和喜爱的安全系统中(例如UNIX和Windows),授予做某事的许可是基于身份的,通常以用户(user)和组(group)的形式出现。 某些受保护的对象(如文件和系统调用)具有附加到这些身份的访问控制方法,这些控制限制了哪些用户和组可以使用该对象。在运行时,操作系统使用环境标识,如正在运行当前进程的用户,基于这些访问控制来检查是否允许执行所请求的操作。

为了说明这个概念,考虑对open API进行如下简单的C语言调用:

void main() {
    int file = open("filename", O_RDONLY, 0);
    // 与`file`进行交互...
}

在内部,此调用将查看当前进程的标识,给定的文件对象的访问控制表,以及相应地允许或拒绝调用的标识符。 有大量的机制用于模仿用户的各种机制,例如UNIX上的susetuid操作以及Windows上的ImpersonateLoggedOnUser操作。 但这里的主要问题是open仅仅是“知道”如何检查一些全局状态,以了解所请求操作的的安全含义。另一个有趣的方面是传递了要求进行只读访问的O_RDONLY标志,这也会影响对授权过程产生影响。

呃,那么这有何问题呢?

问题在于基于环境权限的访问控制这是不精确的,它依赖于对程序不可见的环境状态,因此无法轻松地对操作存在的安全隐患进行审计。 你只需要知道open是如何工作的,而且正是由于不精确,所以很容易出错,而错误通常导致安全漏洞。 具体来说,它很容易伪装成用户并欺骗程序做一些从未打算做的事情。 这被称为“混淆代理人问题”。你需要做的就是欺骗shell或程序以冒充超级用户,那么就几乎可以做任何特权操作。

相反地,基于权能的安全性不以同样的方式依赖于全局权限。 它使用所谓的“不可伪造的令牌”来表示执行特权操作的能力。 无论决策是如何制定的,都存在一个完全复杂的策略管理主题和关于社会和人类行为的授权行为。总的来说,如果软件无意执行某些操作,它根本就不会收到执行这些操作所需的令牌。 并且由于令牌是不可伪造的,程序甚至无法尝试操作。在像Midori这样的系统中,类型安全也意味着程序不仅不能执行未授权的操作,而且通常会在编译期间捕获这些操作。

在编译时拒绝了不安全操作,这有多酷!

正如您可能已经猜到的那样,之前假设的open API看起来会非常不同:

void main(File file) {
    // 和`file`交互...
}

好的,显然我们不再是在基于环境权限的访问控制范畴内了,那么事情将变得非常不同。 我刚刚没有提到的是,这里的其他部分(调用者)必须获得一个File对象,那他们又是如何得到的?

老套的回答是,没有人会谁在乎,获得的方式完全取决于调用者。但是如果他们确实持有File句柄,那么它们必须被授权访问File,因为在类型安全的系统中,对象引用是不可伪造的。 策略和授权的问题现在被推到可以说它们本来就属于的源头处。

我想我可能过度简化了一点,因为这个回答可能会产生更多的问题。 那么让我们继续深入分析。

那么,让我们再问一个问题:如何获得File对象?

上面的代码既不知道也不关心File来自何处。 它只知道给它一个具有类File的API的对象。 它可能是由调用者通过new操作获得, 更有可能的情况是,它是通过调用一个单独的实体获得的,比如文件系统或目录,而这两个实体也都是权能对象:

Filesystem fs = ...;
Directory dir = ... something(fs) ...;
File file = ... something(dir) ...;
MyProgram(file);

你现在可能真的会对我生气了。fs又来自哪里?我又如何从fs获取Directory对象?我又是如何从dir获取File对象的?我刚刚把所有有趣的话题都挤到一起,就像水球一样,但却什么都没回答!

现实情况是,当你尝试使用权能设计文件系统时,这些都是你现在将遇到的所有有趣问题。你应该不希望允许用户自由地在整个文件系统层次结构上进行枚举访问,因为如果用户可以访问Filesystem对象,或系统的根目录,那么它其实可以以向下传递的方式访问所有内容。这就是你开始和权能打交道时所做的那种想法。您认真考虑信息封装和曝光,因为您所拥有的只是保护系统安全的对象。也许,你会有一种方法,一个程序请求在文件系统的某个地方请求访问某个状态,声明,然后“权能母体”决定是否给你。这是我们的应用程序模型所扮演的角色,主要是如何main掌握程序清单所需的权能。从那时起,它只是对象。关键是整个系统中没有任何地方可以找到经典的环境权威,因此这些抽象都不能在其构造中“作弊”。

Butler Lampson的一篇经典论文“Protection”清楚地阐明了一些设计上关键基本原则,例如不可伪造的令牌。 从某种意义上说,我们系统中的每个对象都是它自己的“保护域”。如果想了解更多的细节(或者错误地任务访问控制列表和基于权能的系统是等价的),那么我也推荐“Capability Myths Demolished”中权能与经典安全模型进行比较和对比的方式。

Midori绝不是第一个以对象权能为核心构建操作系统的系统。事实上,我们从KeyKOS及其后继者EROSCoyotos中获得了重要的灵感。 这些系统像Midori一样,利用面向对象方式来提供权能,我们很幸运的是能够在团队中拥有这些项目的一些最初设计者。

在继续讨论之前,按顺序发出警告:即使某些系统不是真正的权能系统,它们也会混淆地使用“capability”这个术语。 例如,POSIX定义了这样一个系统,因此Linux和Android都继承使用了它。 虽然POSIX的“权能”比典型的经典基于环境状态和访问控制机制表现更好,因为它实现了比这些方式更细粒度的控制,但它们确实比我们在这里讨论的真正权能更接近经典模型。

对象和状态

作为对象的权能的一个好处是,你可以将有关面向对象的现有知识应用于安全和权限领域。

由于对象代表着权能,因此它们可以如你所希望的那样进行细粒度或粗粒度控制。您可以通过合成方式创建新的权能,或通过继承方式修改现有的权能。 依赖关系的管理方式与面向对象系统中的任何依赖关系一样:通过封装,共享和请求对象的引用。 因此你可以在安全领域利用各种经典的设计模式。 但我不得不承认这个想法过于简单,以至于使某些人感到震惊。

一个基本的想法是撤销(revocation)。 对象是具有类型的,我们的系统允许使用另一个实现来替换现有的实现。 这意味着如果你向我请求一个Clock对象,我无需在任何时候都向你授予访问时钟的权限,我甚至都不需向你提供真正的Clock对象。 相反地,我可以向你提供我自己实现的一个Clock子类,它作为真正Clock的代理,并可以在某些事件发生后拒绝你的请求。 因此你必须要么信任时钟源,要么在在不确定的情况下,显式地保护自己免受攻击。

另一个概念是状态。在我们的系统中,我们通过在编译期间“从构造”的方式,在编程语言中禁掉了可变的静态变量。这是正确的,不仅静态字段只能被写入一次,而且它所引用的整个对象图在构造之后也将被冻结。 事实证明,可变静态变量实际上只是环境权限的一种形式,不可变静态变量可以阻止用户在全局静态变量中缓存Filesystem对象,并自由地共享它,从而构造出一些和经典安全模型非常类似,而且是Midori极力避免的东西。 不可变静态变量在安全并发方面也有很多好处,甚至给我们带来了性能优势,因为这种方式下,静态只是变成了更加丰富的常量对象图,可以在二进制文件中固化和共享。

完全消除可变静态变量对Midori系统的可靠性带来了难以量化和低估的改善,而这也是我最怀念的地方之一。

回想一下上面提到的Clock,这是一个极端的例子。但没错的是,它没有诸如C的localtime或C#的DateTime.Now的读取时间的全局函数,而为了获得时间,你必须显式地请求Clock权能,而这具有消除整个类函数中非确定性的效果。一个无需IO,即可以在类型系统中确定(想想Haskell的monad) 的静态函数,现在变得纯函数化、可记忆化并且甚至可以在编译时进行eval (这有点类似于constexpr on steroids)。

我将首先承认,这将存在一个逐渐成熟的过程,是开发者需要面对的,正如他们学习了对象权能系统中的设计模式。 权能的“大袋子”随着时间的推移而增长,以及在不合时宜的时候请求权能是很常见的。例如,设想存在一个秒表Stopwatch的API,它可能会需要Clock,那么你是否需要将Clock传递给需要访问当前时间的每个操作,例如Start和Stop?或者你是否预先构建了一个带有Clock实例的Stopwatch,从而将秒表对时间的使用进行封装,使其更容易传递给其他对象(重要的是,意识到这基本上向接收者赋予了读取时间的权能)。另一个例子,如果你的抽象需要15个不同的权能才能完成它的工作,那么它的构造函也需要使用15个参数的列表?这将是多么笨重和烦人的构造函数!相反,更好的方法是将这些权能逻辑地分组到单独的对象中,甚至可以使用父类和子类等上下文存储来简化它们的获取。

经典的面向对象系统的缺陷也给这种方式带来了弱点。 例如,向下类型转换(downcasting)意味着你不能完全信任继承作为信息隐藏的手段。 如果你请求一个File,而我提供了派生自File的CloudFile类,它向其添加了自己的类似公有云的功能,那么你可能会悄悄地向下转换为CloudFile并执行我不想要的操作。 我们通过对类型转换的严格限制,以及将最敏感的权能放在另一个完全不同的计划上来解决此问题……

分布式权能

我将简要介绍在未来的帖子中需要进一步涉及的领域:我们的异步编程模型。 该模型构成了我们如何进行并发和分布式计算的基础,和我们执行IO的方式,以及与本文最相关的是,权能是如何扩展它们在这些关键域中的覆盖范围的。

在上面的Filesystem示例中,我们的系统通常在不同的进程中托管该Filesystem后面引用的真实对象。 这种方式下,调用一个方法实际上是将一个远程调用分派到另一个进程上,而进程为该调用提供相应的服务。因此,在实践中,大多数(但不是全部的)权能都是异步对象,或者更确切地说,是允许与服务交互的不可伪造的令牌,我们称之为“最终(eventual)权能”。而Clock却是一个反例,它是我们称之为“提示(prompt)权能”——它包含系统调用而不是远程调用。 但是大多数与安全相关的权能往往是远程的,因为大多数需要权限的操作通常会最终触及到IO上,而很少需要权限来仅仅执行简单地计算。 而实际上,文件系统、网络堆栈、设备驱动程序、图形界面以及更多的子系统都采用了最终权能的形式。

这种操作系统整体安全性的统一以及我们构建分布式的,高度并发的安全系统的方式,是我们最显著,最具创新性和最重要的成就之一。

我应该指出的是,就像通用权能的想法一样,类似的想法在Midori之前就已经存在。虽然我们没有直接使用,但是来自于Joule语言 和后来的E语言 的想法为我们提供了非常强大的构建基础。Mark Miller在2006年的博士论文是这整个领域的重要财富。 我们有幸与我曾合作过的最聪明的人之一密切合作,而他恰好是这两个系统的首席设计师。

封装起来

关于权能的优点还有太多地方可以说的。总之,类型安全的基础让我们大踏步前进,它产生了一种与环境权限和访问控制相比非常不同的系统架构。该系统以前所未有的方式将安全的分布式计算带到了最前沿。出现的设计模式确实充分利用了面向对象,也充分利用了各种看起来比以往更加重要的设计模式。

我们从未在该模型上进行较多的曝光。与策略管理等体系结构方面相比,面向用户的层面还未得到充分研究。例如,我很怀疑我们是否想在程序界面中提示我的母亲,她是否想让程序使用Clock。最有可能的方式是,我们希望自动授予某些权能(如时钟),并将其他权能通过组合的方式分组为相关的权能,而作为对象的权能幸运地为我们提供了大量已知的设计模式。我们的系统中确实有一些蜜罐,却没有一个被黑客攻击(好吧,至少我们尚不知道有被攻击成功过),但我无法确定最终系统的可量化安全性。因此我可以定性地说,在系统结构的许多层面上感觉具有更好的冗余安全性,但我们却没有机会大规模地加以证明。

在下一篇博客中,我们将深入研究贯穿于整个系统的异步模型。异步编程在当前是一个热门话题,例如await出现在C#ECMAScript7,以及PythonC++等语言中。同时加上通过消息传递方式连接的细粒度分解的轻量级进程,可实现高度并发,可靠且高性能的系统,其异步性与所有这些语言一样易于使用。下篇博客见!