Lattigo之整数加解密
Lattigo中目前所有的文件的目录结构如下:
lattigo/he
: 这是一个主要的包,为不同的明文域提供同态加密的接口。hebin
:二进制算术的同态加密。它包括了基于RLWE密文的盲旋转(又称查表)。hefloat
: 复数或实数上定点近似算术的同态加密。bootstrapping
: 复数和实数上定点近似算术的自举(bootstrapping),支持共轭不变环,具有自动打包/解包稀疏打包/小环度密文的批量自举,任意精度自举,以及高级电路定制/参数化。
heint
: 整数上模算术的同态加密。
lattigo/mhe
:多方(又称分布式或阈值)密钥生成和带有秘密共享秘钥的交互式密文自举的包。mhefloat
:同态解密、包括线性秘密分享的重加密,以及针对he/hefloat包的交互式密文自举。mheint
:同态解密、包括线性秘密分享的重加密,以及针对he/heint包的交互式密文自举。
lattigo/schemes
: 实现基于RLWE的同态加密方案的包。bfv
:Brakerski-Fan-Vercauteren比例不变同态加密方案的完全RNS变体。该方案通过一个bgv方案的封装实现。它提供整数上的模算术。bgv
: Brakerski-Fan-Vercauteren比例不变(BFV)和Brakerski-Gentry-Vaikuntanathan(BGV)同态加密方案的完全RNS泛化。它提供整数上的模算术。ckks
: 用于近似数字算术的全RNS同态加密(HEAAN,又称CKKS)方案。它提供了复数(在其经典变体中)和实数(在其共轭不变变体中)上的定点近似算术。
lattigo/core
: 实现Lattigo库的核心功能包。rlwe
: 通用RLWE基础上的同态加密的共同基础。它提供所有同态功能并定义所有不特定于方案的结构。这包括明文、密文、密钥生成、加密、解密和密钥转换,以及其他更高级的原语,如RLWE重新打包。rgsw
:一个完整的RNS变体的RGSW密文及其外积。
lattigo/ring
: RNS基础上的多项式模算术操作,包括:RNS基础扩展;RNS重缩放;数论变换(NTT);均匀、高斯和三元采样。lattigo/examples
: 可执行的Go程序,展示了如何使用Lattigo库。每个子包都包括测试文件,进一步展示了Lattigo原语的使用。lattigo/utils
: 通用实用方法。该包还包含以下子包:bignum
: 任意精度线性代数和多项式逼近。buffer
: 在io.Writer
和io.Reader
上读写的高效方法。factorization
: 中等大小整数的各种分解算法。sampling
: 安全字节采样。structs
: 包括序列化的通用结构体,如映射、向量和矩阵。
Lattigo库中包含的内容十分丰富,但从实用的角度,我们只需要从lattigo/examples
入手,当然我们先使用单方,多密钥的方案有空再聊,如果对源码有兴趣,可以看官网源码,接下来我们先实现数据加密。
BFV方案
我们先简单回顾一下BFV方案。BFV (Brakerski/Fan-Vercauteren) 方案是一种基于环上的学习困难问题的同态加密方案。在这里我们简要说明,有兴趣的还是看原文更准确。在BFV方案中,我们通常工作在一个特定的多项式环 ( R q = Z q [ x ] / ( x n + 1 ) ) (R_q = \mathbb{Z}_q[x] / (x^n + 1)) (Rq=Zq[x]/(xn+1)) 上,其中 ( q ) (q) (q) 是模数, $(n) $是度数,通常是 2 2 2的某个次幂,使得操作能够利用快速傅里叶变换(FFT)进行高效计算。
密钥生成
- 私钥: 私钥 (sk) 是一个随机选择的小多项式。
- 公钥: 生成公钥需要选择两个随机多项式 ( a a a), 和 ( e e e),其中 ( e e e) 是一个小的噪声多项式。公钥 ( p k pk pk) 是一个二元组 ( ( [ − ( a ⋅ s k + e ) ] q , a ) ([-(a \cdot sk + e)]_q, a) ([−(a⋅sk+e)]q,a)),其中操作是在多项式环 ( R q R_q Rq) 中进行,并且 ( [ − ⋅ ] q [- \cdot ]_q [−⋅]q) 表示模 (q) 的减法。
加密
给定明文 ( m ∈ R t m \in R_t m∈Rt),其中 ( t t t) 是一个比 ( q q q) 小得多的模数,加密过程如下:
- 选择一个随机多项式 ( u u u) 和两个噪声多项式 ( e 1 e_1 e1, e 2 e_2 e2)。
- 使用公钥 (
p
k
=
(
p
k
0
,
p
k
1
)
pk = (pk_0, pk_1)
pk=(pk0,pk1)) 来生成密文 (
c
=
(
c
0
,
c
1
)
c = (c_0, c_1)
c=(c0,c1)) 通过以下运算:
- ( c 0 = [ p k 0 ⋅ u + e 1 + m ⋅ q t ] q ) (c_0 = [pk_0 \cdot u + e_1 + m \cdot \frac{q}{t}]_q) (c0=[pk0⋅u+e1+m⋅tq]q)
- ( c 1 = [ p k 1 ⋅ u + e 2 ] q ) (c_1 = [pk_1 \cdot u + e_2]_q) (c1=[pk1⋅u+e2]q)
解密
给定一个密文 ( c = ( c 0 , c 1 ) c = (c_0, c_1) c=(c0,c1)),解密过程如下:
- 计算 ( m ′ = [ c 0 + c 1 ⋅ s k ] q m' = [c_0 + c_1 \cdot sk]_q m′=[c0+c1⋅sk]q)。
- 将 ( m ′ m' m′) 重新缩放回模 ( t t t) 的空间,得到明文 ( m = [ [ m ′ ⋅ t q ] ] t m = \left[ \left[ \frac{m' \cdot t}{q} \right] \right]_t m=[[qm′⋅t]]t)。
同态运算
-
加法: 给定两个密文 ( c ( 1 ) = ( c 0 ( 1 ) , c 1 ( 1 ) ) ) (c^{(1)} = (c_0^{(1)}, c_1^{(1)})) (c(1)=(c0(1),c1(1))) 和 ( c ( 2 ) = ( c 0 ( 2 ) , c 1 ( 2 ) ) ) (c^{(2)} = (c_0^{(2)}, c_1^{(2)})) (c(2)=(c0(2),c1(2))),它们的同态加法就是简单的逐项相加:
- ( c ( a d d ) = ( c 0 ( 1 ) + c 0 ( 2 ) , c 1 ( 1 ) + c 1 ( 2 ) ) ) (c^{(add)} = (c_0^{(1)} + c_0^{(2)}, c_1^{(1)} + c_1^{(2)})) (c(add)=(c0(1)+c0(2),c1(1)+c1(2)))
-
乘法: BFV方案的同态乘法较为复杂,因为它会导致密文中的噪声水平上升,而且乘法操作还会增加密文的度数。为了说明BFV方案中的同态乘法,给定两个密文$ (c^{(1)} = (c_0^{(1)}, c_1^{(1)}))$ 和 ( c ( 2 ) = ( c 0 ( 2 ) , c 1 ( 2 ) ) ) (c^{(2)} = (c_0^{(2)}, c_1^{(2)})) (c(2)=(c0(2),c1(2))),它们分别加密了明文$ (m^{(1)})$ 和 ( m ( 2 ) ) (m^{(2)}) (m(2))。我们想要计算一个新的密文,它加密了$ (m^{(1)} \cdot m^{(2)})$。
-
计算乘积:首先,我们计算两个密文的乘积的每一个组成部分:
-
( d 0 = c 0 ( 1 ) ⋅ c 0 ( 2 ) ) (d_0 = c_0^{(1)} \cdot c_0^{(2)}) (d0=c0(1)⋅c0(2))
-
( d 1 = c 0 ( 1 ) ⋅ c 1 ( 2 ) + c 1 ( 1 ) ⋅ c 0 ( 2 ) ) (d_1 = c_0^{(1)} \cdot c_1^{(2)} + c_1^{(1)} \cdot c_0^{(2)}) (d1=c0(1)⋅c1(2)+c1(1)⋅c0(2))
-
( d 2 = c 1 ( 1 ) ⋅ c 1 ( 2 ) ) (d_2 = c_1^{(1)} \cdot c_1^{(2)}) (d2=c1(1)⋅c1(2))
注意这些乘积 ( d 0 , d 1 , d 2 ) (d_0, d_1, d_2) (d0,d1,d2) 实际上组成了一个维度更高的密文。
-
-
重线性化:因为乘法会增加密文的度(即不再是简单的一对多项式),所以我们需要应用一个叫做重线性化的过程将加密后的结果重新映射回原始的密文空间。这需要用到额外的密钥(如重线性化密钥 r l k rlk rlk)。简单来说,重线性化的目的是将 ( d 2 ) (d_2) (d2) 的信息以某种方式融入 ( d 0 ) (d_0) (d0) 和 ( d 1 ) (d_1) (d1) 中,减少结果的总噪声水平,以保证解密的可行性。
-
假设 ( d 2 d_2 d2) 经过某种形式的变换后可以与 ( d 0 d_0 d0) 和 ( d 1 d_1 d1) 结合,这个过程可以抽象地表示为:
- ( c 0 ( m u l ) = d 0 + f ( d 2 , r l k 0 ) ) (c_0^{(mul)} = d_0 + f(d_2, rlk_0)) (c0(mul)=d0+f(d2,rlk0))
- ( c 1 ( m u l ) = d 1 + g ( d 2 , r l k 1 ) ) (c_1^{(mul)} = d_1 + g(d_2, rlk_1)) (c1(mul)=d1+g(d2,rlk1))
其中,( f f f) 和 ( g g g) 分别表示使用重线性化密钥 ( r l k 0 rlk_0 rlk0) 和 ( r l k 1 rlk_1 rlk1) 对 ( d 2 d_2 d2) 进行某种转换的函数。这些转换的目的是使 ( c 0 ( m u l ) c_0^{(mul)} c0(mul)) 和 ( c 1 ( m u l ) c_1^{(mul)} c1(mul)) 一起加密 ( m ( 1 ) ⋅ m ( 2 ) m^{(1)} \cdot m^{(2)} m(1)⋅m(2)) 的结果,同时保持整个密文的可解性。
-
-
结果:重线性化后,我们得到一个新的密文 ( c ( m u l ) = ( c 0 ( m u l ) , c 1 ( m u l ) ) ) (c^{(mul)} = (c_0^{(mul)}, c_1^{(mul)})) (c(mul)=(c0(mul),c1(mul))),它在 ( R q ) (R_q) (Rq) 上加密了 ( m ( 1 ) ⋅ m ( 2 ) ) (m^{(1)} \cdot m^{(2)}) (m(1)⋅m(2)) 的结果。
-
这个过程大大简化了BFV方案中的许多细节,特别是在同态乘法中。实际实现中,这些运算会涉及复杂的算法来确保在每次同态运算后,噪声水平仍然控制在可解密的范围内,因此感兴趣的还是读原文章更靠谱。特别是对于Lattigo中的实现,如果后续会阅读源码,我们再聊具体如何用代码实现这个过程。
BFV加密整数实例
-
声明参数
var err error var params heint.Parameters // 128-bit secure parameters enabling depth-7 circuits. // LogN:14, LogQP: 431. if params, err = heint.NewParametersFromLiteral( heint.ParametersLiteral{ LogN: 14, // log2(ring degree) LogQ: []int{55, 45, 45, 45, 45, 45, 45, 45}, // log2(primes Q) (ciphertext modulus) LogP: []int{61}, // log2(primes P) (auxiliary modulus) PlaintextModulus: 0x10001, // log2(scale) }); err != nil { panic(err) }
LogN: 14
:这是环的度的对数。环度指的是同态加密使用的多项式的最大次数加一。这里的值是14,意味着环度为( 2 14 2^{14} 214)。环度越大,能处理的数据量也越大,但计算复杂度也会增加。LogQ: []int{55, 45, 45, 45, 45, 45, 45, 45}
:这是一个整数数组,表示用于加密文本模数的素数大小(以比特为单位)。模数 Q Q Q是构成模数链的一系列素数的乘积。在这个设置中,有 8 8 8个素数,第一个素数的位大小为55,其余的位大小为45。加密文本模数越大,可支持的计算深度越高,但同时也会增加计算和存储的复杂度。LogP: []int{61}
:这是用于辅助模数 P P P的大小(以比特为单位)。辅助模数用于密文的重线性化和加法操作,有助于管理噪声。PlaintextModulus: 0x10001
:这是明文模数,通常用于将明文编码为多项式以及从多项式解码。这里的值是 65537 65537 65537,是一个常用的值,因为它既是一个素数也是一个 2 2 2的次幂加1(即( 2 16 + 1 2^{16} + 1 216+1)),这使得它在计算上既高效又方便。
-
构造公私钥
// Key Generator kgen := rlwe.NewKeyGenerator(params) // Secret Key sk := kgen.GenSecretKeyNew() // Encoder ecd := heint.NewEncoder(params) // Encryptor enc := rlwe.NewEncryptor(params, sk) // Decryptor dec := rlwe.NewDecryptor(params, sk)
-
生成测试数据
// Vector of plaintext values values := make([]uint64, params.MaxSlots()) // Source for sampling random plaintext values (not cryptographically secure) /* #nosec G404 */ r := rand.New(rand.NewSource(0)) // Populates the vector of plaintext values T := params.PlaintextModulus() for i := range values { values[i] = r.Uint64() % T }
values
:是一个数组,其中数组的维度为( 2 14 2^{14} 214),数组值都是在模 T T T下随机获取的。 -
对明文进行编码并加密
// Allocates a plaintext at the max level. // Default rlwe.MetaData: // - IsBatched = true (slots encoding) // - Scale = params.DefaultScale() pt := heint.NewPlaintext(params, params.MaxLevel()) // Encodes the vector of plaintext values if err = ecd.Encode(values, pt); err != nil { panic(err) } // Encrypts the vector of plaintext values var ct *rlwe.Ciphertext if ct, err = enc.EncryptNew(pt); err != nil { panic(err) }
pt
:将明文设置在模链的最高级,使用SIMD技术,并设置噪声为默认大小。 -
复制明文,并将解密结果进行比较
// Allocates a vector for the reference values want := make([]uint64, params.MaxSlots()) copy(want, values) PrintPrecisionStats(params, ct, want, ecd, dec)
具体来说,
PrintPrecisionStats
为:-
func PrintPrecisionStats(params heint.Parameters, ct *rlwe.Ciphertext, want []uint64, ecd *heint.Encoder, dec *rlwe.Decryptor) { var err error // Decrypts the vector of plaintext values pt := dec.DecryptNew(ct) // Decodes the plaintext have := make([]uint64, params.MaxSlots()) if err = ecd.Decode(pt, have); err != nil { panic(err) } // Pretty prints some values fmt.Printf("Have: ") for i := 0; i < 4; i++ { fmt.Printf("%d ", have[i]) } fmt.Printf("...\n") fmt.Printf("Want: ") for i := 0; i < 4; i++ { fmt.Printf("%d ", want[i]) } fmt.Printf("...\n") if !utils.EqualSlice(want, have) { panic("wrong result: bad decryption or encrypted/plaintext circuits do not match") } }
将密文信息进行解码,并解密进行显示输出,最后用
utils
中的函数来比较有没有误差。
-
以上实例具体在lattigo/examples/single_party/templates/int/main.go at v5.0.2 · tuneinsight/lattigo · GitHub,如果有需要可以自行查看源码,下一章我们具体看一看如何完成同态计算。