【準同型暗号】ついにキメラが実装！！OpenFHEの1.1系で追加された格子暗号の最先端を解説！（暗号形式の動的変換）

## 概要

今回は、**準同型暗号（暗号化したままデータを計算できる暗号形式）**

の中でも注目されている「格子暗号」のオープンソースライブラリ

#### OpenFHE

https://github.com/openfheorg/openfhe-development

の**最新バージョン v1.1 系**
でできる様になった機能について解説します。

また、実際に動かすことのできるサンプルコードも紹介します。


## 忙しい人のためのまとめ


### 新機能

OpenFHEのv1.1を使用すると、次の様な機能を使えるよ

①**閾値型のCKKS形式の暗号が使えるよ**
②**CKKS形式からFHEW（TFHE）形式に暗号状態のまま変換、その逆変換もできるよ**


この記事ではそのうち、

**暗号形式の動的変換**

について解説するよ

### 何が美味しいの？

#### 動的変換

格子暗号形式の動的変換が行えると、

- CKKS形式
- TFHE形式

のいいとこどりが可能になって嬉しいよ



## 解説

それでは、実際にOpenFHE1.1系で追加された機能について解説していきます。


### 暗号形式の変換

さて、追加された機能のうち
この　**「暗号形式の変換」**　ですが、これはある意味待ち望まれていた機能です。

#### 始祖論文

もともとこの「変換」について提唱した論文は

https://eprint.iacr.org/2018/758

この論文でした。
タイトルでは暗号形式の「統一」と書かれていますが、

**「統一」を実現すると、各形式を「変換」をすることが可能になります。**


#### 暗号形式の変換とは

格子暗号にはいくつかの形式が存在し、今現在よく使われているものを挙げると

- BFV
- BGV
- CKKS
- TFHE

が有名です。その中でも、

- **CKKS**
- **TFHE**

形式が非常によく使われています。
CKKS形式とTFHE形式の準同型暗号についての比較は、

https://qiita.com/kenmaro/items/c6a97402848aa3fb2954

をお読みいただければと思います。

OpenFHEに実装された変換は、まさにこの
CKKS形式とTFHE形式（FHEW形式）の間の変換関数たちです。



#### 変換をすると何がいいのか

CKKS形式は線形演算が得意である一方で、非線形演算や比較演算は苦手でしたし、
TFHE形式は非線形演算や比較演算も可能なものの、線形演算などもすべてやろうとすると遅すぎてちょっと、、
という状況でした。

動的変換とは、暗号状態のままこの形式を変換させる技術のことであり、
これによりCKKS形式で演算したい場面ではCKKS形式を使用し、
CKKS形式では演算が難しい処理に対してはまず暗号形式を「変換」し、
TFHE形式にしてから演算を実行することが可能になります。

要はいいとこ取りが可能になります。



## 実装

今回は、OpenFHEで実装されている暗号形式変換のExampleの

https://github.com/openfheorg/openfhe-development/blob/main/src/pke/examples/scheme-switching.cpp

の関数を参考にして、

1. 二つのベクトルをCKKS形式で暗号化して内積を計算
2. 内積結果をFHEW形式に変換
3. ReLU関数を計算
4. FHEW形式からCKKS形式に戻す
5. 復号して結果を確認

というチュートリアルを作ってみました。

注意）パラメータはExampleが使っているセキュリティが標準的ではないものをデバッグ用に使っています。
実際のセキュリティを担保した形でのパラメータはまだ試せていないので注意してください。


```example.cpp
void MultThenReLUViaSchemeSwitching() {
    std::cout << "\n-----MultThenReLUViaSchemeSwitching-----\n" << std::endl;
    std::cout << "Output precision is only wrt the operations in CKKS after switching back.\n" << std::endl;

    // Setup CryptoContext for CKKS ==============================================
    ScalingTechnique scTech = FIXEDAUTO;
    uint32_t multDepth      = 17;
    if (scTech == FLEXIBLEAUTOEXT)
        multDepth += 1;

    uint32_t scaleModSize = 50;
    uint32_t firstModSize = 60;
    uint32_t ringDim      = 8192;
    SecurityLevel sl      = HEStd_NotSet;
    BINFHE_PARAMSET slBin = TOY;
    uint32_t logQ_ccLWE   = 25;
    uint32_t slots        = 16;  // sparsely-packed
    uint32_t batchSize    = slots;

    CCParams<CryptoContextCKKSRNS> parameters;
    parameters.SetMultiplicativeDepth(multDepth);
    parameters.SetScalingModSize(scaleModSize);
    parameters.SetFirstModSize(firstModSize);
    parameters.SetScalingTechnique(scTech);
    parameters.SetSecurityLevel(sl);
    parameters.SetRingDim(ringDim);
    parameters.SetBatchSize(batchSize);
    parameters.SetSecretKeyDist(UNIFORM_TERNARY);
    parameters.SetKeySwitchTechnique(HYBRID);
    parameters.SetNumLargeDigits(3);

    CryptoContext<DCRTPoly> cc = GenCryptoContext(parameters);

    // Enable the features that you wish to use ==============================================
    cc->Enable(PKE);
    cc->Enable(KEYSWITCH);
    cc->Enable(LEVELEDSHE);
    cc->Enable(ADVANCEDSHE);
    cc->Enable(SCHEMESWITCH);

    std::cout << "CKKS scheme is using ring dimension " << cc->GetRingDimension();
    std::cout << ", number of slots " << slots << ", and suports a multiplicative depth of " << multDepth << std::endl
              << std::endl;

    // Generate encryption keys  ==============================================
    auto keys = cc->KeyGen();
    cc->EvalSumKeyGen(keys.secretKey);

    // Prepare the FHEW cryptocontext and keys for FHEW and scheme switching
    auto FHEWparams = cc->EvalSchemeSwitchingSetup(sl, slBin, false, logQ_ccLWE, false, slots);

    auto ccLWE          = FHEWparams.first;
    auto privateKeyFHEW = FHEWparams.second;
    ccLWE.BTKeyGen(privateKeyFHEW);

    cc->EvalSchemeSwitchingKeyGen(keys, privateKeyFHEW);

    std::cout << "FHEW scheme is using lattice parameter " << ccLWE.GetParams()->GetLWEParams()->Getn();
    std::cout << ", logQ " << logQ_ccLWE;
    std::cout << ", and modulus q " << ccLWE.GetParams()->GetLWEParams()->Getq() << std::endl << std::endl;

    // Set the scaling factor to be able to decrypt; the LWE mod switch is performed on the ciphertext at the last level
    // auto pLWE1       = ccLWE.GetMaxPlaintextSpace().ConvertToInt();  // Small precision
    auto modulus_LWE = 1 << logQ_ccLWE;
    auto beta        = ccLWE.GetBeta().ConvertToInt();
    auto pLWE2       = modulus_LWE / (2 * beta);  // Large precision

    double scaleSignFHEW    = 1.0;
    const auto cryptoParams = std::dynamic_pointer_cast<CryptoParametersCKKSRNS>(cc->GetCryptoParameters());
    uint32_t init_level     = 0;
    if (cryptoParams->GetScalingTechnique() == FLEXIBLEAUTOEXT)
        init_level = 1;
    cc->EvalCompareSwitchPrecompute(pLWE2, init_level, scaleSignFHEW);

    // Encoding and encryption of inputs  ==============================================
    // Inputs
    std::vector<double> x1 = {4.0, -3.0};
    std::vector<double> w1;
    for (int i = 0; i < 16; i++) {
        if (i == 0 || i == 1) {
            w1.emplace_back(1.0);
        }
        else {
            w1.emplace_back(0.0);
        }
    }
    std::vector<double> unit0;
    for (int i = 0; i < 16; i++) {
        unit0.emplace_back(0.0);
    }
    unit0[0] = 1.0;

    std::vector<double> x2(slots, 0.0);
    std::vector<double> x3(slots, 1.0);

    // Encoding as plaintexts  ==============================================
    Plaintext ptxt1   = cc->MakeCKKSPackedPlaintext(x1, 1, 0, nullptr, slots);
    Plaintext ptxt2   = cc->MakeCKKSPackedPlaintext(x2, 1, 0, nullptr, slots);
    Plaintext pw1     = cc->MakeCKKSPackedPlaintext(w1, 1, 0, nullptr, slots);
    Plaintext p_unit0 = cc->MakeCKKSPackedPlaintext(unit0, 1, 0, nullptr, slots);

    // Encrypt the encoded vectors  ==============================================
    auto c1      = cc->Encrypt(keys.publicKey, ptxt1);
    auto c2      = cc->Encrypt(keys.publicKey, ptxt2);
    auto c_w1    = cc->Encrypt(keys.publicKey, pw1);
    auto c_unit0 = cc->Encrypt(keys.publicKey, p_unit0);

    // Calculate Inner Product of c1 and c_w1  ==============================================
    auto c_mult = cc->EvalInnerProduct(c1, c_w1, batchSize);
    c_mult      = cc->EvalMultAndRelinearize(c_mult, c_unit0);

    // CKKS to FHEW switching and sign evaluation to test correctness  ==============================================
    Plaintext p1;
    cc->Decrypt(keys.secretKey, c_mult, &p1);
    p1->SetLength(slots);
    std::cout << "c_mult: ";
    for (uint32_t i = 0; i < slots; ++i) {
        //std::cout << p1->GetRealPackedValue()[i] << " ";
        printf("%f ", p1->GetRealPackedValue()[i]);
    }
    printf("\n");



    // Operate ReLU function  ==============================================
    Plaintext ptmp;

    auto cResult = cc->EvalCompareSchemeSwitching(c_mult, c2, slots, slots);
    cc->Decrypt(keys.secretKey, cResult, &ptmp);
    std::cout << "cResult of compare: ";
    for (uint32_t i = 0; i < slots; ++i) {
        printf("%f ", ptmp->GetRealPackedValue()[i]);
    }
    printf("\n");
    cc->EvalSubInPlace(1.0, cResult);
    cResult = cc->EvalMultAndRelinearize(cResult, c_mult);


    // Decrypt and check the result  ==============================================
    cc->Decrypt(keys.secretKey, cResult, &ptmp);
    ptmp->SetLength(slots);
    std::cout << "cRes: ";
    for (uint32_t i = 0; i < slots; ++i) {
        printf("%f ", ptmp->GetRealPackedValue()[i]);
    }
    printf("\n");

}
```

### やっていることの言葉での説明

プログラム自体は長いですが、やっていることはシンプルです。

```example.cpp
    std::vector<double> x1    = {4.0, -3.0};
    std::vector<double> w1    = {1.0, 1.0};

    std::vector<double> unit0 = {1.0, 0.0};
    std::vector<double> x2(slots, 0.0);
    std::vector<double> x3(slots, 1.0);
```

`x1` と`w1`の内積を計算し、
結果をReLU関数に通しています。

`unit0`, `x2`, `x3` はReLU関数を計算する時に、用意されている
`EvalCompareSchemeSwitching`関数を使ってReLUを表現するために使用しているだけです。


実行結果は

```
cRes: 0.999999 -0.000000 0.000000
```

となり、確かに内積結果をReLU関数に通したものになっています。
1.0ではなく0.9999..　となるのはCKKS形式特有の誤差などによるものです。


## まとめ


今回はOpenFHEの1.1系に追加された、
CKKSとFHEW（TFHE）形式の暗号状態での形式変換について解説してみました。
また、彼らのExmapleを応用した、機械学習の計算を意識した、線形計算 --> ReLU関数の評価
という一連の流れを暗号状態で実行するプログラムを作ってみました。

パラメータの選定や、それによってどのくらい実行時間が遅くなるかは評価すべき点ですが、
今までできなかったことができる様になったことはすごいことだと思います。

機能面で言うとOpenFHEが一つ抜けている状況になったかと思いますが、
他にもライブラリはあり、それらも研究開発が活発に行われているため、きちんとフォローしていくことが必要だと思います。

興味があったらぜひ動かしてみてください。


今回はこの辺で。


@kenmaro