参考文献:
Oblivious Random-Access Machine (ORAM) 是一种计算机模型,可以抵御(主动/被动)敌手观察到 “访存模式”。所谓 Oblivious 指的就是敌手无法区分不同的访存地址序列,只要这两个输入下程序的执行时间相同。ORAM 最初是用来做软件保护的,但之后在 MPC 等其他领域中大展拳脚(类似于 ZKP 的命运)。
应用场景:假设数据在内存中是加密的,敌手无法观察到内存中写入的数据是什么。但是,假如敌手可以观察到 CPU 的访存地址,那么就会泄露一定的信息(比如,相邻地址的数据被读/写了、某个地址的数据被访问了两次,等等)。
一个最简单的 ORAM 方案:假设在 RAM 中的程序,CPU 共执行 t t t 次访存,内存 MEM 包含 m m m 个 words,那么我们每次访存都:按顺序读取(即使 CPU 只写)并且写入(即使 CPU 只读)每一个字(即使这个 word 不读也不写),共计花费 t ⋅ m t\cdot m t⋅m 次访存。这样,自然地掩盖了访存模式,因为所有的包含 t t t 次访存的序列,都是以完全相同的 t ⋅ m t \cdot m t⋅m 次访存来实现的。
当然上述解决方案太愚蠢了(恐怖直立猿儿时的扳手指数数,就是个人形 ORAM)。一般地,访存次数 t t t 都远小于内存大小 m m m,因此上述方案的 m m m 倍减速是不可承受的。Goldreich 等人在 [GO96] 中给出了更高效的解决方案:
我们将 RAM 分解为两部分:CPU 以及 MEM,并将两者建模为 Interactive Turing Machine(ITM)。所谓 ITM 是一种 message-driven 的多带图灵机,按 rounds 对 “只读输入带、只写输出带、读写工作带、只读通信带、只写通信带” 进行一定的操作。简记 I T M ( c , w ) ITM(c,w) ITM(c,w) 表示工作带长度为 w w w(内存/寄存器的大小)、通信带长度为 c c c(每一轮的消息长度)的交互式图灵机。
这里的 ( i , a , v ) ∈ { 0 , 1 } 2 + k + O ( k ) (i,a,v) \in \{0,1\}^{2+k+O(k)} (i,a,v)∈{0,1}2+k+O(k) 是由 CPU 发出的访存指令。
一般地,我们认为敌手可以观察 MEM 的各个 Cell 是否发生读写(观察通信带),但是敌手无法观察 CPU 内的 Register 是否发生读写。现在我们可以给出 (确定性)RAM 的定义,它包括一对 ITM 以及它们的交互:
但是对于 ORAM 的实现来说,必须要考虑随机性,比如 Random Oracle 或者 PRF。我们需要定义可以访问 Oracle 尤其是 RO 的 RAM 模型。在复杂度分析时,我们认为询问 Orcale 是 “free” 的。 下面给出 Probabilistic RAMs 的定义:
下面我们定义 memory access pattern 以及 Oblivious RAMs。访存模式是一个地址序列 ( a 1 , ⋯ , a t ) (a_1,\cdots,a_t) (a1,⋯,at),其中 a i a_i ai 指定了 CPU 访问 MEM 时所请求的内存地址。
所谓的 “不经意”,就是说 RAM 执行过程中的访存模式不会泄露(前提是序列的长度本身是相同的),即敌手不可区分 CPU 访问 MEM 的指令中的内存地址序列。
现在,我们试图用 ORAM 来模拟任意一个 RAM,使得运行在 RAM 上的程序转变为运行在 ORAM 上的安全程序。基本要求是两者的函数性相同,安全性要求是后者应当是 Oblivious 执行的。另外,如果原始程序中,两个不同输入的运行时间(确定性的)相同,那么转换之后这两个输入的运行时间(随机变量的分布)也应该相同。
用 Probabilistic-RAM 不经意模拟 (确定性)RAM,定义如下:
对于 Orcale-RAM,可以类似的定义。注意区分两个神谕:原始 Orcale-RAM 的神谕、Probabilistic-RAM 使用到的 Random Oracle。
假设存在函数 g : N → N g:\mathbb N \to \mathbb N g:N→N,对于任意的输入 y y y,若 R A M ( y ) RAM(y) RAM(y) 的运行时间为 T T T,不经意转换后 R A M ′ ( y ) RAM'(y) RAM′(y) 的运行时间至多为 g ( T ) ⋅ T g(T)\cdot T g(T)⋅T,那么我们就说 overhead of oblivious simulations 是函数 g g g(开销即减速倍率)
有时候,我们要求 CPU 知道它在时刻 j j j 之前访问地址 a a a 的 MEM Cell 的次数。这有助于抵御 tampering adversary 把 Cell 的内容替换为旧内容。我们说模拟是 Time-labeled 的,如果存在一个函数 Q ( j , a ) Q(j,a) Q(j,a) 正确计数了时刻 j j j 之前形如 ( s t o r e , a , ⋅ ) (store,a,\cdot) (store,a,⋅) 的指令数。
软件保护的任务,可以归约到用 ORAM 模拟 RAM 的任务上。[GO96] 给出了两种 ORAM 的构造方法:平方根算法、层级算法。
正如上面所说的,只要我每次都读写全部的内存单元,就没有人可以区分出我的访存模式!但是这导致了 m m m 倍的减速,为了降低损失,我们可以仿照 Cache 的思路,假如访问的数据就落在 Cache 内,我们只对 Cache 的内容做完全的扫描,就可以实现 Oblivious 的访存了。
对于一个 R A M m RAM_m RAMm,平方根算法使用一个 O R A M m + 2 m ORAM_{m+2\sqrt m} ORAMm+2m 来模拟它。不经意的两个问题:如何隐藏访存序列的次序(shuffle the memory)、如何隐藏访问该地址的次数(be accessed at most once),使用随机置换 π \pi π 来解决以上问题。
以示区分,我们将原始 RAM 的内存单元称为 virtual address/word,而将 ORAM 的内存单元称为 actual address/word。我们将 O R A M m + 2 m ORAM_{m+2\sqrt m} ORAMm+2m 的 MEM 的内存空间分为两部分:
同时,令 ORAM 按照 epoch 将时间分片,每个时期 CPU 将执行 m \sqrt m m 次访存,然后对 MEM 重新执行一次随机置换,从而降低均摊成本。
初始化:将 ORAM 的 permuted memory 填充上 virtual words 以及 dummy words,将 shelter 的所有单元都置为 empty(注意区分 empty 和 dummy)
模拟过程:ORAM 按照 epochs 依次执行,每个 epoch 至多包含 m \sqrt m m 次 virtual address 的访存,
随机选择 [ m + m ] [m+\sqrt m] [m+m] 上的置换 π \pi π,将 permuted memory 中的虚拟地址 i i i 上的 word,映射到实际地址 π ( i ) \pi(i) π(i),这儿的置换操作是 Oblivious 的
每当 RAM 读/写虚拟地址 i i i 上的 word,假设这是第 j j j 次访存(用 CPU 寄存器 c o u n t count count 记录)
结束当前 epoch 时,将 shelter 中累积的脏数据写回到 permuted memory 中,这个过程也是 Oblivious 的
因为排序网络是数据无关的,step 1 和 step 3 是不经意的。由于 CPU 寄存器不可被敌手观察,因此 step 2.2 和 step 2.3 是不经意的。因为 CPU 扫描了整个 Shelter,因此 step 2.1 和 step 2.4 是不经意的。最终,平方根算法是一个 Oblivious Simulation,一个 epoch 计算复杂度为 O ( m log 2 m ) + m ⋅ O ( 2 m + log ( m + m ) ) O(m \log^2 m) + \sqrt m \cdot O(2\sqrt m + \log(m+\sqrt m)) O(mlog2m)+m⋅O(2m+log(m+m)),均摊的减速为 O ( m ⋅ log 2 m ) O(\sqrt m \cdot \log^2 m) O(m⋅log2m)。
实际上,将 Shelter 的大小设置为 m ⋅ log m \sqrt m \cdot \log m m⋅logm 将达到最优化,使用渐进复杂度更低的 [AKS83] 排序网络(但是隐藏的常数很大)成本也可以更低。
但是平方根算法的减速依然是 p o l y ( m ) poly(\sqrt m) poly(m) 量级的,正如但是往往 m ≫ t m \gg t m≫t,导致大内存 RAM 的模拟开销巨大。假设我们用一个 powerful CPU,它拥有 m \sqrt m m 个寄存器,就可以把 shelter 移动到 CPU 里,从而不需要一次次 scan 了。不过这依然不能达到 p o l y ( log ( t ) ) poly(\log(t)) poly(log(t)) 开销。
[G096] 给出了层次算法,使用不同大小的 Hash Table 排列成层次结构(类似于 L1 Cache、L2 Cache、Main Memory 的关系),越大的表更新频率越慢(类似于 Huffman Tree),可以将开销降低到仅仅 O ( log 3 t ) O(\log^3 t) O(log3t)
现在我们考虑受限的情形:virtual memory 的所有 words 都至多被访问一次。简记 A = { ( V 1 , X 1 ) , ⋯ , ( V n , X n ) } A=\{(V_1,X_1),\cdots,(V_n,X_n)\} A={(V1,X1),⋯,(Vn,Xn)} 是 n n n 个字,其中 V i V_i Vi 是虚拟地址, X i X_i Xi 是数值。
我们称 t t t-legal sequence 是一个虚拟地址访问模式,包括 t t t 次访存,且每个地址都仅被访问一次。对于虚拟地址 V 1 , ⋯ , V n V_1,\cdots,V_n V1,⋯,Vn,只要做 Hash 映射后没有超过 l = O ( log n ) l=O(\log n) l=O(logn) 个原像映射到同一个像,那么对于任意的 t ≤ n t \le n t≤n 的 legal sequence,上述 ORAM 的实际地址访问模式都是同分布的。即,对于受限情形,这个 ORAM 是 Oblivious 的。
现在我们考虑一般情况:访存模式中存在对于某地址的多次访问。只要我们将数据不断地使用新的 Hash Function 将数据(Oblivious)哈希到各个桶里,使得相邻两次 Hash 之间哈希表里的每个数据被至多访问一次,那么就转化为 Simple Case 了。我们使用多个大小不同的 Hash Table,并且它们的更新频率与大小成反比。
假设当前的访存模式长度为 t t t,我们申请 N = 1 + ⌈ log t ⌉ N=1+\lceil\log t\rceil N=1+⌈logt⌉ 块 buffers,第 i i i 个 buffer 包括 2 i 2^i 2i 个桶,每个桶的大小为 m = O ( log t ) m=O(\log t) m=O(logt)(仅仅是为了防止数据溢出)。我们将各个 buffer 的时间也划分为 epochs,第 i i i 个 buffer 的时间长度为 2 i − 1 2^{i-1} 2i−1(时间单位是访存次数,第一个哈希表的 epoch 长度为 1 1 1,但它并不主动重新散列;第二个哈希表每经过 2 2 2 次访存就重新散列,同时合并第一个哈希表的数据)。
我们令 h i j : { 0 , 1 } k → { 1 , 2 , ⋯ , 2 i } h_{ij}:\{0,1\}^k \to \{1,2,\cdots,2^i\} hij:{0,1}k→{1,2,⋯,2i} 是第 i i i 个 buffer 在它的第 j j j 个 epoch 使用的虚拟地址散列函数,它们相互独立且均匀随机,例如 h i j ( V ) : = f ( i , j , V ) ( m o d 2 i ) h_{ij}(V):=f(i,j,V) \pmod{2^i} hij(V):=f(i,j,V)(mod2i),这里 f f f 是个 RO。注意,随着时间 t t t 的推进,buffer 的数量 N N N 和 bucket 的大小 m m m 都在增加(在时刻 t = 2 i t=2^i t=2i),不过增加的频率指数级减慢。已经分配了内存的 buffer 不需要调整 m m m,毕竟它只是为了减少溢出概率,而上层的 buffer 中存储的数据量本来就不大。在时刻 t t t,大约消耗了 O ( t ⋅ log 2 t ) O(t \cdot \log^2 t) O(t⋅log2t) 的存储空间。
初始化:
模拟过程:时刻 t t t 原始 RAM 访问虚拟地址 V V V 的数据,
注意,当 t t t 是奇数时 max { j : 2 j ∣ t } = 0 \max\{j:2^{j} \mid t\}=0 max{j:2j∣t}=0,只有第 i = 1 i=1 i=1 个 buffer 的时期结束了,第二个 buffer 的时期还没结束,因此第 i + 1 i+1 i+1 个 buffer 的数据不需要重新散列(也不合并前 i i i 个 buffer 的数据)。此时 step 5 没有做任何事,虽然 h 1 , t h_{1,t} h1,t 被变更了(不影响 step 1 的 oblivious read,它同时扫描了两个桶)。
每经过 2 i + 1 2^{i+1} 2i+1 时间,第 i + 2 i+2 i+2 个 buffer 重新散列时将会清空前 i + 1 i+1 i+1 个 buffer。在第 ( 2 q + 1 ) ⋅ 2 i (2q+1) \cdot 2^i (2q+1)⋅2i 时刻,前 i i i 个 buffer 中至多被写入了 2 i 2^i 2i 个脏数据,此时第 i + 1 i+1 i+1 个 buffer 是空的,重新散列将这 2 i 2^i 2i 个数据合并到第 i + 1 i+1 i+1 个 buffer(含有 2 i + 1 2^{i+1} 2i+1 个桶)中。然后在第 ( 2 q + 2 ) ⋅ 2 i (2q+2) \cdot 2^i (2q+2)⋅2i 时刻,前 i i i 个 buffer 中的至多 2 i 2^i 2i 个数据以及第 i + 1 i+1 i+1 个 buffer 中的至多 2 i 2^i 2i 个数据合并到第 i + 1 i+1 i+1 个 buffer 中,紧接着就被合并到了第 i + 2 i+2 i+2 个 buffer 内。因此,第 i + 1 i+1 i+1 个 buffer 中所存储的巅峰数据量大小为 2 i + 1 2^{i+1} 2i+1,发生数据溢出的可能极小。同时由于每次 epoch 结束都更换 Hash 函数,因此碰撞也不会累积。
现在我们实现模拟过程中 step 5 里的 Oblivious re-hash 功能。抽象地,有两个数组 A , B A,B A,B:数组 A A A 含有 n n n 个桶,存储了至多 n n n 个(新)数据;数组 B B B 含有 2 n 2n 2n 个桶,存储了至多 n n n 个(旧)数据。我们将这至多 2 n 2n 2n 个数据,散列到数组 B B B 内。
[GO96] 将数组 A , B A,B A,B 合并为长度 n m + 2 n m nm+2nm nm+2nm 的数组,通过 [AKS83] 排序网络,类似于平方根算法,清理掉旧数据。然后插入一些占位符,再次排序使得前 2 n m 2nm 2nm 前缀出现桶结构,截断为 merge 之后的数组 B B B。
把上述的 Oblivious Hash 应用到层次算法的 re-hash 过程中,便得到了一个 ORAM。使用 AKS 排序网络,时间复杂度为 O ( t log 3 t ) O(t \log^3 t) O(tlog3t),减速因子 O ( log 3 t ) O(\log^3 t) O(log3t);使用 Batcher 排序网络,时间复杂度为 O ( t log 4 t ) O(t \log^4 t) O(tlog4t),减速因子 O ( log 4 t ) O(\log^4 t) O(log4t)。在整个 Oblivious Simulation 过程中,CPU 仅仅需要 3 3 3 个寄存器:两个用于存储 MEM 发送的数据(比较运算),一个用于 t t t 的计时(其实还有个布尔寄存器 f o u n d found found)。