资讯动态

FPGA上的后量子密码ML-KEM实现:NTT与Keccak硬件加速全记录

发布时间:2026/10/16 11:57:14 来源:尧图企业网站定制
半年前有客户问我公司手里那把用于长期数据加密的证书密钥申请一次要用好几年要不要现在就把算法换成后量子加密PQC我当时没敢拍胸脯。回来后我花了几个星期把权威标准组织陆续公开的后量子密码学标准化文件翻了个遍把候选算法的参考实现跑了个遍最后干脆拿了块带ARM硬核的FPGA开发板把其中最典型的一个密钥封装算法从数学公式一路写到了RTL代码完整复现了一遍。这篇文章就是这次调研与复现项目的全程记录。文章覆盖三块内容一是后量子密码学目前的算法版图和选型逻辑二是如何在FPGA上把以ML-KEM为代表的核心计算单元跑起来三是硬件实现过程中那些软件工程师永远遇不到的坑。想入坑PQC、做密码硬件加速、或者打算把设备密钥体系升级到抗量子方案的读者这篇文章可以直接当参照系。1. 为什么是现在量子威胁倒计时与PQC调研起点1.1 Shor算法到底撕开了多大的口子要说PQC为什么突然从学术圈冲到工程圈绕不开Shor算法。1994年Shor提出用量子计算机在多项式时间内分解大整数之后RSA这类基于大整数分解困难性的公钥体系就在数学层面被判了死刑。椭圆曲线密码也好不到哪去因为椭圆曲线离散对数问题同样被Shor算法覆盖。注意我说的是数学层面不是现实层面——目前没人造出一台能稳定运行几千个逻辑量子比特的机器所以在座的RSA-2048短期内依然安全。但问题恰恰出在短期这个词上。公钥密码体系有一个经常被忽略的隐含假设秘密存续期。证书私钥、长寿命密钥、档案加密密钥很多数据需要保密十年、二十年甚至更长。如果今天有人把密文截获等十多年后量子计算机成熟后再解密那今天部署的系统就等于白部署了。这就是业界常说的现在收割以后解密harvest now, decrypt later。对有长期保密需求的数据来说威胁不是未来的而是当下的。调研时我反复跟团队强调别只盯着量子计算机哪年造出来要先看自己的数据哪年必须解密。1.2 标准化窗口已关闭工程迁移窗口才刚刚打开另一个更现实的推动力是标准化。权威标准组织在2024年正式发布了几份后量子密码标准FIPS 203ML-KEM密钥封装、FIPS 204ML-DSA数字签名、FIPS 205SLH-DSA无状态哈希签名随后又补了FN-DSA相关标准。标准的落地意味着算法设计竞赛阶段结束工程适配阶段开始。操作系统、浏览器、加密库已经在逐步加入混合模式——也就是经典算法和后量子算法同时使用、只有两者一起被攻破才失效。这个趋势非常明确越晚动手存量系统的迁移包袱就越重。所以我调研的第一个体会是PQC已经不是要不要做的问题而是什么时候做、从哪开始做的问题。真正留给工程师的是迁移顺序问题。先把哪些资产保护起来、哪些链路先换、怎么保证过渡期兼容这些问题比算法本身更影响落地节奏。1.3 调研范围怎么收敛算法多但真正要复现的只有一个PQC候选算法从最早的几十个收敛到现在的几个标准背后的数学基础分了好几派格、编码、多变量、哈希。最终进入标准的主要是格基和哈希两类。格基算法又分两路做密钥封装的ML-KEM以及做签名的ML-DSA和FN-DSA哈希签名SLH-DSA走的是完全不同的保守路线。调研时不要平均用力。我把火力集中在ML-KEM上理由有三第一它是TLS混合模式里预期最先大规模部署的组件第二它的模块格结构在硬件上非常规整适合FPGA实现第三它涉及到的NTT数论变换和Keccak恰好是FPGA实现里最有含金量的两个计算单元。把这两个单元啃下来后面再看其他格基算法都会轻松很多。2. 算法版图梳理密钥封装三兄弟与一套曲线救国组合2.1 ML-KEMKyber模块格上的密钥封装ML-KEM的前身是Kyber属于Module-LWE模块格方案。看不懂数学没关系先把骨架记住——它做的是一件事让双方在没有预先共享秘密的情况下通过公钥协商出一个只有双方知道的会话密钥。KEM是密钥封装机制的缩写你可以把它理解成一次性密码信封发送方拿公钥封装一个随机生成的会话密钥接收方用私钥解封装。它和传统TLS里的协商不太一样不需要来回多个握手轮逻辑更简单。具体到参数多项式环是R_q Z_q[x]/(x^2561)这里q3329n256。每个多项式有256个系数每个系数是0到3328之间的整数。安全级别靠模块维度k区分ML-KEM-512的k2、ML-KEM-768的k3、ML-KEM-1024的k4。密钥生成过程可以简单理解为生成一个随机的小系数秘密向量s再从种子扩展出一个公开矩阵A计算b A·s e其中e是精心设计的小噪声。公钥是(A, b)私钥是s。攻击者拿到(A, b)要恢复s等于在格上求解带噪声的线性方程组——这就是LWE问题的困难本质。为什么带噪声的方程难解生活化类比你在一张表格里知道每一行是某些列值的线性组合但每个结果都被一个几毛钱的小误差污染了。小误差单看无所谓但列数一多可能的组合数量爆炸每个组合都对应一个几乎相同的表格攻击者无法分辨哪一个是真秘密。格密码的安全性恰恰来自区分最接近的解的难度。2.2 签名三件套ML-DSA、SLH-DSA、FN-DSA怎么选签名方面ML-DSADilithium和ML-KEM同出一门都是模块格性能均衡适合作为主力签名。FN-DSAFalcon的优势是签名尺寸小但实现复杂度高需要处理浮点数和NTRU格更适合对带宽敏感的封闭系统。SLH-DSASPHINCS则彻底抛弃格安全性只依赖哈希函数的性质属于最保守的选择代价是签名和性能都不好看。工程上常用组合是正常流量用ML-DSA重要的长期资产用SLH-DSA做交叉备份。签名不同于密钥封装不是二选一的关系可以根据场景混用这也是PQC落地时比较灵活的一点。2.3 规格速查表与选择逻辑算法标准化名称数学基础类型突出特点通常场景KyberML-KEM模块格密钥封装密钥密文小、性能好TLS、证书、加密会话DilithiumML-DSA模块格数字签名综合性能均衡通用签名、代码签名FalconFN-DSANTRU格数字签名签名尺寸最小带宽受限场景SPHINCSSLH-DSA哈希函数数字签名保守、无结构长期资产、密钥备份RSA/ECC—数论/椭圆曲线混合过渡已成熟与PQC混合过渡期这套表是调研后给团队做选型用的最终结论是如果只选一个算法开始工程适配选ML-KEM如果只选一个签名选ML-DSASLH-DSA作为长期备份。这个结论直接指导了后面FPGA复现的选题——先啃ML-KEM这颗硬骨头。3. FPGA复现选型为什么要用硬件把算法焊一遍3.1 三个说服自己的理由很多人一听说调研PQC第一反应是跑个Python脚本或者C参考实现就够了为什么非得上FPGA我当时的想法有三层。第一PQC真正的大规模应用场景不一定是PC和服务器而是嵌入式设备、网关、工业控制器、车机这一类算力和内存都紧张的地方。这些设备目前大量依赖专用密码引擎算法迁移时必须在硬件上验证不能只靠软件模拟。第二软件实现把算法关键性能藏在内存带宽和CPU指令流水线里FPGA实现能逼着你把存储布局、乘法器数量、关键路径一个个摆到桌面上来理解深度完全不一样。第三FPGA能接入真实物理环境做功耗和侧信道评估这是软件库给不了的。第三个理由在调研时显得尤为重要。PQC算法虽然是新面孔但硬件实现中的侧信道风险一点没少——功耗曲线、电磁辐射、指令时序都可能泄露私钥信息。FPGA上可以做可控的电源噪声注入和电磁探头测量这套环境在普通软件运行环境里根本搭不出来。3.2 硬件平台与开发环境开发板选了一块带ARM硬核的中端FPGA板逻辑规模大概覆盖几万LUT板载UART、若干开关和指示灯。ARM核负责接收上位机指令、通过UART把测试向量和种子灌进来可编程逻辑部分只留一个简洁的计算核。开发环境用的是主流FPGA厂商的综合工具RTL语言用Verilog。为什么选这个组合因为调试DDR和复杂总线太费时间PQC核心计算单元只需要几十KB存储双端口BRAM足够ARM只是当个高级串口控制器把我从繁琐的PC通信中解放出来。这也是给没接触过FPGA的读者一个建议不要迷信高端板卡先看算法需求再看接口需求最后看调试复杂度。ML-KEM这个量级的算法一块中端板子完全够用。3.3 实现范围只做核心引擎不做完整产品项目没有把完整的ML-KEM-768握手流程做成产品级引擎而是聚焦三件事第一跑通密钥生成、封装、解封装三个主流程第二重点验证模多项式乘法和Keccak这两个热点模块第三对外开放状态观测接口方便调试和教学。这个边界一开始就定好后面少走很多弯路。如果一开始就想做全流程旁路安全多通道并发大概率会因为调试复杂度失控而卡壳。先把边界切小把核心模块的质量做扎实后面扩展才有基础。4. 系统顶层架构从串口数据到三块BRAM再到状态机4.1 模块划分六个模块各管一摊顶层框架按数据流划分了六个模块通信接口、顶层控制器、存储管理器、NTT加速核、Keccak核、多项式运算单元。通信接口最简单就是把ARM软核发过来的数据按包格式拆开存成seed、公钥、密文等测试输入。顶层控制器是一个细粒度状态机负责把密钥生成、封装、解封装三大流程翻译成一系列存储读写和计算指令。存储管理器不直接让计算单元访问物理RAM而是通过一组带读写valid/ready信号的接口来管理这样既方便仿真也方便后面换DDR或者加内存隔离。Keccak核是PQC的配角但主角——ML-KEM的矩阵A生成、哈希函数、随机数扩展全都靠它。NTT加速核和多项式运算单元是真正的算力核心一次多项式乘法打包成正变换-点乘-逆变换三步。所有大数运算走到乘法单元后都统一经过流水线蝶形算子。这个架构几乎没有为特定算法做过度的定制后续换参数集或者改算法映射都比较容易。4.2 存储布局12比特系数和双端口带宽存储布局早期设计时吃过亏多讲几句。ML-KEM-768需要维护k×k矩阵k3的多项式也就是9个多项式每个多项式256个系数一个系数12比特。按单系数12比特算一个多项式需要384字节9个约3.5KB。板载36Kb约4.5KB的块RAM单块就能放下这些实际用了几块双端口BRAM。为什么用双端口因为蝶形运算每个周期需要同时读两个系数写完结果还要让另一个端口继续输出给下级处理单端口会卡死流水线。还有一个实用教训系数位宽不要图省事按16比特存。12比特才是有效位宽按16比特存会白占三分之一带宽。算带宽时严格按12比特计BRAM的读写使能按字节拼接。前几版为了省事统一按16比特结果内存带宽差了25%改回12比特后性能立竿见影。对FPGA来说BRAM带宽往往比逻辑资源更紧缺位宽设计从一开始就要抠。4.3 主状态机八个状态走完一个流程主状态机定义了八个状态按顺序走IDLE、LOAD_SEED、EXPAND_A、NTT_WS、POINTWISE、INTT_WT、PACK、DONE。每个状态做的事很专一。比如EXPAND_A是从种子出发用Keccak生成矩阵A的系数NTT_WS是对秘密向量s做正变换POINTWISE则是在NTT域做逐项乘加INTT_WT做逆变换回到普通域。状态之间用一组completion信号握手避免出现一拍没对准就全盘错乱的情况。所有的中间系数都缓存在BRAM工作区不额外加通用寄存器堆——寄存器堆在这种密集数据流场景里是浪费。这里值得说明为什么用状态机而不用微代码或软核状态机每个跳转都是确定性的时序容易收敛验证简单。对于ML-KEM这种流程固定的算法状态机是最不容易出错的实现方式。ARM软核只负责通信和结果回传计算流程不依赖它路径很干净。5. 核心计算单元解剖NTT蝶形、模约减与Keccak5.1 多项式乘法为什么是性能命门ML-KEM的安全性建立在格上工程性能却几乎全部押在多项式乘法上。一个多项式有256个系数直接做教科书卷积需要65536次乘加——看起来不算多但密钥生成、封装、解封装要反复做很多次而且每次乘加之后还要做模3329约减。如果直接做光约减涉及的除法就要占掉大部分周期。NTT就是来解决这个问题的它把多项式转到频域两个多项式乘法变成逐项点乘再做一次逆变换。复杂度从O(n²)降到O(n log n)对n256来说大概是8倍差距。更重要的是NTT之后所有系数固定在某个域内模约减可以统一用一个乘法加一个移位替代除法硬件实现非常友好。Kyber的q3329专门选成了满足q≡1 mod 256的素数这意味着x^2561在模q的多项式环上能完全分解可以直接做256点NTT而不需要额外补零。实际参考实现还把256点NTT拆成两层用128个二次因子迭代完成硬件上表现为一组组小的蝶形级联。这个性质不是巧合是算法设计时为了让硬件和软件都能高效实现特意选的。复现PQC时多留意这类参数选择会让实现思路清晰很多。5.2 蝶形运算单元一乘两加流水线要细抠NTT的核心算子是蝶形运算a a ζ·b (mod q) b a - ζ·b (mod q)其中ζ是预计算的旋转因子。硬件上只需要一个乘法器加两个加减法器就能实现一个蝶形。我最初的实现是每四个周期完成一组蝶形流水线分四段取系数、乘法、约减、加减。后来发现乘法器和约减器的组合逻辑深度是瓶颈改成六级流水后频率从150MHz提到180MHz有效吞吐反而提升。代码骨架大致如下module butterfly_unit #( parameter Q 3329 ) ( input wire clk, input wire valid_in, input wire [11:0] a_in, input wire [11:0] b_in, input wire [11:0] zeta_in, output reg valid_out, output reg [11:0] a_out, output reg [11:0] b_out ); // 流水线第一级乘法 reg [23:0] prod_reg; always (posedge clk) begin if (valid_in) prod_reg b_in * zeta_in; end // 第二级Barrett约减 wire [11:0] prod_mod barrett_reduce(prod_reg); // 第三级加减 always (posedge clk) begin if (valid_in) begin a_out (a_in prod_mod) % Q; b_out (a_in - prod_mod Q) % Q; end end // valid同步打拍 reg [2:0] valid_delay; always (posedge clk) valid_delay {valid_delay[1:0], valid_in}; assign valid_out valid_delay[2]; endmodule注意这个骨架为了表达清楚简化了很多细节真实代码里a_out/b_out必须与valid信号严格同步而且流水线级数变化时控制信号也要跟着变化。这就是第7章第一个坑的伏笔。5.3 Barrett约减用乘法器替代除法位宽是命门模约减在FPGA里的核心问题是如何用乘法器替代除法。我的方案是Barrett约减预计算常数c floor(2^16 / 3329)约减时先做一次乘法得到商的高位估计再做一次乘法和减法得到余数。误差控制在很小的范围内只要输入范围不超过某个上限最后一步只需一次条件减法就能把结果拉回[0, 3328]。这个方案比Montgomery舒服的地方在于不需要把模数转成特殊形式也不需要额外的转换因子系数在每个阶段都可以直接解释为标准值。但位宽必须小心。Barrett约减要求乘法器输出位宽足够容纳高位估计我最初用的普通乘法器截到高16位结果在边界情况偶尔丢精度。后来改用完整位宽乘法器高位估计单独接出来问题立刻消失。具体计算公式是t a - floor(a·c / 2^16)·q。这个floor操作在硬件里的等价实现是先做a×c乘法取结果的高16位作为商估计再乘以q做减法。因为c≈2^16/q商估计会有约1的误差最后补一次条件减法。理解这个误差边界对位宽设计非常重要。5.4 Keccak核24轮轮函数单轮展开最划算Keccak是SHA-3系列的核心置换一个f[1600]置换包括24轮每轮要执行θ、ρ、π、χ、ι五个步骤处理1600比特的状态。ML-KEM里Keccak承担的角色很重种子扩展为矩阵A、哈希、XOF都靠它。如果Keccak核太慢整个密钥生成和封装流程都会拖住。我选择了单轮展开结构把一轮的θ/ρ/π/χ/ι组合逻辑写出来状态寄存器在每个周期载入一轮结果重复24次完成一次置换。全展开24轮全部展开能省循环开销但LUT消耗大好几倍频率也难上去不划算。单轮展开配合状态旁路Keccak核跑一次SHAKE256大约1.3µs对整体流程来说够用。Keccak的旋转偏移表和轮常数是固定查表这个坑我在第7章详述。这里只提醒一句直接从规范附件复制不要凭记忆。6. 综合结果与实测数据面积换吞吐的账6.1 资源和时序报告综合后的关键数字如下基于我这版只实现了单路径核心的配置资源项数量说明LUT约12k主要是Keccak和NTT逻辑FF约8.7k流水线寄存器和状态寄存器DSP6蝶形乘法器含Barrett相关BRAM6块36Kb系数存储与中间工作区最大频率180MHz关键路径在Keccak轮函数这是单套NTT核、单套Keccak核、主频180MHz的结果。资源不算紧张但也不能算宽裕因为Keccak的24轮组合逻辑确实吃资源。如果把Keccak和NTT都做成双实例并行面积大概翻1.6倍而不是2倍——控制逻辑可以共用这是并行化时的一个隐藏红利。6.2 关键流程耗时拆解用板级测试跑完整ML-KEM-768三流程实测耗时如下从ARM收到命令到打印结果的时间UART开销约2µs可忽略流程实测耗时密钥生成KeyGen42µs封装Encaps64µs解封装Decaps85µs拆下来看密钥生成里矩阵A的Keccak扩展约占三分之一NTT相关计算占二分之一其余是打包和随机数处理。封装和解封装因为要处理更多参数Keccak调用次数更多时间更长。6.3 与软件实现的对照FPGA并不更慢对比软件参考实现普通PC上优化的C版本大致在几十微秒到一百多微秒之间FPGA这组数据放到资源受限的嵌入式场景里是很有竞争力的。更重要的是这个性能是可预测的、确定性的没有操作系统调度干扰。对工业控制、车载、网关这类实时性敏感的场合确定性比峰值性能更值钱。功耗方面我实测的核心逻辑功耗在几百毫瓦量级远低于常见CPU跑软件的功耗。虽然这次没有做精细的DVFS调优但已经能看出FPGA在PQC场景下的功耗优势。对电池供电的物联网设备来说这个优势往往比绝对性能更关键。7. 踩坑实录流水线错位、约减溢出与上板崩7.1 蝶形流水线的valid延迟必须同步第一个坑出现在NTT核联调的时候。单独测蝶形单元输入输出都正确单独测NTT的地址生成访存地址也对可一旦把两级连起来输出结果就完全对不上。查了两天最后把波形拉出来逐周期看才发现问题蝶形单元里加了流水线寄存器a_out和b_out比a_in和b_in晚了几拍才出来但地址生成模块还按下一拍读下一组系数在操作读到的数据完全错位。这是典型的valid延迟不同步问题数据通路的流水级数变了控制通路没有跟着变。解决方式很朴素但通用给控制信号手动打同样拍数的延迟保证valid和data在时间上严格对齐。更稳的做法是让地址生成模块把数据请求做成请求-应答式的握手而不是固定时序的发起-等待这样流水线任何一级改变深度都不会影响正确性。经过这次之后我在项目里所有数据通路都强制使用握手信号再没犯这类错。7.2 Barrett约减位宽溢出单测过不了边界第二个坑是约减偶尔飘。表现为单测几百组都没问题跑长流程密钥生成加封装加解封装会在随机位置冒出个别系数大于3329。一开始以为是存取的随机性问题后来把出问题的系数单独拎出来复算发现每次都是同一个中间多项式里的同一类位置。对照Barrett约减公式细看才明白常量c约等于19.7做高位估计时如果乘法结果截断太多部分输入的商估计偏小最后的条件减法来不及修正。解决方法是把乘法器输出完整保留扩大乘法位宽而不是裁到高16位或者把条件减法扩展为循环减法。我选了更直接的方案乘法器输出完整保留高位估计单独拉出来极大减小截断误差。改完之后连续跑了一整夜随机测试向量零异常。这个坑给的经验是模约减这类模块单测样例再多也顶不上随机长流程但反过来调试时又要先构造特定边界值来压测把最坏情况暴露出来再跑长流程验证。两个方向都缺一不可。7.3 Keccak旋转常量记错半个比特中间状态就全乱第三个坑有点丢人Keccak的旋转偏移表我一开始按记忆写结果反了。Keccak规范里旋转量表的某一行我记错了两个数单轮置换测不出来因为很多状态值对错误旋转不敏感但在跑ML-KEM的向量比对时扩展出来的矩阵A系数偶尔对不上。最后是用离线参考实现对中间状态逐一比对定位到某一轮的输出差异才把表改对。教训Keccak的轮常量和旋转表是公认的又长又容易抄错务必直接从规范文档复制不要手输更不要凭记忆。硬件验证里小错不爆发、爆发难定位是最贵的能用离线脚本一个周期跑完的检查不要留给板级调试。7.4 仿真全过一上板就随机出错第四个坑集中在通信和时钟域。RTL仿真里UART、顶层控制器、Keccak全连起来能一次跑通但板子上一跑就随机丢字节、偶发握手超时。排查过程先怀疑UART波特率微小偏差用逻辑分析仪抓了波形发现是ARM发出的读请求在可编程逻辑端偶尔收不到——原因是异步时钟域间的握手信号没有同步。FPGA里两个时钟域的信号直接采大概率会采到亚稳态。解决分两步第一给所有跨时钟信号加两级同步器第二在时序约束里显式声明异步时钟组把跨时钟路径设为false path不然工具乱报时序综合结果也会偏。改完再上板跑连续十二小时没有一次错误。仿真过不等于上板过这个道理做FPGA的都知道但每次踩到都还是疼。跨时钟域问题尤其隐蔽因为它在仿真里往往不出现只有真实硅片上才露出獠牙。8. 调研结论与后续扩展8.1 PQC落地比想象的快但迁移顺序很重要调研下来我最大的感受是PQC落地的真实阻力已经不是算法成熟度而是存量系统的迁移成本。混合模式经典算法与PQC同时启用是过渡期的必然选择。TLS场景预计会先动然后是证书体系、代码签名、固件升级链路。对普通企业来说先把密钥生命周期管理摸清楚把哪些资产需要长期保密梳理出来这个优先级比纠结选Kyber还是Dilithium更现实。算法本身方面ML-KEM的优势非常突出它和现有密码库的接口兼容性也做得很好工程适配成本比想象中低。签名方面ML-DSA是均衡之选SLH-DSA则适合作为长期资产的保守备份。这轮调研让我对什么时候换、先换什么有了清晰答案。8.2 如果继续做我会碰这几个方向FPGA这个项目还可以往几个方向走一是把单一NTT核改成多实例并行用面积换吞吐适合网关等高并发场景二是把核心计算单元加上侧信道防护比如在NTT蝶形上做掩码处理用FPGA验证防护对性能的影响三是把经典算法和PQC做成同一个硬核里的混合模式加速单元这样设备升级时不用换芯片只换逻辑。我个人接下来最想做的是第三项因为它更接近真实产品的形态。混合模式的硬件加速器一旦成熟设备厂商只需要更新比特流不需要改动电路板这对存量设备的平滑迁移价值很大。最后分享一点体会。PQC的数学门槛不高难的是把一个看起来只是换算法的工程问题拆成算法选型、硬件映射、验证三条线并行推进。FPGA复现的过程逼着我从数学定义一路走到电路时序这种全局视角是只看文档学不来的。如果你也想做类似的事我建议从ML-KEM起步先跑通参考实现再动手写NTT核不要一上来就追求完整产品。先让核心计算单元稳定跑起来再谈优化和扩展这条路走下来最顺。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑