同态加密的Bootstrapping操作最早由Gentry在他的博士论文里提出,是实现分级同态加密到全同态加密之间转换的关键步骤。目前所有的bootstrapping工作都是基于Gentry的这个想法,未有出其右者。

这篇博客打算讲一下Bootstrapping的原理,同时看一下在CKKS中,Bootstrapping是怎么实现的。

为了理解Bootstrapping的原理,我们首先看一个故事。

故事

Alice是一个资深的珠宝加工匠,她也管着一家珠宝店,某个聪明的年轻人Bob是她的学徒。

俗话说教会徒弟饿死师傅。Alice不想让Bob看到珠宝加工的每一个步骤执行之后半成品是什么样子的,因为如果Bob看见了加工过程中每一步的中间件的话,他就能悟出来加工的全过程,然后自己出来单干,用效率和年龄卷死自己。

不信的话,可以让另一个人在你的背上划拉一个字,对比一下看到划拉字的全过程和只靠触觉的话,辨识出来这个字是啥的可能性有多大。

但是两个人干活总是比一个人干活的产出要多。Alice舍不得撵走这样一个好用的劳动力。

Alice的核心需求:让Bob在看不见珠宝胚子的情况下还能做加工。

这天Alice发明了一个手套箱子,就像下面这张图里的一样:
在这里插入图片描述

只见这手套箱:

通体漆黑,从外面看不到里面有什么东西。有两扇门,一扇门上了锁,在开锁了之后可以往外取东西也可以往里放东西;另一扇门是单向的,可以往里面扔任何的东西,但是拿不出来。

于是Alice把手套箱交给Bob,让他来做珠宝加工。具体地,每次Alice会把珠宝坯子扔到手套箱里,让Bob对它按照固化的流程进行加工。等到加工完了之后,Alice再把手套箱的锁打开,把成品取出来。

如此这般这般如此了一段时间之后,Alice发现Bob隔着手套箱摸索着工作效率不行,以及由于手套箱自身设计的原因,在每一步加工过程中都会引入一些误差,相应地,在一个手套箱里加工珠宝最多只能加工L次,否则误差积累到足够大之后,在手套箱里的珠宝就会被加工废掉,成了残次品了。Alice暂时的解决方案是隔几个步骤就把中间件拿出来,自己再精琢一番,但是这么做实在是太费事了。

直到有一天,Alice又买了一个手套箱。为了区别,旧有的手套箱叫手套箱A,新买的手套箱叫手套箱B。

Alice自己把两个手套箱的钥匙都做了备份。今天开工前,Alice把坯子放在了手套箱A里,然后把手套箱A的钥匙放在了手套箱B中。

于是,当Bob隔着手套箱A做了若干次加工之后,他就把手套箱A整个扔到了手套箱B中。用已经放在手套箱B中的A的钥匙把A中的半成品取出来,在手套箱B中继续加工。

现实

到这里故事就讲完了。和现实世界对比,很显然珠宝坯子是私密数据,手套箱是同态加密方案,单向门意味着公钥,钥匙就是私钥。

这里的手套箱套娃操作,其实就是bootstrapping了。

本质上,bootstrapping这个操作就是在密文下走完解密-加密的一个流程。

一般的同态加密涉及加密、运算和解密三样东西。可以看到,手套箱故事能不能拿到现实中来应用,很大程度上取决于能不能在加密的情况下实现加解密函数。

而这不是一个简单的事情。

CKKS方案的Bootstrapping操作

CKKS方案的实现其实和Gentry的想法不完全一样。
在CKKS方案里没有用到对 s s s的加密结果,而是在密文下做了一遍解码和编码。

首先看一下CKKS方案的主要API和主要原理。可以参考这里,也可以看下面的图。当然了,这两篇文章里可能有一些地方表述不清甚至会出错,但是本文的目的不在于严谨地介绍,只是给大家留下一个大概印象方便更好地理解原文。
为了方便,首先放一个CKKS方案的主要API:

在这里插入图片描述
在这里插入图片描述
这里着重提一下CKKS的编码和解码问题。

在我的CKKS介绍文章里,我提到过里面的向量编码需要做一个插值。具体地,考虑分圆多项式的原根 ξ i \xi_i ξi 和明文向量的分量 m i m_i mi ,插值(编码)后生成一个多项式 p p p ,通过 p ( ξ i ) = m i p(\xi_i) = m_i p(ξi)=mi 来确定 p p p 的系数。

我们也提到过编码和解码可以用SIMD(单条指令操纵多条数据)的方式并行计算。比如,解码操作可以写成:
m = U p m = Up m=Up
其中这里的 p p p 是多项式 p p p 的系数组成的向量, U U U见下图。通过并行计算手段(GPU/FPGA等)对矩阵运算进行加速。
在这里插入图片描述

想要实现编码的话,只需要把这个过程倒过来就行了。

CKKS Bootstrapping方案中的一些预备知识

这一部分的东西,如果觉得看不明白,可以先跳到下一部分,直接把握Bootstrapping的脉络,根据脉络中的问题,再回来看各个细节。

首先,分析一下CKKS解密的方法:
m = ( c 0 + c 1 s ) m o d    q m= (c_0+c_1s) \mod q m=(c0+c1s)modq

这里的主要步骤有两个:一个是计算 c 0 + c 1 s c_0+c_1s c0+c1s,一个是计算 F ( x ) = x m o d    q F(x) = x \mod q F(x)=xmodq

但是这个 F F F是不连续的周期函数。我们想给它搞一个近似玩一玩。

我们给出一个前提条件:明文多项式 m m m 中系数的规模和 q q q 相比很小。其实这个事情可以办到,因为你完全可以在还剩足够多的乘法深度的时候就解密。

于是,我们对照着上面的前提条件,重新写一下解密的过程:
对某个level l l l下的一个密文 ( c 0 , c 1 ) (c_0, c_1) (c0,c1)进行解密:
t = ( c 0 + c 1 s ) m o d    Q l t = (c_0+c_1s) \mod Q_l t=(c0+c1s)modQl

这里, t = q I + m t = qI + m t=qI+m,其中 ∣ I ∣ < K |I| < K I<K K K K是一个正整数。

这个时候,我们可以用正弦函数(泛指数函数)对 F [ t ] = t m o d    q F[t] = t \mod q F[t]=tmodq 进行近似。

在这里插入图片描述

在这里插入图片描述
但是因为同态加密只支持加法和乘法,所以可以使用泰勒展开或者切比雪夫展开。

在CKKS方案中,还涉及到在密文情况下进行矩阵-向量乘法的操作。这里我们用一张图来表示一个经典的矩阵-向量乘法的步骤,使用到了CKKS全部的API——向量加法、向量乘法和重线性化、以及旋转操作。
在这里插入图片描述

CKKS Bootstrapping方案大体逻辑介绍

在这里插入图片描述

如图。

分为模数提升、解码、正弦近似、编码四个部分。都在密文下完成。

第一步,我们已有一个层数耗尽的密文 c t = ( c 0 , c 1 ) m o d    Q l ct = (c_0, c_1) \mod Q_l ct=(c0,c1)modQl,对应的明文多项式是 m ( X ) = ( c 0 + c 1 s ) m o d    q m(X) = (c_0 + c_1s) \mod q m(X)=(c0+c1s)modq

但是如果解密的过程中不模 q q q(或者模一个远大于 q q q的数 Q 0 Q_0 Q0)的话,记 t ( X ) = ( c 0 + c 1 s ) m o d    Q 0 t(X) = (c_0 + c_1s) \mod Q_0 t(X)=(c0+c1s)modQ0,这里就有 t = q I + m t = qI + m t=qI+m。其中 I I I是一个不太大的整数。那么有:
( c 0 + c 1 s ) m o d    Q 0 = t ( X ) (c_0 + c_1s) \mod Q_0 = t(X) (c0+c1s)modQ0=t(X)
体现在密文之中,就是 c 0 , c 1 m o d    Q l c_0, c_1 \mod Q_l c0,c1modQl 的模数被提升,重新改写成 c 0 , c 1 m o d    Q 0 c_0, c_1 \mod Q_0 c0,c1modQ0了。
注意,这里的 Q 0 Q_0 Q0 Q l Q_l Ql都是 q q q的倍数。

在这一步的执行中我们发现,对模数提升前后的 ( c 0 , c 1 ) (c_0, c_1) (c0,c1)进行解密: m ( X ) = ( c 0 + c 1 s ) m o d    q m(X) = (c_0 + c_1s) \mod q m(X)=(c0+c1s)modq,最后都是需要模 q q q的。这意味着,模数提升前后的 ( c 0 , c 1 ) (c_0, c_1) (c0,c1),解密后对应着相同的 m m m

做了这一步模数提升之后,我们又可以做很多的乘法运算了。

第二步,我们把加了密的明文多项式 t ( X ) = ( c 0 + c 1 s ) m o d    Q 0 t(X) = (c_0 + c_1s) \mod Q_0 t(X)=(c0+c1s)modQ0做密文下的解码运算。这里的解码的意思是,把 t ( X ) t(X) t(X)中的 X X X用原根进行带入,得到解码后的向量 t m o d    Q 0 t \mod Q_0 tmodQ0,可以通过前文所说的矩阵-向量乘法进行实现。

但是由于“编码”和“解码”的可逆性, t t t包含的信息并没有被消除。

第三步,我们要对 t m o d    Q 0 t \mod Q_0 tmodQ0在密文下进行正弦近似,得到 t m o d    q = m t \mod q = m tmodq=m,即获得真正的明文 m m m(因为由于模数的存在, m m m t m o d    Q 0 t \mod Q_0 tmodQ0本质上还是不同的)。

最后,在密文下重新走一遍编码的流程,把 t t t变成 t ( X ) t(X) t(X)。于是Bootstrapping的操作就完成了。

现在已有的一些改进Bootstrapping的工作,很多都是在此基础上进行的优化。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐