6_serialization

在本例中,展示了SEAL的serialization操作。同时提出了一些重要的概念,使用户在外包计算通信密文和密钥时能够优化数据大小。与前面的示例不同,为了最大限度地清晰,我们将此示例组织为客户-服务器类型。服务器选择加密参数,客户端生成密钥,服务器进行加密计算,客户端解密。

本例要求ZLIB或Zstandard支持

1
2
3
4
5
#if (!defined(SEAL_USE_ZSTD) && !defined(SEAL_USE_ZLIB))
cout << "Neither ZLIB nor Zstandard support is enabled; this example is not available." << endl;
cout << endl;
return;
#else

类Serializzable<T>是一个包装器类,可以包装任意可序列化的类,包括:

  • 加密参数
  • 模数
  • 明文和密文
  • 私钥、公钥、重新线性化密钥和Galois密钥

Serializzable<T>通过将调用转到类型T的包装对象的对应函数,来提供序列化包装对象所需的最小函数功能。这需要SEAL中的对象包含两部分,其中一部分是独立于另一部分的伪随机数据。在实际使用之前,伪随机部分可以作为种子存储。我们将调用具有属性"seedable"的对象。

例如,Galois密钥的大小通常很大,但实际上一半的数据都是伪随机的,可以作为种子存储。因为Galois密钥的生成方不会使用它,在反序列化时扩展种子是有意义的。另一方面,我们不能允许用户使用未扩展的Galois密钥,这是通过将其包装为Serializable<GaloisKeys>并只能够被序列化来保证的。

只用一些SEAL对象是具有"seedable"的。包括:

  • 公钥、重新线性化密钥和Galois密钥
  • 私钥模式下的密文(Encryptor::encrypt_symmetric或Encryptor::encrypt_zero_symmetric)

公钥模式下的密文是不具有"seedable"的。因此当公钥不必需时,使用私钥模式的SEAL可能更有好处。

输出Serializable<T>的函数:

  • Encryptor::encrypt(和variants)输出Serializable<Ciphertext>
  • KeyGenerator::create_...输出不同密钥类型的Serializable<T>

注意Encryptor::encrypt包含在上述列表中,它在公钥模式下生成的密文也不具有"seedable"。这是为了保证API中公钥和私钥加密的一致性。输出Serializable<T>对象的函数也有以类型T的普通对象作为目标参数的重载。这些重载。这些重载对于不需要序列化且需要在构造点使用对象的本地测试来说很方便。这样的对象不能再被转换回种子状态。

为了模拟客户-服务器交互,我们设置了一个共享的c++流。在实际的用例中,这可以是网络缓冲区、文件流或任何共享资源。

需要注意的是,SEAL序列化的所有数据都是二进制格式的,因此将数据打印为ASCII字符是没有意义的。像Base64这样的编码会增加数据的大小,这已经是同态加密的瓶颈。因此,不支持或建议将序列化为文本。

文件流序列化需要ios::binary标志来表示被序列化的数据是二进制数据而不是文本。例如,一个适当的输出文件流可以设置为:ofstream ofs("filename",ios::binary);

在本例中,使用std::stringstream,不需要ios::binary标志。注意std::stringstream的默认构造使用ios::in|ios::out打开流,因此读或写都是支持的。

1
2
3
stringstream parms_stream;
stringstream data_stream;
stringstream sk_stream;

服务器首先决定计算并设置解密参数。

1
2
3
4
EncryptionParameters parms(scheme_type::ckks);
size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, { 50, 20, 50 }));

使用EncryptionParameters::save函数对共享流的加密参数的序列化是非常简单的。

1
auto size = parms.save(parms_stream);

该函数的返回值是写入流中数据的实际字节数。

1
2
print_line(__LINE__);
cout << "EncryptionParameters: wrote " << size << " bytes" << endl;

在序列化时,可以通过提供EncryptionParameters::save以所需的压缩模式启用或禁用压缩,如下示例所示:

1
2
3
auto size = parms.save(shared_stream, compr_mode_type::none);
auto size = parms.save(shared_stream, compr_mode_type::zlib);
auto size = parms.save(shared_stream, compr_mode_type::zstd);

如果SEAL是由Zstandard或ZLIB支持下压缩的,默认使用其中之一。如果可行,Zstandard更优于ZLIB。

压缩可以对序列化的数据大小产生重大影响,因为密文和关键数据由许多对模coeff_modulus素数的均匀随机整数组成。当使用CKKS时,coeff_modulus中的素数与用于在内部存储密文和关键数据的64位字相比,可以相对较小。序列化项目标缓冲区或流中写入64位字,高字节可能留下许多0字节。去掉这些0的一种方便的方法是对加密的数据应用通用压缩算法。当使用带有小素数的CKKS时,压缩率可以是显著的(高达50-60%)。

也可以直接将数据序列化到缓冲区中。为此,需要知道所需缓冲区大小的上限,可以使用EncryptionParameters::save_size函数获取。此函数也接受所需的压缩模式,或者使用默认选项。

EncryptionParameters::save_size的输出如下:

  • compr_mode_type::none所需的精确缓冲区大小
  • compr_mode_type::zlib或compr_mode_type::zstd所需大小的上限

从输出中可以看到,这些函数返回的大小要比一开始写入共享流中的压缩数据大小更大。压缩在数据大小上产生了显著的改善,但是,不可能提前知道压缩数据的确切大小。如果不使用压缩,则大小完全由加密参数决定。

1
2
3
4
5
6
print_line(__LINE__);
cout << "EncryptionParameters: data size upper bound (compr_mode_type::none): "
<< parms.save_size(compr_mode_type::none) << endl;
cout << " "
<< "EncryptionParameters: data size upper bound (compression): "
<< parms.save_size(/* Serialization::compr_mode_default */) << endl;

将加密参数序列化到一个固定大小的缓冲区

1
2
vector<seal_byte> byte_buffer(static_cast<size_t>(parms.save_size()));
parms.save(reinterpret_cast<seal_byte *>(byte_buffer.data()), byte_buffer.size());

为了说明反序列化,将加密参数从缓冲区加载回EncryptionParameters的另一个实例中。注意这种情况下,EncryptionParameters::load要求缓冲区大小比压缩参数的实际数据大小更大。序列化格式包括数据的真实大小,缓冲区的大小仅用于健全性检查。

1
2
EncryptionParameters parms2;
parms2.load(reinterpret_cast<const seal_byte *>(byte_buffer.data()), byte_buffer.size());

我们可以检查保存和加载的加密参数是否确实匹配。

1
2
print_line(__LINE__);
cout << "EncryptionParameters: parms == parms2: " << boolalpha << (parms == parms2) << endl;

这里提供和使用的函数适用于所有具有序列化意义的SEAL对象。但是重要的是要了解更高级的技术,这些技术可用于进一步压缩数据大小。下面将介绍这些技术。

客户加载加密参数,设置SEALContext并创建所需的密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EncryptionParameters parms;
parms.load(parms_stream);

//我们将使用相同的流重复读取参数
parms_stream.seekg(0, parms_stream.beg);

SEALContext context(parms);

KeyGenerator keygen(context);
auto sk = keygen.secret_key();
PublicKey pk;
keygen.create_public_key(pk);

//我们需要保存私钥来后续解密
sk.save(sk_stream);

//正如本例开始所讨论的,密文可以在私钥模式下以种子状态创建,这大大减少了序列化后的数据大小。为此,我们需要在Encryptor的构造函数中为其提供私钥,或者稍后使用Encryptor::set_secret_key函数,并使用Encryptor::encrypt_symmetric函数进行加密。
encryptor.set_secret_key(sk);
auto size_sym_encrypted2 = encryptor.encrypt_symmetric(plain2).save(data_stream);

大小大幅约简

1
2
3
4
print_line(__LINE__);
cout << "Serializable<Ciphertext> (public-key): wrote " << size_encrypted1 << " bytes" << endl;
cout << " "
<< "Serializable<Ciphertext> (seeded secret-key): wrote " << size_sym_encrypted2 << " bytes" << endl;

服务器在加密数据上进行计算。重新创建SEALContext并设置评估器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    EncryptionParameters parms;
parms.load(parms_stream);
parms_stream.seekg(0, parms_stream.beg);
SEALContext context(parms);

Evaluator evaluator(context);

//从data_stream中载入重新线性化密钥和密文
RelinKeys rlk;
Ciphertext encrypted1, encrypted2;

//反序列化
rlk.load(context, data_stream);
encrypted1.load(context, data_stream);
encrypted2.load(context, data_stream);

//计算乘积、rescale和重新线性化
Ciphertext encrypted_prod;
evaluator.multiply(encrypted1, encrypted2, encrypted_prod);
evaluator.relinearize_inplace(encrypted_prod, rlk);
evaluator.rescale_to_next_inplace(encrypted_prod);

//使用data_stream将encrypted_prod发送回客户端。encrypted_prod无法存储为种子对象:只有新加密的私钥密文可以作为种子。注意结果密文的大小比新加密密文的大小更小,因为rescale操作使得它为欸与更低level
data_stream.seekp(0, parms_stream.beg);
data_stream.seekg(0, parms_stream.beg);
auto size_encrypted_prod = encrypted_prod.save(data_stream);

print_line(__LINE__);
cout << "Ciphertext (secret-key): wrote " << size_encrypted_prod << " bytes" << endl;
}

最后客户解密结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
EncryptionParameters parms;
parms.load(parms_stream);
parms_stream.seekg(0, parms_stream.beg);
SEALContext context(parms);

//从sk_stream载入私钥
SecretKey sk;
sk.load(context, sk_stream);
Decryptor decryptor(context, sk);
CKKSEncoder encoder(context);

Ciphertext encrypted_result;
encrypted_result.load(context, data_stream);

Plaintext plain_result;
decryptor.decrypt(encrypted_result, plain_result);
vector<double> result;
encoder.decode(plain_result, result);

print_line(__LINE__);
cout << "Result: " << endl;
print_vector(result, 3, 7);

序列化数据通常以16字节SEALHeader构造开头,定义位于 native/src/seal/serialization.h,然后是对象的压缩数据。

SEALHeader包含如下数据:

1
2
3
4
5
6
7
[offset 0] 2-byte magic number 0xA15E (Serialization::seal_magic)
[offset 2] 1-byte indicating the header size in bytes (always 16)
[offset 3] 1-byte indicating the Microsoft SEAL major version number
[offset 4] 1-byte indicating the Microsoft SEAL minor version number
[offset 5] 1-byte indicating the compression mode type
[offset 6] 2-byte reserved field (unused)
[offset 8] 8-byte size in bytes of the serialized data, including the header

作为示例,我们将演示通过保存明文创建的SEALHeader。注意,SEALHeader从未被压缩过,因此不需要指定压缩模式。

1
2
3
Plaintext pt("1x^2 + 3");
stringstream stream;
auto data_size = pt.save(stream);

从流中载入SEALHeader

1
2
Serialization::SEALHeader header;
Serialization::LoadHeader(stream, header);

写入流中的数据大小与SEALHeader中指定的大小一致

1
2
3
4
5
6
    print_line(__LINE__);
cout << "Size written to stream: " << data_size << " bytes" << endl;
cout << " "
<< "Size indicated in SEALHeader: " << header.size << " bytes" << endl;
cout << endl;
#endif


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!