Tenseal库

在此记录Tenseal的学习笔记

TenSeal的原理介绍,参考:TENSEAL: A LIBRARY FOR ENCRYPTED TENSOR OP- ERATIONS USING HOMOMORPHIC ENCRYPTION 解读

介绍

在张量上进行同态计算的库,是对Seal的python版实现,给开发者提供简单的python接口,无需深究底层密码实现。

当前最新版本:3.11
位置:A library for doing homomorphic encryption operations on tensors

具备以下特点:

  • BFV方案的加解密(整数)
  • CKKS方案的加解密(浮点数)
  • 密文-密文、密文-明文的加法和乘法运算(同态计算)
  • 点积和矩阵乘法
  • 将Seal封装为tenseal.sealapi

安装

环境:MacOS + python3.9

pip安装

此方法安装出来的是Tenseal的库,是编译好的,是直接拿来用的,但不能源码修改,这种方法对于源码学习者,不建议。

前提:安装pip,也就是需要安装python,这里安装的是3.2版,、
一键安装: python3 pip install tenseal


举例:
(1)新建test.py文件

import tenseal as ts

# Setup TenSEAL context
context = ts.context(
            ts.SCHEME_TYPE.CKKS,
            poly_modulus_degree=8192,
            coeff_mod_bit_sizes=[60, 40, 40, 60]
          )
context.generate_galois_keys()
context.global_scale = 2**40

v1 = [0, 1, 2, 3, 4]
v2 = [4, 3, 2, 1, 0]

# encrypted vectors【编码和加密】
enc_v1 = ts.ckks_vector(context, v1)  
enc_v2 = ts.ckks_vector(context, v2)

# 密文+密文
result = enc_v1 + enc_v2
result.decrypt() # ~ [4, 4, 4, 4, 4]

# 点积:<密文,密文>
result = enc_v1.dot(enc_v2)
print(result.decrypt()) # ~ [10]

matrix = [
  [73, 0.5, 8],
  [81, -5, 66],
  [-100, -78, -2],
  [0, 9, 17],
  [69, 11 , 10],
]
# 密文向量*明文矩阵
result = enc_v1.matmul(matrix)
print(result.decrypt()) # ~ [157, -90, 153]

(2)执行:python3 test.py

cmake 安装

手动cmake安装,适合阅读源码者,这里安装的是最新版:3.11
(1)下载

git clone git://github.com/OpenMined/TenSEAL.git

(2)编译

mkdir build
cmake ..

翻好墙,耐心等待就行!

开始

Tenseal中很多细节都封装了,比如代码中就没有出现密钥生成算法!

同态加密

同态加密(HE)是一种加密技术,它允许对密文进行计算,并生成解密后与对明文进行相同计算的结果一致。

image

下面举个例子:

x = 7
y = 3

x_encrypted = HE.encrypt(x)
y_encrypted = HE.encrypt(y)

z_encrypted = x_encrypted + y_encrypted

# z should now be x + y = 10
z = HE.decrypt(z_encrypted)

TenSEALContext对象

TenSEALContext对象保存密钥和参数。
(1)下面创建一个TenSEALContext:

import tenseal as ts
context = ts.context(ts.SCHEME_TYPE.BFV, poly_modulus_degree=4096, plain_modulus=1032193)
context

输出:<tenseal.enc_context.Context object at 0x7fcd0b2e88b0>

需要指定要使用的HE方案(此处为BFV)及其参数。
(2)TenSEALContext现在持有私钥,可以其传递给需要私钥的函数。

public_context = ts.context(ts.SCHEME_TYPE.BFV, poly_modulus_degree=4096, plain_modulus=1032193)
print("Is the context private?", ("Yes" if public_context.is_private() else "No"))//私钥为不空返回 True
print("Is the context public?", ("Yes" if public_context.is_public() else "No"))//私钥为空返回 True

sk = public_context.secret_key()//暂存私钥

# the context will drop the secret-key at this point,删除私钥
public_context.make_context_public()
print("Secret-key dropped")
print("Is the context private?", ("Yes" if public_context.is_private() else "No"))
print("Is the context public?", ("Yes" if public_context.is_public() else "No"))

输出:
Is the context private? Yes
Is the context public? No
Secret-key dropped
Is the context private? No
Is the context public? Yes

(3)TenSEALContext包含的属性很多,因此值得一提的是其他一些有趣的属性。比如用于设置自动重新线性化、重新缩放(仅适用于CKK)和模数切换的属性。这些属性默认启用,如下所示:

print("Automatic relinearization is:", ("on" if context.auto_relin else "off"))
print("Automatic rescaling is:", ("on" if context.auto_rescale else "off"))
print("Automatic modulus switching is:", ("on" if context.auto_mod_switch else "off"))
输出:
Automatic relinearization is: on
Automatic rescaling is: on
Automatic modulus switching is: on

(4)TenSEALContext 还提供一个全局默认的scale(在使用CKKS方案时),当用户不提供时,默认使用这个

# this should throw an error as the global_scale isn't defined yet
try:
    print("global_scale:", context.global_scale)
except ValueError:
    print("The global_scale isn't defined yet")
    
# you can define it to 2 ** 20 for instance
context.global_scale = 2 ** 20
print("global_scale:", context.global_scale)

输出:
The global_scale isn't defined yet
global_scale: 1048576.0

加密和计算

(1)创建一个加密的整数向量。

plain_vector = [60, 66, 73, 81, 90]
encrypted_vector = ts.bfv_vector(context, plain_vector)
print("We just encrypted our plaintext vector of size:", encrypted_vector.size())
encrypted_vector

输出:
We just encrypted our plaintext vector of size: 5
<tenseal.tensors.bfvvector.BFVVector object at 0x7f8446d27e50>

这里是将一个明文向量加密(编码、加密)为一个BFV密文向量
(2)进行密文加法、减法和乘法。

#密文+明文
add_result = encrypted_vector + [1, 2, 3, 4, 5]
print(add_result.decrypt())
#密文-明文
sub_result = encrypted_vector - [1, 2, 3, 4, 5]
print(sub_result.decrypt())
#密文*明文
mul_result = encrypted_vector * [1, 2, 3, 4, 5]
print(mul_result.decrypt())
#密文+密文
encrypted_add = add_result + sub_result
print(encrypted_add.decrypt())
#密文-密文
encrypted_sub = encrypted_add - encrypted_vector
print(encrypted_sub.decrypt())
#密文*密文
encrypted_mul = encrypted_add * encrypted_sub
print(encrypted_mul.decrypt())

输出:
[60, 66, 73, 81, 90]
We just encrypted our plaintext vector of size: 5
[61, 68, 76, 85, 95]
[59, 64, 70, 77, 85]
[60, 132, 219, 324, 450]
[120, 132, 146, 162, 180]
[60, 66, 73, 81, 90]
[7200, 8712, 10658, 13122, 16200]

(3)c2p比c2c计算快的多

ciphertext to plaintext (c2p) and ciphertext to ciphertext (c2c)

import tenseal as ts
from time import time

# Setup TenSEAL context
context = ts.context(
            ts.SCHEME_TYPE.CKKS,
            poly_modulus_degree=8192,
            coeff_mod_bit_sizes=[60, 40, 40, 60]
          )
context.generate_galois_keys()
context.global_scale = 2**40

v1 = [0, 1111, 2222, 3333, 4444]
v2 = [4444, 3333, 2222, 1111, 0]

# encrypted vectors【编码和加密】
enc_v1 = ts.ckks_vector(context, v1)  
enc_v2 = ts.ckks_vector(context, v2)

t_start = time()
_ = enc_v1 * enc_v2 #密文*密文
t_end = time()
print("c2c multiply time: {} ms".format((t_end - t_start) * 1000))

t_start = time()
_ = enc_v1 * v2 #密文*明文
t_end = time()
print("c2p multiply time: {} ms".format((t_end - t_start) * 1000))

t_start = time()
_ = enc_v1.dot(enc_v2) #<密文,密文>
t_end = time()
print(_.decrypt())
print("<c,c>  time: {} ms".format((t_end - t_start) * 1000))

t_start = time()
_ = enc_v1.dot_(v2) #<密文,明文>
t_end = time()
print(_.decrypt())
print("<c,p> multiply time: {} ms".format((t_end - t_start) * 1000))

输出:
c2c multiply time: 10.8489990234375 ms
c2p multiply time: 3.325939178466797 ms
[12343211.655333618]
<c,c>  time: 27.49800682067871 ms
[12343211.655338768]
<c,p> multiply time: 22.28689193725586 ms

在密文上的逻辑回归训练和计算

待补充

近似计算(CKKS)

本节介绍CKKS方案原理及其实现,详细的CKKS解读请参考:
'Part 1, Vanilla Encoding and Decoding'.
'Part 2, Full Encoding and Decoding'.
'Part 3, Encryption and Decryption'.
'Part 4, Multiplication and Relinearization'.
'Part 5, Rescaling'.

CKKS原理

中文参考:
CKKS Part1:普通编码和解码
CKKS Part2: CKKS的编码和解码
CKKS Part3: CKKS的加密和解密
CKKS Part4: CKKS的乘法和重线性化
CKKS Part5: CKKS的重缩放

大致方案流程:
image

参数

(1)缩放因子(scaling factor)
CKKS方案的第一步是将实数向量编码为明文多项式。
缩放因子指的是编码精度,用数字二进制表示。直观地说,我们讨论的是二进制精度,如下图所示:
image
(2)模多项式的级数(poly_modulus_degree)
即多项式环上的\(Z_q=Z_q[X]/F(X)\)\(F(X)\)的级数\(N\)
\(N\)产生的影响:

  • 明文多项式的系数个数
  • 密文元素的大小
  • 方案的计算性能(越大越差)
  • 安全级别(越大越好)

在TenSEAL中,就像在Microsoft SEAL中一样,多项式模的次数必须是2的幂,比如:(1024,2048,4096,8192,16384,32768)

(3)模多项式的系数模数(coefficient modulus sizes)
多项式的系数模数(素数列表),即\(q\)
\(q\)产生的影响:

  • 密文元素的大小
  • 方案的安全级数\(L\),即乘法次数
  • 安全级别(越大越好)

在TenSEAL中,就像在Microsoft SEAL中一样,系数模数中的每个素数必须最多为60位,并且必须满足mod 2*poly_modulus_degree=1

密钥

(1)私钥
用于解密,不共享,在TenSEALContext对象中
(2)公钥
用于加密
(3)计算密钥(relinearization keys)
用于重线性化(密钥交换),在乘法后用于降低密文维数。可公开
(4)伽罗瓦密钥(Galois Keys)
用于批处理密文的旋转。可公开

批处理向量的旋转的应用是密文求和

内部计算

这些操作由TenSEAL自动执行。
(1)重线性化(Relinearization)
该操作在密文乘法后由TenSEAL自动执行,将密文的维数降到2维。若密文的维数维\(K+1\),则计算密钥的维数为\(K-1\)
(2)重缩放(Rescaling)
每次在密文密文或者密文明文后由TenSEAL自动执行。

计算误差随同态乘法次数增多呈指数增长。为了克服这个问题,大多数HE方案通常使用模交换(module switching)技术。CKKS中,使用重缩放,相当于模数切换。可以降低误差。在同态乘法后使用重缩放,误差线性增长,而不是指数增长。

即给定密文的模数为\(q_1,...,q_k\),经过重缩放后,密文模数变为\(q_1,..,q_{k-1}\),所相应的缩小密文中的“明文值”。

此步骤消耗系数模数\(q_1,...,q_k\)中的一个素数。当你消耗掉所有的时候,你将无法执行更多的乘法运算,即Leveled-FHE方案。

使用

引入

import torch
from torchvision import transforms
from random import randint
import pickle
from PIL import Image
import numpy as np
from matplotlib.pyplot import imshow
from typing import Dict

import tenseal as ts

Context

首先生成Context:

ctx = ts.context(ts.SCHEME_TYPE.CKKS, 8192, coeff_mod_bit_sizes=[60, 40, 40, 60])

其中:

  • 方案类型:ts.SCHEME_TYPE.CKKS
  • poly_modulus_degree:8192
  • coeff_mod_bit_sizes:系数模数大小,这里的[60, 40, 40, 60]表示系数模数将包含4个素数,分别为60位、40位、40位和60位。
  • global_scale:缩放因子(scaling factor),即\(2^{40}\)

TenSEAL支持在公钥和对称加密之间切换。默认情况下使用公钥加密。
默认情况下,会自动执行重线性化后和重缩放。通过generate_galois_keys产生伽罗瓦密钥(Galois Keys)

def context():
    context = ts.context(ts.SCHEME_TYPE.CKKS, 8192, coeff_mod_bit_sizes=[60, 40, 40, 60])
    context.global_scale = pow(2, 40)
    context.generate_galois_keys()
    return context

context = context()

明文张量(PlainTensor)

张量:可以看成一种数据存储格式
PlainTensor类作为一个转换层,将普通数据类型(例如List,array等)转换为tenseal所支持的明文形式
image

import numpy as np

plain1 = ts.plain_tensor([1,2,3,4], [2,2])
print(" First tensor: Shape = {} Data = {}".format(plain1.shape, plain1.tolist()))

plain2 = ts.plain_tensor(np.array([5,6,7,8]).reshape(2,2))
print(" Second tensor: Shape = {} Data = {}".format(plain2.shape, plain2.tolist()))

输出:
First tensor: Shape = [2, 2] Data = [[1.0, 2.0], [3.0, 4.0]]
Second tensor: Shape = [2, 2] Data = [[5.0, 6.0], [7.0, 8.0]]

从上面可以看出:plain1和plain2就是张量形式,包含数据和形状(shape)

加密

CKKS由于明文空间是浮点数或实数,而计算是在多项式环上,所以加密前需要先编码。
(1)编码
编码分为两步:浮点数 -》实数多项式 -》整数多项式

假设,模多项式的级数为\(N\),那么将\(N/2\)个浮点数编码到明文元素中,然后加密,同态计算就是对密文(多项式)上的系数计算(逐coefficient (一个系数就是一个slot?)),从而实现SIMD操作。整个过程叫做"打包"(batching
image
(2)加/解密
加密:对一个明文多项式加密
image

下面举一个例子:将明文张量(PlainTensor)加密为密文张量(encrypted tensor)

为了创建密文张量,TenSEAL会自动执行编码和加密。这适用于CKKS和BFV方案。
将明文张量(PlainTensor)加密为密文张量(encrypted tensor),存储形式为【密文、shape】

下面有几种密文张量形式:

  • BFVVector:1D(1维)整数数组
  • CKKSVector:1D(1维)浮点数数组
  • CKKSTensor:N维浮点数数组,支持密文张量的reshaping或者broadcasting操作

image

import tenseal as ts
import numpy as np

# Setup TenSEAL context
context = ts.context(
            ts.SCHEME_TYPE.CKKS,
            poly_modulus_degree=8192,
            coeff_mod_bit_sizes=[60, 40, 40, 60]
          )
context.generate_galois_keys()
context.global_scale = 2**40

plain1 = ts.plain_tensor([1,2,3,4], [2,2])
print(" First tensor: Shape = {} Data = {}".format(plain1.shape, plain1.tolist()))

plain2 = ts.plain_tensor(np.array([5,6,7,8]).reshape(2,2))
print(" Second tensor: Shape = {} Data = {}".format(plain2.shape, plain2.tolist()))

encrypted_tensor1 = ts.ckks_tensor(context, plain1)
encrypted_tensor2 = ts.ckks_tensor(context, plain2)

print(" Shape = {}".format(encrypted_tensor1.shape))
print(" Encrypted Data = {}.".format(encrypted_tensor1))


encrypted_tensor_from_np = ts.ckks_tensor(context, np.array([5,6,7,8]).reshape([2,2]))
print(" Shape = {}".format(encrypted_tensor_from_np.shape))

输出:
First tensor: Shape = [2, 2] Data = [[1.0, 2.0], [3.0, 4.0]]
Second tensor: Shape = [2, 2] Data = [[5.0, 6.0], [7.0, 8.0]]
Shape = [2, 2]
Encrypted Data = <tenseal.tensors.ckkstensor.CKKSTensor object at 0x7f9ddd530400>.
Shape = [2, 2]

从上面看出,将普通数据(list:[1,2,3,4])转换为明文张量(plain1),再加密为密文张量(encrypted_tensor1),内部存储【密文数据,shape】

同态计算

下面是CKKS所支持的密文张量计算:
image

下面举例:

import tenseal as ts
import numpy as np

# Setup TenSEAL context
context = ts.context(
            ts.SCHEME_TYPE.CKKS,
            poly_modulus_degree=8192,
            coeff_mod_bit_sizes=[60, 40, 40, 60]
          )
context.generate_galois_keys()
context.global_scale = 2**40

def decrypt(enc):
    return enc.decrypt().tolist()

plain1 = ts.plain_tensor([1,2,3,4], [2,2])
print("First tensor: Shape = {} Data = {}".format(plain1.shape, plain1.tolist()))

plain2 = ts.plain_tensor(np.array([5,6,7,8]).reshape(2,2))
print("Second tensor: Shape = {} Data = {}".format(plain2.shape, plain2.tolist()))

encrypted_tensor1 = ts.ckks_tensor(context, plain1)
encrypted_tensor2 = ts.ckks_tensor(context, plain2)

#密文(张量)+ 密文(张量)
result = encrypted_tensor1 + encrypted_tensor2
print("Plain equivalent: {} + {}\nDecrypted result: {}.".format(plain1.tolist(), plain2.tolist(), decrypt(result)))

#密文(张量)- 密文(张量)
result = encrypted_tensor1 - encrypted_tensor2
print("Plain equivalent: {} - {}\nDecrypted result: {}.".format(plain1.tolist(), plain2.tolist(), decrypt(result)))

#密文(张量)* 密文(张量)
result = encrypted_tensor1 * encrypted_tensor2
print("Plain equivalent: {} * {}\nDecrypted result: {}.".format(plain1.tolist(), plain2.tolist(), decrypt(result)))

#密文(张量)* 明文(张量)
plain = ts.plain_tensor([5,6,7,8], [2,2])
result = encrypted_tensor1 * plain
print("Plain equivalent: {} * {}\nDecrypted result: {}.".format(plain1.tolist(), plain.tolist(), decrypt(result)))

#取反:密文(张量)
result = -encrypted_tensor1 
print("Plain equivalent: -{}\nDecrypted result: {}.".format(plain1.tolist(), decrypt(result)))

#求幂:密文(张量)^3
result = encrypted_tensor1 ** 3
print("Plain equivalent: {} ^ 3\nDecrypted result: {}.".format(plain1.tolist(), decrypt(result)))

#多项式计算(整数):1 + X^2 + X^3,X是密文(张量)
result = encrypted_tensor1.polyval([1,0,1,1])
print("X = {}".format(plain1.tolist()))
print("1 + X^2 + X^3 = {}.".format(decrypt(result)))

#多项式计算(浮点数):1 + X^2 + X^3,X是密文(张量)
result = encrypted_tensor1.polyval([0.5, 0.197, 0, -0.004])
print("X = {}".format(plain1.tolist()))
print("0.5 + 0.197 X - 0.004 x^X = {}.".format(decrypt(result)))

输出:
First tensor: Shape = [2, 2] Data = [[1.0, 2.0], [3.0, 4.0]]
Second tensor: Shape = [2, 2] Data = [[5.0, 6.0], [7.0, 8.0]]
Plain equivalent: [[1.0, 2.0], [3.0, 4.0]] + [[5.0, 6.0], [7.0, 8.0]]
Decrypted result: [[6.000000000510762, 7.99999999944109], [10.000000000176103, 11.999999999918177]].
Plain equivalent: [[1.0, 2.0], [3.0, 4.0]] - [[5.0, 6.0], [7.0, 8.0]]
Decrypted result: [[-3.999999998000314, -3.9999999987240265], [-4.0000000013643, -4.0000000013791075]].
Plain equivalent: [[1.0, 2.0], [3.0, 4.0]] * [[5.0, 6.0], [7.0, 8.0]]
Decrypted result: [[5.000000678675058, 12.000001612431278], [21.000002812898412, 32.000004287986336]].
Plain equivalent: [[1.0, 2.0], [3.0, 4.0]] * [[5.0, 6.0], [7.0, 8.0]]
Decrypted result: [[5.000000676956037, 12.000001612473657], [21.000002810086173, 32.00000428474004]].
Plain equivalent: -[[1.0, 2.0], [3.0, 4.0]]
Decrypted result: [[-1.0000000012552241, -2.000000000358531], [-2.9999999994059015, -3.999999999269536]].
Plain equivalent: [[1.0, 2.0], [3.0, 4.0]] ^ 3
Decrypted result: [[1.0000008094463497, 8.000006439159353], [27.000021714154222, 64.00005146475934]].
X = [[1.0, 2.0], [3.0, 4.0]]
1 + X^2 + X^3 = [[3.000000945752252, 13.000006978595758], [37.00002291844665, 81.000053606697]].
X = [[1.0, 2.0], [3.0, 4.0]]
0.5 + 0.197 X - 0.004 x^X = [[0.6930000194866153, 0.8620000226394146], [0.9829999914891329, 1.0319998662943677]].

其中密文张量乘法后需要重线性化:
image
其中多项式计算(浮点数),来自:Logistic regression over encrypted data from fully homomorphic encryption

demo

下面对MNIST数据集的分类,使用一个卷积和两个完全连接的层以及一个平方激活函数。
它是同态加密的一个重要用例:来自:https://github.com/youben11/encrypted-evaluation
image

对卷积不了解,后期补充!

性能测试

下面将提供一些关于如何对同态加密应用程序进行基准测试的提示,并选择最合适的参数。

序列化:通信传输时需要序列化,比如:读写就是序列化

代码和结果:https://github.com/OpenMined/TenSEAL/blob/main/tutorials/Tutorial 3 - Benchmarks.ipynb

Context 序列化

结果:

  • 对称加密方案创建的Context比公钥加密方案创建的Context更小。
  • 减少系数模数(coefficient modulus)的长度会减少Context的大小,但也会减少可用乘法的深度\(L\),也会影响精度(对于CKKS)。
  • Galois密钥只会增加公共Context的大小(没有私钥)。仅当需要执行密文旋转时发送它们。
  • 重新线性密钥只会增加公共Context的大小。仅当需要执行密文乘法时才发送它们。
  • 当我们发送私钥时,可以重新生成重新线性化/伽罗瓦密钥,而无需发送它们。

密文(Ciphertext)序列化

设置的参数不同,会影响密文的序列化
对称或者公钥加密方案实际上并不影响密文的大小,只影响Context的大小。
下面结果是针对堆成加密场景:
【明文数据大小:8.8 KB】

  • 多项式模\(N\)的增加导致密文的增加。
  • 系数模数(coefficient modulus)的长度影响密文大小。
  • 系数模数大小的值会影响密文大小以及精度。
  • 对于一组固定的多项式模数\(N\)和系数模数,更改精度不会影响密文大小。

MNIST上的加密卷积

后续补充!

总结

posted @ 2022-05-19 21:26  PamShao  阅读(4698)  评论(2编辑  收藏  举报