Tenseal庫

PamShao發表於2022-05-19

在此記錄Tenseal的學習筆記

介紹

在張量上進行同態計算的庫,是對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上的加密卷積

後續補充!

總結