這篇文章,翻譯於:【CKKS EXPLAINED: PART 1, VANILLA ENCODING AND DECODING】
主要介紹為CKKS中編碼/解碼做鋪墊,講一些基礎知識
介紹
同態加密是一個很有前途的領域,它允許對密文進行計算。下面這篇優秀的文章《什么是同態加密》對同態加密是什么以及這一研究領域的利害關系進行了廣泛的解釋。
在本系列文章中,我們將深入研究Cheon-Kim-Song(CKKS)方案,該方案首次在論文《Homomorphic Encryption for Arithmetic of Approximate Numbers》中討論。CKKS允許我們對復數向量(也就是實數)進行計算。我們的想法是,我們將用Python實現CKKS,然后通過使用這些加密原語,我們可以探索如何執行復雜的操作,如線性回歸、神經網絡等。

上圖提供了CKKS的主要流程。我們可以看到,消息m是一個向量,我們希望對其執行某些計算,它首先被編碼為明文多項式$p(X)$,然后使用公鑰進行加密。
CKKS使用多項式,因為與向量計算相比,它們在安全性和效率之間提供了良好的折衷。【也就是使用RLWE和LWE之間區別,RLWE更加安全和高效】
一旦消息m被加密為c(一對多項式),CKKS就提供了幾個可以對其執行的操作,例如加法、乘法和旋轉。
如果我們用f表示函數,f是同態運算【加法和乘法】的組合,那么用私鑰去解密 c'=f(c),然后解碼,我們將得到m=f(m)。
實現同態加密方案的核心思想是在編碼、解碼、加密和解密上具有同態屬性,這樣,對密文的操作將被正確地解密和解碼,並提供輸出,就像直接對明文進行操作一樣。
因此,在本文中,我們將看到如何實現編碼和解碼,在后面的文章中,我們將繼續實現加密和解密,以獲得同態加密方案。
預備知識
建議掌握線性代數和環理論的基本知識,以便更好地理解CKKS是如何實現的。您可以通過以下鏈接了解這些主題:
- 線性代數導論為線性代數提供了良好的基礎。
- 環理論(數學113)是學習環理論的好資源。
具體到本文中,我們將依賴以下概念:
- 分圓多項式(Cyclotomic polynomials)是一種具有優良性質的多項式,當用作多項式模時,它具有高效的計算。
- 用於編碼和解碼的標准嵌入(canonical embedding)。它們具有很好的同構性質,即向量和多項式之間的一對一同態對應。
- 范德蒙矩陣(Vandermonde matrices)是一類特殊的矩陣,我們將用它來求標准嵌入的逆。
如果你想運行該項目的代碼,你可以在這里找到它。
CKKS編碼
CKKS利用整型多項式環的豐富結構實現其明文和密文空間。盡管如此,數據更多地以向量的形式出現,而不是以多項式的形式出現。
因此,我們需要將輸入的復數向量\(z\in \mbox{C}^{\frac{N}{2}}\)編碼成一個多項式\(m\left( X \right)=\frac{Z\left[ X \right]}{X^{N}+1}\)。
用N表示多項式模的次數,其中N是二的次冪。把\(\Phi _{M}=X^{N}+1\)(其中M=2N)叫做M次分圓多項式。明文空間是多項式環\(R=\frac{Z\left[ X \right]}{X^{N}+1}\)。用\(\xi _{M}=e^{\frac{2i\pi }{M}}\)表示M次單位根。
為了理解如何將向量編碼為多項式,以及對多項式執行計算是如何反映在向量上的,我們將首先用一個普通的示例進行實驗,我們將一個復數向量\(z\in \mbox{C}^N\)編碼為一個復數多項式\(m\left( X \right)=\frac{C\left[ X \right]}{X^{N}+1}\)。
然后介紹CKKS編碼:將一個復數向量\(z\in \mbox{C}^{N/2}\)編碼為一個整數多項式\(m\left( X \right)=\frac{Z\left[ X \right]}{X^{N}+1}\)。
普通編碼
這里我們將簡單介紹將一個復數向量\(z\in \mbox{C}^N\)編碼為一個復數多項式\(m\left( X \right)=\frac{C\left[ X \right]}{X^{N}+1}\)的情況。
為此,我們使用標准嵌入\(\sigma :\frac{\mbox{C}\left[ X \right]}{X^{N}+1}—>\mbox{C}^{N}\),對其進行解碼和編碼。
想法很簡單,將多項式\(m(X)\)解碼為一個向量\(z\) ,即用分圓多項式\(\Phi _{M}=X^{N}+1\)的根\(\xi ,\xi ^{3},...,\xi ^{2N-1}\)去計算該多項式。
所以為了解碼多項式\(m(X)\),我們定義\(\sigma \left( m \right)=m\left( m\left( \xi \right),m\left( \xi ^{3} \right),...,m\left( \xi ^{2N-1} \right) \right)\),這里\(σ\)定義了一個同構,這意味着它是一個雙射同態,因此任何向量都將唯一地編碼到其相應的多項式中,反之亦然。【意思就是,只需把這些根帶入多項式,就能得到對應的向量值,這就是解碼!】。
麻煩的是如何把向量\(z\in \mbox{C}^N\)編碼成多項式\(m\left( X \right)=\frac{C\left[ X \right]}{X^{N}+1}\),這意味着要求\(σ^{-1}\),因此問題就是要找到一個多項式\(m\left( X \right)=\sum_{}^{}{\alpha _{i}X^{i}\in \frac{\mbox{C}\left[ X \right]}{\left( X^{N}+1 \right)}}\),給出一個向量\(z\in \mbox{C}^{N}\),使得\(\sigma \left( m \right)=m\left( m\left( \xi \right),m\left( \xi ^{3} \right),...,m\left( \xi ^{2N-1} \right) \right)=(z_1,...,z_N)\)。
進一步研究這個問題,我們最終得到了以下系統:\(\sum_{}^{}{\alpha _{j}\left( \zeta ^{2i-1} \right)^{j}=z_{i}},i=1,...,N\)。
這可以看作是一個線性方程:\(\Alpha \alpha =z\),其中A是\(\left( \zeta ^{2i-1} \right)_{i=1,...,N}\)的范德蒙矩陣,$\alpha $是多項式的系數,z是需要編碼的向量。
所以我們有\(\alpha =\Alpha ^{-1}z\),\(\sigma ^{-1}\left( z \right)=\sum_{}^{}{\alpha _{i}X^{i}\in \frac{\mbox{C}\left[ X \right]}{X^{N}+1}}\)。
舉例
強力推薦使用:https://colab.research.google.com/drive/1C2WlzTh-28GUxobvIQK6Nj5GdfunAlH2?usp=sharing
現在讓我們來看一個例子,以便更好地理解。
假定\(M=8,N=\frac{M}{2}=4,\Phi _{8}\left( X \right)=X^{4}+1,\omega =e^{\frac{2i\pi }{8}}=e^{\frac{i\pi }{4}}\)
我們的目標是對以下向量進行編碼:[1,2,3,4]和[−1,−2,−3,−4] ;對它們進行解碼;對它們編碼后的多項式進行加法和乘法;然后對其計算結果進行解碼。

正如我們所見,為了解碼多項式,我們只需要根據M次單位根的冪來計算它。我們這里選擇\(\xi _{8}=\omega =e^{\frac{i\pi }{4}}\)。
一旦我們有了\(\xi\)和 \(M\),我們就可以定義\(σ\)及其逆,分別進行解碼和編碼。
實現
1、現在我們使用Python實現普通的編碼和解碼:
import numpy as np
# First we set the parameters
M = 8
N = M //2
# We set xi, which will be used in our computations(計算M次單位根)
xi = np.exp(2 * np.pi * 1j / M)
xi
輸出:(0.7071067811865476+0.7071067811865475j)
from numpy.polynomial import Polynomial
class CKKSEncoder:
"""Basic CKKS encoder to encode complex vectors into polynomials."""
def __init__(self, M: int):
"""Initialization of the encoder for M a power of 2.
xi, which is an M-th root of unity will, be used as a basis for our computations(計算M次單位根).
"""
self.xi = np.exp(2 * np.pi * 1j / M)
self.M = M
@staticmethod
def vandermonde(xi: np.complex128, M: int) -> np.array:
"""Computes the Vandermonde matrix from a m-th root of unity.(根據M次單位根計算范德蒙矩陣)"""
N = M //2
matrix = []
# We will generate each row of the matrix
for i in range(N):
# For each row we select a different root
root = xi ** (2 * i + 1)
row = []
# Then we store its powers
for j in range(N):
row.append(root ** j)
matrix.append(row)
return matrix
def sigma_inverse(self, b: np.array) -> Polynomial:
"""Encodes the vector b in a polynomial using an M-th root of unity(將向量編碼為多項式)."""
# First we create the Vandermonde matrix
A = CKKSEncoder.vandermonde(self.xi, M)
# Then we solve the system
coeffs = np.linalg.solve(A, b)//得到系數
# Finally we output the polynomial
p = Polynomial(coeffs)//轉為多項式形式
return p
def sigma(self, p: Polynomial) -> np.array:
"""Decodes a polynomial by applying it to the M-th roots of unity.(將多項式解碼為向量)"""
outputs = []
N = self.M //2
# We simply apply the polynomial on the roots(只需將M次單位根的次冪代入多項式即可)
for i in range(N):
root = self.xi ** (2 * i + 1)
output = p(root)
outputs.append(output)
return np.array(outputs)
2、讓我們先對一個實數向量進行編碼:
# First we initialize our encoder(新建一個對象)
encoder = CKKSEncoder(M)
b = np.array([1, 2, 3, 4])
b
array([1, 2, 3, 4])
現在對該向量進行編碼:
p = encoder.sigma_inverse(b)
p
輸出:\(x↦(2.5+4.440892098500626e^{-16}j)+((-4.996003610813204e^{-16}+0.7071067811865479j))x+((-3.4694469519536176e^{-16}+0.5000000000000003j))x^2+((-8.326672684688674e^{-16}+0.7071067811865472j))x^3\)
3、現在讓我們看看如何從多項式中提取我們最初得到的向量(解碼):
b_reconstructed = encoder.sigma(p)
b_reconstructed
輸出:\(array([1.-1.11022302e^{-16}j, 2.-4.71844785e^{-16}j, 3.+2.77555756e^{-17}j, 4.+2.22044605e^{-16}j])\)
我們可以看到解碼值和初始向量非常接近。
np.linalg.norm(b_reconstructed - b)//誤差范數
輸出:\(6.944442800358888e^{-16}\)
如前所述,不是隨機選擇的\(σ\)來編碼和解碼的,但它有很多很好的特性。其中,σ是同構的,因此多項式的加法和乘法將導致向量的加法和乘法。
\(σ\)的同態性質是由於:\(X^{N}+1=0\; and\; \zeta ^{N}+1=0\)
4、我們現在可以開始對幾個向量進行編碼,看看如何對它們執行同態運算並對其進行解碼。
m1 = np.array([1, 2, 3, 4])
m2 = np.array([1, -2, 3, -4])
p1 = encoder.sigma_inverse(m1)
p2 = encoder.sigma_inverse(m2)
輸出:\(p1=(2.5+4.440892098500626e^{-16}j)+((-4.996003610813204e^{-16}+0.7071067811865479j))x+((-3.4694469519536176e^{-16}+0.5000000000000003j))x^2+((-8.326672684688674e^{-16}+0.7071067811865472j))x^3\)
\(p2=(-0.4999999999999997-3.3306690738754696e^{-16}j)+((-0.7071067811865472-9.43689570931383e^{-16}j))x+((2.4563684419831585e^{-15}-2.5j))x^2+((0.7071067811865475+1.8318679906315083e^{-15}j))x^3\)
我們可以看到,加法非常簡單:
p_add = p1 + p2
p_add
輸出:\(x↦(2.0000000000000004+1.1102230246251565e^{-16}j)+((-0.7071067811865477+0.707106781186547j))x+((2.1094237467877966e^{-15}-1.9999999999999996j))x^2+((0.7071067811865466+0.707106781186549j))x^3\)
正如預期的那樣,我們看到p1+p2正確解碼為[2,0,6,0]。
encoder.sigma(p_add)
輸出:\(array([2.0000000e^{+00}+3.25176795e^{-17}j, 4.4408921e^{-16}-4.44089210e^{-16}j, 6.0000000e^{+00}+1.11022302e^{-16}j, 4.4408921e^{-16}+3.33066907e^{-16}j])\)
5、因為在進行乘法運算時,我們可能會得到階數大於N的項,我們需要使用\(X^N+1\)進行模運算。
要執行乘法,我們首先需要定義我們將使用的多項式模。
poly_modulo = Polynomial([1,0,0,0,1])
poly_modulo
輸出:\(x↦1.0+0.0x+0.0x^2+0.0x^3+1.0x^4\)
現在我們可以進行乘法運算了。
p_mult = p1 * p2 % poly_modulo
p
輸出:\(x↦(2.5+4.440892098500626e^{-16}j)+((-4.996003610813204e^{-16}+0.7071067811865479j))x+((-3.4694469519536176e^{-16}+0.5000000000000003j))x^2+((-8.326672684688674e^{-16}+0.7071067811865472j))x^3\)
6、最后,如果我們解碼它,我們可以看到我們得到了預期的結果。
encoder.sigma(p_mult)
輸出:\(array([ 1.-8.67361738e^{-16}j, -4.+6.86950496e^{-16}j, 9.+6.86950496e^{-16}j, -16.-9.08301212e^{-15}j])\)
因此,我們可以看到,我們的簡單編碼和解碼正常,因為它具有同態特性,並且是向量和多項式之間的一一映射。
雖然這是一個很大的進步,但我們實際上撒謊了,因為如果你之前注意到,當我們編碼時\(σ^{-1}\),多項式有復系數。因此,雖然編碼和解碼確實是同態的,而且是一對一的,但它們所處的域是相同的(復數域)\(C^N→ℂ[X] /(X^N+1)\)。因為我們真的希望多項式屬於(整數域)\(ℤ[X] /(X^N+1)\),為了使用整數多項式環的所有屬性,我們需要確保編碼輸出具有整數系數而不是復數系數的多項式。
舉一個比較具體的例子:

所以我希望你們喜歡這篇關於將復數編碼成多項式進行同態加密的小介紹。我們將在下一篇文章中看到如何實現CKKS中使用的實際編碼和解碼,敬請期待!
