Tongsuo 支援半同態加密演算法 Paillier

SOFAStack發表於2022-11-24

圖片

文|王祖熙(花名:金九 )

螞蟻集團開發工程師

負責國產化密碼庫 Tongsuo 的開發和維護

專注於密碼學、高效能網路、網路安全等領域

本文 4316 字 閱讀10 分鐘

1. 背景

《Tongsuo 支援半同態加密演算法 EC-ElGamal》中,已經闡述了同態和半同態加密演算法的背景和原理,可以移步查閱。總之,同態演算法在隱私計算領域有著重要的作用,目前應用比較廣泛的是 Paillier 和 EC-ElGamal 半同態加密演算法,它們介面類似且只支援加法同態。

但是它們兩者的效能和原理有很大的差異:

原理方面,Paillier 是基於複合剩餘類的困難性問題 (大數分解難題) 的公鑰加密演算法,有點類似 RSA;而 EC-ElGamal 是基於橢圓曲線數學理論的公鑰加密演算法,其安全性理論上要比 Paillier 要更好。

效能方面,EC-ElGamal 的加密和密文加法效能要比 Paillier 好;而 Paillier 的解密和密文標量乘法效能要比起 EC-ElGamal 要更好更穩定 (EC-ElGamal 的解密效能與解密的數字大小有關係,數字越大可能需要解密的時間越長,這與 EC-ElGamal 解密用到的解密表有關係,而 Paillier 的解密就沒有這個問題。)

所以這兩個產品各有優劣,大家可以根據自己的業務特點選擇使用 Paillier 還是 EC-ElGamal。

2. Paillier 原理

2.1 金鑰生成

1.隨機選擇兩個大素數 p、q,滿足 圖片,且滿足 p 和 q 的長度相等;

2.計算圖片以及 圖片圖片表示最小公倍數;

3.隨機選擇整數圖片,一般 g 的計算公式如下:   

a. 隨機選擇整數圖片;   

b. 計算:圖片,為了簡化和提高效能,k 一般選 1,g=1+n;

4.定義 L 函式:圖片,計算:圖片

5.公鑰:(n, g),私鑰:(λ, μ)。

2.2 加密

  1. 明文 m,滿足 −n<m<n;
  2. 選擇隨機數 r,滿足  0≤r<n  且  圖片
  3. 計算密文:圖片

2.3 解密

  1. 密文 c,滿足圖片
  2. 計算明文:圖片

2.4 密文加法

  1. 密文:c1 和 c2,圖片,c 就是密文加法的結果。

2.5 密文減法

  1. 密文:c1 和 c2,計算:圖片,c 就是密文減法的結果。

2.6 密文標量乘法

  1. 密文:c1,明文標量:a,計算:圖片,c 就是密文標量乘法的結果。

3. 正確性

3.1 加解密正確性

公式推導需要用到 Carmichael 函式和確定合數剩餘的公式,下面簡單說明一下:

● Carmichael 函式

a. 設 n=pq,其中:p、q 為大素數;

b. 尤拉函式:ϕ(n) ,Carmichael 函式:λ(n);

c. 當 圖片圖片 時,

其中:圖片 。

對於任意 圖片,有如下性質:圖片

● 判定合數剩餘

a. 判定合數剩餘類問題是指 n=pq,其中:p、q 為大素數,任意給定圖片,使得圖片,則說 z 是模 圖片 的第 n 次剩餘;

b. 第 n 項剩餘的集合是 圖片 的一個圖片 階乘法子集;

c. 每個第 n 項剩餘 z 都正好擁有 n 個 n 階的根,其中只有一個是嚴格小於 n 的 (即 圖片 ;d. 第n項剩餘都可以寫成 圖片的形式。

● 正確性驗證

圖片

解密:

圖片

3.2 密文加法正確性

圖片

3.3 密文減法正確性

圖片

3.4 密文標量乘法正確性

圖片

4. 演算法實現

4.1 介面定義

●物件相關介面

○公/私鑰物件:PAILLIER_KEY ,該物件用來儲存 Paillier 公鑰和私鑰的基本資訊,比如 p、q、n、g、λ、μ 等資訊,私鑰儲存所有欄位,公鑰只儲存 n、g,其他欄位為空或者 0。相關介面如下:

// 建立 PAILLIER_KEY 物件
\PAILLIER_KEY *PAILLIER_KEY_new(void);

// 釋放 PAILLIER_KEY 物件
void PAILLIER_KEY_free(PAILLIER_KEY *key);

// 複製 PAILLIER_KEY 物件,將 src 複製到 dest 中
PAILLIER_KEY *PAILLIER_KEY_copy(PAILLIER_KEY *dest, PAILLIER_KEY *src);

// 複製 PAILLIER_KEY 物件
PAILLIER_KEY *PAILLIER_KEY_dup(PAILLIER_KEY *key);

// 將 PAILLIER_KEY 物件引用計數加1,釋放 PAILLIER_KEY 物件時若引用計數不為0則不能釋放其記憶體
intPAILLIER_KEY_up_ref(PAILLIER_KEY *key);

// 生成 PAILLIER_KEY 物件中的引數,bits 為隨機大素數 p、q 的二進位制位長度
int PAILLIER_KEY_generate_key(PAILLIER_KEY *key, int bits);

// 獲取 key 的型別:公鑰 or 私鑰
// PAILLIER_KEY_TYPE_PUBLIC 為私鑰,PAILLIER_KEY_TYPE_PRIVATE 為私鑰
int PAILLIER_KEY_type(PAILLIER_KEY *key);

○上下文物件:PAILLIER_CTX,該物件用來儲存公私鑰物件以及一些其他內部用到的資訊,是 Paillier 演算法其他介面的第一個引數。相關介面如下:

// 建立 PAILLIER_CTX 物件,key 為 paillier 公鑰或者私鑰,threshold 為支援最大的數字閾值,加密場景可設定為 0,解密場景可使用預設值:
PAILLIER_MAX_THRESHOLDPAILLIER_CTX *PAILLIER_CTX_new(PAILLIER_KEY *key, int64_t threshold);

// 釋放 PAILLIER_CTX 物件
void PAILLIER_CTX_free(PAILLIER_CTX *ctx);

// 複製 PAILLIER_CTX 物件,將 src 複製到 dest 中
PAILLIER_CTX *PAILLIER_CTX_copy(PAILLIER_CTX *dest, PAILLIER_CTX *src);

// 複製 PAILLIER_CTX 物件
PAILLIER_CTX *PAILLIER_CTX_dup(PAILLIER_CTX *src);

○密文物件: PAILLIER_CIPHERTEXT ,該物件是用來儲存 Paillier 加密後的結果資訊,用到 PAILLIER_CIPHERTEXT 的地方,可呼叫如下介面:

// 建立 PAILLIER_CIPHERTEXT 物件
PAILLIER_CIPHERTEXT *PAILLIER_CIPHERTEXT_new(PAILLIER_CTX *ctx);

// 釋放 PAILLIER_CIPHERTEXT 物件
void PAILLIER_CIPHERTEXT_free(PAILLIER_CIPHERTEXT *ciphertext);

●加密/解密介面

// 加密,將明文 m 進行加密,結果儲存到 PAILLIER_CIPHERTEXT 物件指標 out 中
int PAILLIER_encrypt(PAILLIER_CTX *ctx, PAILLIER_CIPHERTEXT *out, int32_t m);

// 解密,將密文 c 進行解密,結果儲存到 int32_t 指標 out 中
int PAILLIER_decrypt(PAILLIER_CTX *ctx, int32_t *out, PAILLIER_CIPHERTEXT *c);

●密文加/減/標量乘運算介面

// 密文加,r = c1 + c2
int PAILLIER_add(PAILLIER_CTX *ctx, PAILLIER_CIPHERTEXT *r,
PAILLIER_CIPHERTEXT *c1, PAILLIER_CIPHERTEXT *c2);

// 密文標量加,r = c1 * m
int PAILLIER_add_plain(PAILLIER_CTX *ctx, PAILLIER_CIPHERTEXT *r,                       PAILLIER_CIPHERTEXT *c1, int32_t m);

// 密文減,r = c1 - c2
int PAILLIER_sub(PAILLIER_CTX *ctx, PAILLIER_CIPHERTEXT *r,
PAILLIER_CIPHERTEXT *c1, PAILLIER_CIPHERTEXT *c2);

// 密文標量乘,r = c * m
int PAILLIER_mul(PAILLIER_CTX *ctx, PAILLIER_CIPHERTEXT *r,
PAILLIER_CIPHERTEXT *c, int32_t m);

●編碼/解碼介面
同態加密涉及到多方參與,可能會需要網路傳輸,這就需要將密文物件 PAILLIER_CIPHERTEXT 編碼後才能傳遞給對方,對方也需要解碼得到 PAILLIER_CIPHERTEXT 物件後才能呼叫其他介面進行運算。

介面如下:

// 編碼,將密文 ciphertext 編碼後儲存到 out 指標中,out 指標的記憶體需要提前分配好;
// 如果 out 為 NULL,則返回編碼所需的記憶體大小;
// flag:標誌位,預留,暫時沒有用size_t PAILLIER_CIPHERTEXT_encode(PAILLIER_CTX *ctx, unsigned char *out, 
size_t size,
const PAILLIER_CIPHERTEXT *ciphertext,
int flag);
// 解碼,將長度為 size 的記憶體資料 in 解碼後儲存到密文物件 r 中
int PAILLIER_CIPHERTEXT_decode(PAILLIER_CTX *ctx, PAILLIER_CIPHERTEXT *r,                               unsigned char *in, size_t size);

以上所有介面詳細說明請參考 Paillier API 文件:https://www.yuque.com/tsdoc/api/slgr6f

4.2 核心實現

●Paillier Key

Paillier 不像 EC-ElGamal,EC-ElGamal 在 Tongsuo 裡面直接複用 EC_KEY 即可,Paillier Key 在 Tongsuo 裡面則需要實現一遍,主要功能有:公/私鑰的生成、PEM 格式儲存、公/私鑰解析和文字展示,詳情請查閱程式碼:

crypto/paillier/paillier_key.c、

crypto/paillier/paillier_asn1.c、

crypto/paillier/paillier_prn.c。

●Paillier 加解密、密文運算

Paillier 的加解密和密文運算演算法非常簡單,主要是大數的模冪運算,使用 Tongsuo 裡面的 BN 相關介面就可以,需要注意的是,負數的加密/解密用到模逆運算,不能直接按公式計算 圖片 ,這是因為 OpenSSL 的介面 BN_mod_exp 沒有關注指數 (上面公式的 m ) 是不是負數,如果是負數的話需要做一次模逆運算:圖片,這裡計算出  之後做一次模逆運算 ( BN_mod_inverse ) 再與相乘;解密的時候,需要確認是否檢查了閾值 PAILLIER_MAX_THRESHOLD ) ,超出則說明是負數,需要減去 n 才得到真正的結果。密文減法也需要用到模逆運算,透過密文減法的公式 () 得知, 需要進行模逆運算 BN_mod_inverse ) 再與 相乘。

詳情請查閱程式碼:

crypto/paillier/paillier_crypt.c

●Paillier 命令列
為了提高 Paillier 的易用性,Tongsuo 實現瞭如下 Paillier 子命令:


$ /opt/tongsuo-debug/bin/openssl paillier -help
Usage: paillier [action options] [input/output options] [arg1] [arg2]

General options:
-help         Display this summary
Action options:
-keygen       Generate a paillier private key
-pubgen       Generate a paillier public key
-key          Display/Parse a paillier private key
-pub          Display/Parse a paillier public key
-encrypt      Encrypt a number with the paillier public key, usage: -encrypt 99, 99 is an example number
-decrypt      Decrypt a ciphertext using the paillier private key, usage:-decrypt c1, c1 is an example ciphertext
-add          Paillier homomorphic addition: add two ciphertexts, usage: -add c1 c2, c1 and c2 are tow example ciphertexts, result: E(c1) + E(c2)
-add_plain    Paillier homomorphic addition: add a ciphertext to a plaintext, usage: -add_plain c1 99, c1 is an example ciphertext, 99 is an example number, result: E(c1) + 99
-sub          Paillier homomorphic subtraction: sub two ciphertexts, usage: -sub c1 c2, c1 and c2 are tow example ciphertexts, result: E(c1) - E(c2)
-mul          Paillier homomorphic scalar multiplication: multiply a ciphertext by a known plaintext, usage: -mul c1 99, c1 is an example ciphertext, 99 is an example number, result: E(c1) * 99

Input options:
-in val       Input file
-key_in val   Input is a paillier private key used to generate public key

Output options:
-out outfile  Output the paillier key to specified file
-noout        Don't print paillier key out
-text         Print the paillier key in text
-verbose      Verbose output

Parameters:
arg1          Argument for encryption/decryption, or the first argument of a homomorphic operation
arg2          The second argument of a homomorphic operation

主要命令有:

- keygen: 生成 Paillier 私鑰;

- pubgen: 用 Paillier 私鑰生成公鑰;

- key: 文字顯示 Paillier 私鑰;

- pub: 文字顯示 Paillier 公鑰;

- encrypt: 對數字進行加密,輸出 Paillier 加密的結果,需要透過引數 -key_in 引數指定 Paillier 公鑰檔案路徑,如果加密負數則需要將 - 用 _ 代替,因為 - 會被 OpenSSL 解析成預定義引數了 (下同)

- decrypt: 對 Paillier 密文進行解密,輸出解密結果,需要透過-key_in引數指定 Paillier 私鑰檔案路徑;

- add: 對兩個 Paillier 密文進行同態加法操作,輸出同態加法密文結果,需要透過引數 -key_in 引數指定 Paillier 公鑰檔案路徑;

- add_plain: 將 Paillier 密文和明文相加,輸出同態加法密文結果,需要透過引數 -key_in 引數指定 Paillier 公鑰檔案路徑;

- sub: 對兩個 Paillier 密文進行同態減法操作,輸出同態減法密文結果,需要透過引數 -key_in 引數指定 Paillier 公鑰檔案路徑;

- mul: 將 Paillier 密文和明文相乘,輸出同態標量乘法密文結果,需要透過引數 -key_in 引數指定 Paillier 公鑰檔案路徑。

透過以上命令即可在命令列進行 Paillier 演算法實驗,降低入門門檻,詳情請查閱程式碼:apps/paillier.c。

另外還實現了 Paillier 的 speed 命令,可以進行效能測試,詳情請查閱程式碼:apps/speed.c。

5. 用法&例子

5.1 demo 程式

#include <stdio.h>
#include <time.h>
#include <openssl/paillier.h>
#include <openssl/pem.h>

#define CLOCKS_PER_MSEC (CLOCKS_PER_SEC/1000)

int main(int argc, char *argv[])
{
int ret = -1;
int32_t r; 
clock_t begin, end;
PAILLIER_KEY *pail_key = NULL, *pail_pub = NULL;
PAILLIER_CTX *ctx1 = NULL, *ctx2 = NULL;
PAILLIER_CIPHERTEXT *c1 = NULL, *c2 = NULL, *c3 = NULL;
FILE *pk_file = fopen("pail-pub.pem", "rb");
FILE *sk_file = fopen("pail-key.pem", "rb");
    if ((pail_pub = PEM_read_PAILLIER_PublicKey(pk_file, NULL, NULL, NULL)) == NULL)    
    goto err;
    if ((pail_key = PEM_read_PAILLIER_PrivateKey(sk_file, NULL, NULL, NULL)) == NULL)      
    goto err;
    
    if ((ctx1 = PAILLIER_CTX_new(pail_pub, PAILLIER_MAX_THRESHOLD)) == NULL) 
    goto err;    
    if ((ctx2 = PAILLIER_CTX_new(pail_key, PAILLIER_MAX_THRESHOLD)) == NULL)   
    goto err;
    
    if ((c1 = PAILLIER_CIPHERTEXT_new(ctx1)) == NULL)
    goto err;
    if ((c2 = PAILLIER_CIPHERTEXT_new(ctx1)) == NULL)
    goto err;
    
    begin = clock();
    if (!PAILLIER_encrypt(ctx1, c1, 20000021))
    goto err;
    end = clock(); 
    printf("PAILLIER_encrypt(20000021) cost: %lfms\n", (double)(end - begin)/CLOCKS_PER_MSEC);
    
    begin = clock();
    if (!PAILLIER_encrypt(ctx1, c2, 500)) 
    goto err;    end = clock();
    printf("PAILLIER_encrypt(500) cost: %lfms\n", (double)(end - begin)/CLOCKS_PER_MSEC);
    
    if ((c3 = PAILLIER_CIPHERTEXT_new(ctx1)) == NULL) 
    goto err;
    begin = clock();
    if (!PAILLIER_add(ctx1, c3, c1, c2))
    goto err;    end = clock(); 
    printf("PAILLIER_add(C2000021,C500) cost: %lfms\n", (double)(end - begin)/CLOCKS_PER_MSEC);
    
    begin = clock();
    if (!(PAILLIER_decrypt(ctx2, &r, c3)))
    goto err;    end = clock();
    printf("PAILLIER_decrypt(C20000021,C500) result: %d, cost: %lfms\n", r, (double)(end - begin)/CLOCKS_PER_MSEC);
    
    begin = clock();
    if (!PAILLIER_mul(ctx1, c3, c2, 800))
    goto err; 
    end = clock();
    printf("PAILLIER_mul(C500,800) cost: %lfms\n", (double)(end - begin)/CLOCKS_PER_MSEC);
    
    begin = clock(); 
    if (!(PAILLIER_decrypt(ctx2, &r, c3)))
    goto err;    end = clock();
    printf("PAILLIER_decrypt(C500,800) result: %d, cost: %lfms\n", r, (double)(end - begin)/CLOCKS_PER_MSEC);

    printf("PAILLIER_CIPHERTEXT_encode size: %zu\n", PAILLIER_CIPHERTEXT_encode(ctx2, NULL, 0, NULL, 1)); 
    ret = 0;
    err:    PAILLIER_KEY_free(pail_key);
    PAILLIER_KEY_free(pail_pub);
    PAILLIER_CIPHERTEXT_free(c1);
    PAILLIER_CIPHERTEXT_free(c2);
    PAILLIER_CIPHERTEXT_free(c3);
    PAILLIER_CTX_free(ctx1);
    PAILLIER_CTX_free(ctx2);
    fclose(sk_file);
    fclose(pk_file);
    return ret;
}

5.2 編譯和執行

先確保 Tongsuo 開啟 Paillier,如果是手工編譯 Tongsuo,可參考如下編譯步驟:

# 下載程式碼
git clone git@github.com:Tongsuo-Project/Tongsuo.git

# 編譯引數需要加上:enable-paillier
./config  --debug no-shared no-threads enable-paillier --strict-warnings -fPIC --prefix=/opt/tongsuo

# 編譯
make -j

# 安裝到目錄
/opt/tongsuo sudo make install

5.3 編譯 demo 程式

gcc -Wall -g -o paillier_test ./paillier_test.c -I/opt/tongsuo/include -L/opt/tongsuo/lib -lssl -lcrypto

5.4 生成 Paillier 公私鑰

# 先生成私鑰
/opt/tongsuo/bin/openssl paillier -keygen -out pail-key.pem# 
用私鑰生成公鑰
/opt/tongsuo/bin/openssl paillier -pubgen -key_in ./pail-key.pem -out pail-pub.pem

5.5 執行結果

$ ./paillier_test
PAILLIER_encrypt(20000021) cost: 3.202000ms
PAILLIER_encrypt(500) cost: 0.442000ms
PAILLIER_add(C2000021,C500) cost: 0.047000ms
PAILLIER_decrypt(C20000021,C500) result: 20000521, cost: 0.471000ms
PAILLIER_mul(C500,800) cost: 0.056000ms
PAILLIER_decrypt(C500,800) result: 400000, cost: 0.464000ms
PAILLIER_CIPHERTEXT_encode size: 0

本週推薦閱讀

你好,我的新名字叫“銅鎖/Tongsuo”

BabaSSL:支援半同態加密演算法 EC-ElGamal

BabaSSL 釋出 8.3.0|實現相應隱私計算的需求

開源專案文件社群化!Tongsuo/銅鎖實踐

相關文章