MIT6.858-SUNDR的实现
Contents
MIT6.858最后一个实验是复现论文Secure Untrusted Data Repository的串行化实现。在开始做这个实验之前,你应该仔细地阅读这篇论文。
源码阅读
MIT6.858提供了基本的文件系统代码,了解这个文件系统是如何构建的,对于代码实现的相当重要。
基本的类型定义
在SUNDR中,有用户和群组的划分。同时每一个文件都是由<principal, i-number>
对应的。显然,每个用户和群组都可以有多个文件,所以只需要再定义一个类包含用户(群组)和所有的文件个数。故源代码中定义了三个类型:
User
:继承于Principal
。Group
:继承于Principal
。I
:包含Principal
和n
。
基本的存储实现
在文件系统中,最基本的存储单元是Block
类,其可以存储数据、inode等信息。由于SUNDR基于快照的机制保障安全,故其与git的实现机制相似,基于content-hash机制对每一个Block进行哈希。故Block
类提供了两个函数:
store(blob)
:blob
是存储的数据,其为二进制数据。其返回值为二进制内容的sha1sum。load(chash)
:根据哈希值找到相应的block,进而读取block中的二进制数据。
同时,文件系统定义了Inode
类,用于作为元数据。其包含了某一个文件或者目录的基本信息,例如类型,创建的时间,修改的时间及其对应的所有的block的哈希值。
1 | class Inode: |
显然,Inode
类也是存储在block中的,所以我们需要基于ihash
通过调用block.load(chash)
函数得到其二进制数据,然后进行反序列化得到其字段值。当我们想要获得文件的值时,我们直接使用如下的函数即可:
1 | def read(self): |
然而,我们必须考虑目录这种情况。在SUNDR中,目录的每一项元素都是一个元组(filename, I)
。其中其定义了add
函数来实现在一个目录里面添加一个新项。同时定义了find_under
函数用来查找目录。
Itable
每个用户和群组都拥有一个i-table
。对于用户而言,i-table
保存的是i-number
到ihash
的映射。对于群组而言,i-table
保存的是i-number
到I
的映射。因此对于Itable
类而言,源码利用了python的动态性,直接定义了一个dict。
1 | class Itable: |
然后,其定义了一个关键的函数resolve
,基于i-table
解析得到ihash
或者I
。同时,该函数支持递归地解析I
最终得到ihash
。其思路实现比较简单。
最关键的在于modmap(mod_as, i, ihash)
函数,对于某个用户mod_as
而言,给定一个I
类,需要修改或者创建其在i-table
中的映射。然而关键在于I
有两类情况,一种是用户,另一种是群组。
- 对于用户而言,其
mod_as
和i.p
必须相等。我们需要修改或者创建其在i-table
中的映射,这个过程相当简单。 - 对于群组而言,情况就会比较复杂了。我们首先需要基于提供的
i
找到其对应的I
,也就是调用resolve
函数并进行一层的解析。如果I.p = mod_as
,证明群组的修改和用户的修改是一致的,我们一定会最先对用户的修改进行处理,所以此处不需要进行任何操作。如果不是那么证明我们需要创建一个新的I
,于是调用modmap(mod_as, I(mod_as), ihash)
在mod_as
用户的i-table
中添加映射。
文件系统API接口
源码已经给出了某些文件系统接口的实现,例如link
函数,link
函数的作用即将一个I
添加到当前目录中,其原理很简单,首先需要基于当前目录构建一个Directory
。然后调用add
函数添加目录,并使用modmap
修改映射关系。
1 | # Omit syntax check and write check |
在SUNDR论文中,文件系统的初始化是root用户生成一对私钥/公钥,同时创建.users
和.groups
文件,存储相应的公钥及印映射信息。文件系统使用init
函数提供了接口。其首先需要创建一个类型为目录的inode,然后添加.
和..
目录。同时需要创建.users
和.groups
文件,调用link
函数添加到根目录中。
在了解了基本的数据结构以及映射关系后,我们就可以开始编写自己的代码。
实现
实现创建文件和目录的功能
我们需要所做的第一个工作就是实现创建文件和目录。无论是文件还是目录我们都需要首先创建一个Inode
。然后我们需要将其存储在文件系统中,并得到其hash值ihash
。然后我们需要调用modmap
修改itable的映射关系。对于目录而言,我们需要做更多额外的工作。我们需要添加在当前目录创建.
和..
,并调用modmap
修改itable的映射关系。然后我们调用link
维护目录结构关系。
然而,我们需要考虑如果是为一个群组创建文件和目录呢?实际上面的流程都是一样的,因为对于群组来说,itable
是存储的映射,所以我们仅仅只需要更新这个映射关系即可。
1 | def _create(parent_i: I, name: str, create_as: User, create_for: Principal, isdir: bool): |
Version Structure List实现
目前所有的映射关系都是存储在current_table
中的,其是存储在内存中的,当有其他客户端时,其访问服务端,得不到current_table
的信息,所以需要对current_table
进行修改,其需要存储在文件系统中。在这个过程中,我们不考虑任何安全方面的实现。
我们首先需要认识到current_table
保存的是Principal
到Itable
的映射。由于函数resolve
以及modmap
严重依赖于current_table
,所以应该尽可能合理地设计数据结构,减少代码的更改。
在SUNDR原论文中,其提供的数据结构VersionStructure
包含每一个用户的i-handle
,所属的群组的g-handle
以及version_vector
。这是与当前的逻辑矛盾的,当前的映射单独保存了用户和群组的,为了简便起见,我没有采取这样的数据结构。我仍然按照当前的逻辑进行实现。因此,我定义了如下的数据结构。
1 | class VersionStructure: |
根据SUNDR论文,每次每个客户端对文件系统进行操作之间,都需要从服务器下载最新的VSL,当进行了修改需要对VSL进行验证,如果验证通过,则进行数字签名,上传VSL。从性能的角度来说,传递单独的VersionStructure
是节省带宽的,然而我认为此处不是这个lab的核心,因此我才用了最粗暴的方式,即上传整个VSL。故定义了如下的数据结构:
1 | class VersionStructureList: |
我们要在server端添加store_version_structure_list
RPC调用,以及read_version_structure_list
RPC调用。
1 | class SecFSRPC(): |
现在到达了最关键的一步,也就是我们需要修改resolve
和modmap
中的代码,我们需要替换掉current_table
。我们需要一点即可,即VersionStructure
存储的是i-handle
我们必须进行磁盘的读写,而不是类似源码中直接存储Itable
(实际上这一步是可以优化的,如果其i-handle
没有发生变化,我们可以实现缓存,而不是从磁盘中实现读写,然而我忽略了因为这并不是这个lab的核心)。
1 | def resolve(i: I, resolve_groups = True): |
1 | def modmap(mod_as: User, i: I, ihash) -> I: |
到了此处,我们就可以通过百分之87的测试。可见,我们已经完成了大部分的工作。剩下的就是实现安全方面的功能,这个lab最难的并不在于代码,而是你如何进行数据结构的设计。
安全方面实现
要实现安全方面就很简单了,需要考虑如下两个(由于时间关系,此处我并没有进行验证):
- 验证服务器端的身份
- 实现SUNDR协议
Author: shejialuo
Link: https://luolibrary.com/2023/09/11/MIT6-858-SUNDR%E7%9A%84%E5%AE%9E%E7%8E%B0/
License: CC BY-NC 4.0