202509
security

Python 中 CKKS 方案的实际实现:噪声管理和密钥切换

面向初学者,给出 CKKS 方案在 Python 中的步步实现,应对噪声管理和密钥切换的工程挑战。

全同态加密(Fully Homomorphic Encryption, FHE)允许在加密数据上直接进行计算,而无需解密,这在隐私保护计算中至关重要。其中,CKKS 方案特别适用于处理实数或复数的近似计算,支持浮点数运算,广泛应用于机器学习和数据分析等场景。本文针对初学者,提供 CKKS 在 Python 中的逐步实现指南,重点探讨噪声管理和密钥切换这些实际挑战。通过 Microsoft SEAL 库的 Python 封装 TenSEAL,我们可以轻松上手,避免底层 C++ 的复杂性。

CKKS 方案概述

CKKS(Cheon-Kim-Kim-Song)方案由 2017 年提出,基于环学习带错误问题(Ring-LWE),其核心创新是将噪声视为近似计算误差。通过编码机制,将复数向量映射到多项式环上,支持加法、乘法和旋转等操作。但每次同态乘法会引入噪声增长,如果不管理好,将导致解密失败。

观点:噪声不是 bug,而是特征;通过参数调优和重缩放(Rescale),可以平衡计算深度与精度。证据显示,在典型机器学习任务中,CKKS 可支持 5-10 次乘法而不显著丢失精度(参考 TenSEAL 基准)。实际落地时,选择合适的多项式度数 N 和缩放因子 Δ 是关键。

环境准备与参数选择

首先,安装 TenSEAL:pip install tenseal。TenSEAL 封装了 SEAL 库,提供 CKKS 支持。

关键参数:

  • 多项式模度数(poly_modulus_degree, N):如 8192,支持 N/2 = 4096 个槽位。N 越大,支持更多并行计算,但计算开销增加。初学者推荐 4096 或 8192。
  • 系数模位大小(coeff_mod_bit_sizes):模数链,如 [60, 40, 40, 60],用于多级计算。第一个是初始模 q0,后续用于重缩放。
  • 全局缩放因子(global_scale):如 2^40,确保初始噪声远小于消息。缩放下限约 2^20,以保留精度。
  • 乘法深度(mult_depth):间接由模链决定,通常 3-5。过多会耗尽“噪声预算”。

风险:参数不当可能导致 128 位安全级别下降,或运行时错误。监控噪声预算:TenSEAL 的 approx_noise_budget() 方法可检查剩余预算,低于 20 位时需重缩放或自举。

步步实现:基本加密与解密

  1. 创建上下文

    import tenseal as ts
    import numpy as np
    
    context = ts.context(
        ts.SCHEME_TYPE.CKKS,
        poly_modulus_degree=8192,
        coeff_mod_bit_sizes=[60, 40, 40, 40, 60]
    )
    context.global_scale = 2**40
    context.generate_galois_keys()  # 用于旋转
    

    这里,Galois 键支持向量旋转,如神经网络中的卷积。

  2. 生成密钥

    secret_key = ts.generate_private_key(context)
    public_key = ts.generate_public_key(secret_key, context)
    context.make_context_public(public_key)  # 如果需公钥加密
    

    私钥用于解密,公钥可选。

  3. 编码与加密: CKKS 支持实数向量编码。

    data = np.array([1.0, 2.0, 3.0, 4.0])  # 实数向量,自动扩展为复数
    plain = ts.ckks_vector(context, data)
    enc_vector = ts.ckks_vector(context, plain)  # 或直接 ts.ckks_vector(context, data)
    

    编码使用规范嵌入(Canonical Embedding),将向量映射到多项式系数。

  4. 基本操作与解密

    enc2 = ts.ckks_vector(context, np.array([5.0, 6.0, 7.0, 8.0]))
    result = enc_vector + enc2  # 加法
    result = enc_vector * 2.0  # 标量乘
    decrypted = result.decrypt()  # 使用私钥解密
    decoded = decrypted.tolist()  # 解码回向量
    print(decoded)  # 近似 [6.0, 8.0, 10.0, 12.0]
    

    加法噪声增长慢,乘法需注意。

噪声管理:挑战与解决方案

噪声是 CKKS 的核心挑战:加密引入初始噪声 e,同态操作累积,导致解密时消息 μ + e' ≈ μ,但 e' 过大则精度丢失。

观点:通过重缩放控制噪声,将其视为浮点截断。证据:在 TenSEAL 中,每次乘法后调用 result.rescale_to(global_scale),模拟模切换,丢弃低位噪声。

可落地参数/清单:

  • 监控噪声:使用 enc.approx_noise_budget(),初始约 60 位。每乘法耗 20-30 位。
  • 重缩放阈值:当预算 < 30 位时,立即 rescale。参数:scale = 2^40,精度损失 ≈ log2(1/scale) 位。
  • 自举(Bootstrap):高级,若深度耗尽,解密-重加密(TenSEAL 未原生支持,需自定义)。初学者避免,限制深度 ≤5。
  • 精度测试清单
    1. 加密小向量,多次乘法后检查 ||decoded - expected|| < 1e-5。
    2. 调整 scale:太大噪声淹没消息,太小精度不足。
    3. 模链长度:每级 1 次乘法,4 级支持 3 次乘。

示例:乘法后 rescale

prod = enc_vector * enc2
prod.rescale_to(2**40)  # 控制噪声

若不 rescale,多次乘后预算为 0,解密失败。

密钥切换:重线性化机制

乘法后,密文度从 2 升至 3(c0 + c1 sk + c2 sk^2),需降回 2 度,使用重线性化键(relin_keys)进行密钥切换。

观点:密钥切换确保密文紧凑,避免指数增长。证据:SEAL/TenSEAL 中,relin_keys 基于私钥生成,切换时替换 sk^2 项为公钥形式。

步步实现:

  1. 生成 relin 键

    relin_keys = ts.generate_relin_keys(secret_key, context)
    context.relin_keys = relin_keys
    
  2. 乘法与切换

    prod = enc_vector * enc2  # 产生 3 度密文
    prod.relinearize()  # 自动使用 relin_keys 降度
    

    无 relinearize,多次乘后内存爆炸。

挑战:生成 relin_keys 耗时(O(N^2 log q)),存储大。解决方案:预生成,仅支持必要深度(如 mult_depth=3 时生成相应键)。监控:prod.degree 应始终 ≤2。

清单:

    1. 上下文初始化后立即生成 relin_keys。
    1. 每乘法后调用 relinearize。
    1. 测试:多层乘法后,检查度数和性能(切换增加 20% 开销)。
    1. 优化:用更小的 N 测试,逐步 scale up。

高级应用与回滚策略

结合噪声与切换,实现线性回归等:编码特征向量,加密后同态矩阵乘(需旋转支持 Galois 键)。

回滚策略:

  • 若噪声超阈值,fallback 到部分同态或解密重加密。
  • 错误处理:捕获 EncryptionException,调整参数重试。
  • 性能:N=8192 上,单乘 ≈1ms(CPU),批量优化并行槽。

通过以上步骤,初学者可快速构建 CKKS 原型。实际项目中,结合 TFHE(如 Concrete-ML)扩展自举。未来,CKKS 将推动隐私 ML 落地,但需警惕量子攻击(后量子安全参数)。

(字数:约 1050)