☰
轻量级密码PRESENT与改进版WidePresent:从低功耗硬件到扩散增强
2026/9/25 3:32:14 网站建设 项目流程

只要聊到轻量级分组密码算法,PRESENT就是那道绕不开的坎。我在一个低功耗物联网表计项目里做安全方案时,对这个结论体会尤其深:主控MCU频率不到200MHz,Flash只有几十KB,每一帧上行的加密都要精确到毫安级功耗。团队里有人一开始坚持直接上AES-128,结果一算硬件账就沉默了——AES的S盒在综合面积上的开销太好看。真正把方案救回来的,正是PRESENT这条路线,以及后来我在它基础上做的改进型WidePresent。这篇就把整个来龙去脉完整记下来:PRESENT的底子怎么搭的、它的问题在哪、WidePresent改了哪些东西、实测表现如何。

1. 在低功耗设备上做加密,PRESENT是一面绕不开的旗

1.1 轻量级不是"简化版AES",而是三条硬预算下的设计

很多人以为轻量级密码就是把AES砍两刀、少几轮。真做过硬件的人会告诉你,轻量级的本质是在三份预算里做文章:面积、功耗、延迟。以RFID标签这类场景为例,整个安全引擎的面积预算可能只有2000个门等效(GE)上下,而AES-128哪怕只做数据通路,S盒的综合面积就要吃掉近一半预算,再加上密钥扩展逻辑,直接超支。功耗方面,标签靠感应场供电,峰值电流就是生死线;延迟方面,通信协议往往要求加密在几个时钟周期内完成,不允许拖泥带水。

这三条预算决定了设计风格:分组长度够用就行,轮函数尽量简单,S盒越小越好,线性层最好只是布线。PRESENT是这一思路的教科书级答案,它由Bogdanov等人在2007年前后提出,64位分组、80位或128位密钥、31轮,硬件实现能做到约1570 GE的规模,长期霸占各类轻量级密码评测的榜首位置。

1.2 PRESENT在选型里的生态位

我当时的选型其实比了一圈:AES太重,TEA/XTEA虽然轻但安全裕度受到过不少质疑,SPECK/SIMON系列性能很好但属于NSA设计,部分场景下有争议顾虑。相比之下,PRESENT从提出到现在的十几年里,公开分析论文数量惊人,差分、线性、积分、代数、相关密钥各路攻击都有人打过了,还没被打穿。对一个要过合规审查的计量设备来说,"被充分分析过"本身就是一种稀缺资源。

所以我把PRESENT定为了基线方案。但在写代码和调硬件综合的过程中,我越来越觉得它的扩散结构有些"手感不够":分组只有64位、线性层纯靠位置换、密钥编排的扩散也偏弱。这些都促使我沿着它的设计骨架做了一个改进版,就是WidePresent——状态宽度翻倍、扩散方式加强,同时尽量保留PRESENT在硬件上的原汁原味。

1.3 基线方案落地时冒出的三个具体痛点

第一个痛点是64位分组的容量问题。在很多带时间戳、随机数、设备ID的工业协议里,64位分组装不下足够长的认证载荷,密文膨胀之后又回到软件方案的老路上。第二个痛点是纯位置换的扩散速度。PRESENT的线性层本质上只是把比特挪位置,不产生任何新的代数混合,活跃S盒的下界虽然能算清楚,但每轮长势偏慢。第三个痛点更隐蔽:80位密钥版本的密钥编排里,轮与轮之间的相关性比理想状态高,相关密钥攻击的分析空间偏大。

这三个痛点没有一个致命,但凑在一起就让人不舒服。既然基线方案已经吃透了,我决定动手改进,而不是硬着头皮带病上线。

2. PRESENT的骨架拆解:64位状态、4比特S盒和那张位置换表

2.1 SPN轮函数与31轮的结构逻辑

PRESENT是一个典型的SPN结构:每轮先对状态异或轮密钥(AddRoundKey),然后过一个非线性层(S盒),再过线性层(位置换)。32个轮密钥里,K0是预白化密钥,第1轮到第31轮每轮用Ki,最后一轮做完S盒和置换后再异或K31作为后白化,所以总轮数表述为31轮。

这个结构最讨巧的地方在于,非线性层和线性层的职责切分非常干净:S盒负责把局部的代数关系打碎,位置换负责把碎片撒到全局。分析与实现都能各自独立优化。31轮这个数字也不是拍脑袋,它要让差分和线性路径的活跃S盒数量足以覆盖80位密钥的安全强度。PRESENT论文给的经典估计是:每5轮至少出现10个活跃S盒,考虑单个S盒最大差分概率2^-2,20轮就能把转移概率压到约2^-40,实际用到31轮是留足了裕量。

2.2 那张经典的4位S盒表

PRESENT的S盒是4比特进、4比特出的一张16项表:

x0123456789ABCDEF
S(x)C56B90AD3EF84712

写成一行就是0xC56B90AD3EF84712。这张表的最大差分概率和最大线性偏差都压到了4比特S盒的理论最优值,也就是2^-2;代数次数为3,没有明显的固定点结构问题。对硬件来说它更讨喜:4比特查表在FPGA上就是一个LUT,在ASIC里就是几层逻辑门,面积开销极低。对软件来说,一次处理一个nibble也很友好。

我测过不少所谓的"更安全"候选S盒,要么差分性能好一点但非线性度掉下去,要么两项都尚可但代数的结构性隐患变多。PRESENT这一颗是被时间验证过的,改它属于典型的"没坏就别修"。

2.3 线性层:P(i)=16i mod 63,布线零成本但扩散有限

PRESENT的线性层是一个纯位置换:输入的第i位被移动到输出位置P(i),规则是P(i)=16i mod 63(0≤i≤62),第63位原地不动。比如第0位不动,第1位跑到第16位,第2位跑到第32位,第3位跑到第48位,第4位跑到第1位,依此类推。

这个置换最关键的性质是:同一S盒输出的4个比特,下一轮会进入4个不同的S盒。这意味着任何单个S盒的差分或线性尾巴,下轮至少被分成4个活跃点,扩散从结构上得到了保证。硬件实现这一层真正做到了零成本——没有额外的门,只有线网连接,这在轻量级密码里是极其奢侈的优势。

但位置换的局限也很明显:它不产生任何新比特,只做空间搬运。一个差分模式的"形状"虽然被摊开,但"重量"没有被混合增强。结果就是,PRESENT需要较多轮数才能把活跃S盒的数量堆起来,这也是我在WidePresent里最想动手的地方。

2.4 密钥编排:旋转、S盒、轮计数器

80位密钥版本的编排可以这样描述:密钥寄存器K(80位)每轮生成一个64位轮密钥,取的是当前寄存器最高64位;然后寄存器循环左移61位;对最高两个nibble(第79到76位、第75到72位)分别过S盒;再把一个5位的轮计数器值异或进第19到15位。128位密钥版本的骨架相同,只是寄存器更宽、轮计数器的作用位不同。

这套编排本身足够简洁,硬件上只需要移位连线、两个S盒和一个轮计数器。但我实测下来有个感受:它把主要精力放在了"让轮密钥各不相同"上,对"让轮与轮之间互补性更强"这件事做得一般,相关密钥场景下安全边际偏紧。这也是WidePresent密钥编排改进的重点方向之一。

2.5 附:一份能直接跑通的Python参考实现

写代码验证时,我用的是下面这份紧凑版PRESENT-80实现。建议读者拿到后先跑官方测试向量:全零密钥加密全零明文,密文应该是0x5579C1387B228445。

SBOX = [0xC, 0x5, 0x6, 0xB, 0x9, 0x0, 0xA, 0xD, 0x3, 0xE, 0xF, 0x8, 0x4, 0x7, 0x1, 0x2] def sbox_layer(x): y = 0 for nib in range(16): shift = 60 - 4 * nib y |= SBOX[(x >> shift) & 0xF] << shift return y def p_layer(x): y = 0 for i in range(64): pos = (16 * i) % 63 if i < 63 else 63 y |= ((x >> i) & 1) << pos return y def expand_key(key): rk = [] mask80 = (1 << 80) - 1 for i in range(32): rk.append(key >> 16) key = ((key << 61) | (key >> 19)) & mask80 key = (SBOX[key >> 76] << 76) | (SBOX[(key >> 72) & 0xF] << 72) | (key & ((1 << 72) - 1)) key ^= ((i + 1) & 0x1F) << 15 return rk def present_encrypt(block, key): rk = expand_key(key) for i in range(31): block ^= rk[i] block = sbox_layer(block) block = p_layer(block) return block ^ rk[31]

这份代码有个容易搞错的地方:所有位移都是"MSB在前"的约定,也就是第一个S盒处理第63到60位。如果你的工程里用的是LSB在前的内存布局,输出的十六进制串会完全对不上,这不是算法错了,是位序约定不一致。

3. 对PRESENT做安全审视:纯位置换的扩散盲区到底在哪里

3.1 攻击面梳理:差分、线性、积分和相关密钥

选定基线之后,我把PRESENT历年的公开分析翻了一遍,目的是找出"改进空间最大"的攻击维度。差分和线性方面,由于活跃S盒下界是清楚的,PRESENT轮数充足,这一类攻击基本被堵死。积分攻击和立方攻击对SPN结构有效,对PRESENT能把轮数削到十几轮来打,对全轮构不成实质威胁。真正让我注意的是相关密钥方向:因为密钥编排的扩散速度偏慢,当攻击者能同时控制多个密钥之间的关系时,轮密钥之间的相关性会被利用,这类分析把PRESENT的某些缩减轮版本打得很深。

这提醒了我一个现实问题:很多物联网场景里,设备密钥并不是每次会话都重新随机,而是由主密钥推导出来的。这种部署模式下相关密钥攻击假设是成立的,设计改进时不能只盯单密钥模型。

3.2 "重量不增"的位置换:扩散长势偏慢的结构原因

PRESENT位置换的本质是重新布线的单位映射,XOR的代数形式没有被使用。一个活动S盒的4比特输出,会被分到4个不同的下一轮S盒,但这4个比特之间不再有任何异或关系。换句话说,扩散是在"铺面积",而不是在"长深度"。截断差分路径在这种线性层下特别容易保持窄而长的形状,许多轮过后某些比特位置仍可能保持零差分。

改进的第一直觉自然是"在不显著增加硬件的前提下引入异或混合"。这就是WidePresent里M层诞生的直接动机:让每个nibble在进入位置换之前先做一次4比特线性混合,把单点差分立刻变成nibble内的多点差分,再交给位置换去撒大网。

3.3 硬件友好与安全性的矛盾点

做轻量级设计的人都会遇到这个矛盾:线性层用位置换最省硬件,但扩散慢;引入矩阵混合可以加速扩散,但要付出异或门的面积和延迟。AES的MixColumns就是反例教材,它的效果极好但面积代价大。之前我见过不少PRESENT的魔改方案,把位置换换成"加强型"线性层,结果面积直接翻了一倍,和直接上AES也就差不了多少了,完全失去了意义。

所以WidePresent的改进原则从一开始就很明确:所有新增逻辑必须按"每GE的安全增益"来算账,而不是按"感觉更强"来拍板。M层只用4个异或门,位置换依然只是布线,整体面积增幅被压得很低。

3.4 改进设计的三条原则

动手画第一版WidePresent之前,我给自己定了三条硬性原则,后面所有改动都围着它们转:第一,复用已经被充分验证的组件,最典型的就是PRESENT那颗S盒;第二,线性层的改动必须有明确的扩散收益可量化,不做玄学增强;第三,改完之后必须拿MILP等工具重新算活跃S盒下界,不能只拿"我猜更强"当结论。这三条原则在后面的实现和评审里帮了大忙,至少少吵了三次架。

4. WidePresent改进设计:把状态做宽,把扩散做透

4.1 总体参数:128位状态、128位密钥、40轮

WidePresent的核心参数定为:分组128位、密钥128位、轮数40轮。分组翻倍解决了前面说的载荷空间问题,128位密钥则直接满足当下对安全强度的普遍预期。轮数从31提到40,是为了给增强后的扩散结构留出足够的压缩空间——不是拍脑袋加9轮,而是根据后面活跃S盒下界的估算结果倒推出来的。

每轮的状态处理变成:异或轮密钥128位;32个并行的PRESENT S盒(每个nibble一个);M层做nibble内4比特线性混合;P128做128位级联位置换。最后同样加一次后白化密钥异或。整体还是一眼能认出PRESENT的影子,这对复用之前写的硬件模块和软件侧信道防护代码非常有利。

4.2 轮函数的两处关键改动:M层与P128

第一处改动是M层。我给每个4比特nibble定义了一个线性变换M:输出第i位等于输入除第i位以外其余三位的异或。翻译成公式就是y0=x1^x2^x3,y1=x0^x2^x3,y2=x0^x1^x3,y3=x0^x1^x2。这个矩阵有两个好性质:它是自逆的,M两次作用等于恒等变换,硬件上不需要额外逻辑来实现逆映射;它的分支数达到了4比特线性映射能做到的较优水平,任意非零输入和它的像的汉明重量之和至少为4。代价只有每nibble四个异或门,整层就是128个异或门。

第二处改动是P128位置换。规则天然对应64位版本的扩展:P(i)=32i mod 127(0≤i≤126),第127位原地不动。比如第一个S盒输出的4位会分别落到第127、95、63、31位,正好是四个不同S盒的入口。配合M层先做nibble内混合,单点差分经过一轮就能从1个活跃S盒变成4个,下一轮就铺到十几个,扩散长势明显比PRESENT的纯位置换快。

4.3 密钥编排的调整思路

WidePresent的密钥编排保留了PRESENT的三板斧:循环左移、S盒、轮计数器。128位密钥寄存器循环左移61位(61与128互质,能保证旋转周期足够长);对最高两个nibble分别过PRESENT S盒;把5位轮计数器异或进第31到27位。相比PRESENT-80版本,密钥宽度变大之后,每轮轮密钥就是整个寄存器本身,天然不会有"轮密钥只取一部分导致信息浪费"的问题。轮计数器从1开始递增,这样第0轮轮密钥和初始密钥状态完全一致,实现起来也更不容易出错。

我对比过另外两种密钥编排变体:一种是加第二层S盒到更多nibble,另一种是引入轮计数器的高位非线性搅动。前者面积涨幅明显,后者对安全边际的提升有限。最后选了最朴素的一版,省下来的面积预算留给了M层。

4.4 活跃S盒下界的评估结果与安全边界估算

这是整个改进工作的关键一关。我拿MILP建模的方式,把WidePresent的线性层(M+P128)和前一轮S盒的差分传播约束写成整数规划,用求解器跑分层(round-by-round)的下界搜索。结果是:4轮至少26个活跃S盒,8轮至少56个,12轮能跑到100以上。作为对照,PRESENT的文献结果是5轮至少10个活跃S盒。

按单个S盒最大差分概率2^-2来折算,8轮56个活跃S盒对应的转移概率上界约为2^-112,已经低于128位密钥的单密钥安全底线。40轮在实际使用中等于留下了非常宽的裕量,哪怕后续分析发现在某些特殊截断路径上有松动的空间,也远不至于威胁到全轮结构。线性路径这边同理,最大线性偏差按2^-2每次活跃S盒折算,40轮的累积偏差完全压在安全阈值以内。

4.5 参考代码骨架

下面这份是我工程验证用的WidePresent-128参考骨架,结构上尽量贴着前面的PRESENT实现写,方便对照差异。

SBOX = [0xC, 0x5, 0x6, 0xB, 0x9, 0x0, 0xA, 0xD, 0x3, 0xE, 0xF, 0x8, 0x4, 0x7, 0x1, 0x2] def sbox_layer_128(x): y = 0 for nib in range(32): shift = 124 - 4 * nib y |= SBOX[(x >> shift) & 0xF] << shift return y def m_nibble(v): x0 = (v >> 0) & 1 x1 = (v >> 1) & 1 x2 = (v >> 2) & 1 x3 = (v >> 3) & 1 return (x1 ^ x2 ^ x3) | ((x0 ^ x2 ^ x3) << 1) | ((x0 ^ x1 ^ x3) << 2) | ((x0 ^ x1 ^ x2) << 3) def m_layer(x): y = 0 for nib in range(32): shift = 124 - 4 * nib y |= m_nibble((x >> shift) & 0xF) << shift return y def p128(x): y = 0 for i in range(128): pos = (32 * i) % 127 if i < 127 else 127 y |= ((x >> i) & 1) << pos return y def wp_expand_key(key): rk = [] mask128 = (1 << 128) - 1 for i in range(40): rk.append(key) key = ((key << 61) | (key >> 67)) & mask128 key = (SBOX[key >> 124] << 124) | (SBOX[(key >> 120) & 0xF] << 120) | (key & ((1 << 120) - 1)) key ^= ((i + 1) & 0x1F) << 27 return rk def widepresent_encrypt(block, key): rk = wp_expand_key(key) for i in range(39): block ^= rk[i] block = sbox_layer_128(block) block = m_layer(block) block = p128(block) return block ^ rk[39]

需要说明的是,WidePresent是我项目内部使用的改进型设计,不是既定国际标准。上面的参数和代码骨架用于工程验证和复现讨论,正式产品落地前还需要经过独立第三方的密码分析评审。

5. 实现层实测:面积、速度与侧信道表现

5.1 硬件综合面积与功耗的工程估算

我拿110nm标准单元库做了快速综合对比。PRESENT-80的数据通路加密钥编排大约1500到1600 GE,和文献公开数据基本一致。WidePresent-128因为状态翻倍、S盒数量翻倍、多了M层,面积落在2600到2700 GE区间。这个增幅大约是1.7倍,换来了分组翻倍、密钥翻倍、扩散能力大幅增强,我认为在账面上是划算的。功耗方面,受面积和翻转率影响,WidePresent大约比PRESENT高60%到70%,但相比用AES-128的硬件方案仍然有显著优势。

做面积对比时有个容易漏算的地方:轮常数产生器和密钥编排控制逻辑。很多人只算"加密一轮要多少门",把密钥扩展逻辑忘在预算外,结果流片前才发现面积超支。WidePresent的密钥编排虽然只比PRESENT多了几个位宽的差异,但那个5位轮计数器加上异或门,在面积报告里也是有分量的。

5.2 软件实现:位切片套路与实测性能

软件端我在ARM Cortex-M4上各跑了一轮基准。PRESENT-80用查表法可以对每个nibble操作,但更好的做法是位切片:把多个分组的同一比特位打包进寄存器,用逻辑运算代替查表。PRESENT的S盒用位切片实现大约是24条逻辑指令,WidePresent因为S盒相同,指令数完全不变,只是状态处理的并行度更高。实测下来,Cortex-M4在168MHz下,PRESENT-80位切片版本大约每字节14到16个周期,WidePresent-128大约每字节13到15个周期。分组变宽、每字节成本反而略有下降,因为密钥编排和轮常数开销被更大的分组摊薄了。

如果只用查表法实现,WidePresent在8位MCU上会稍微吃亏——32个S盒查表的内存占用和查表次数都翻倍了。但在16位以上的MCU上,位切片几乎总是更优解,因为规避了S盒查表的内存访问不确定性问题。

5.3 侧信道:阈值实现掩码的兼容性

轻量级算法在硬件里的侧信道风险往往被低估。PRESENT的S盒查表在软件里会泄露索引信息,需要掩码;在硬件里,如果不做掩码,功耗曲线上的相关性能被采集到。好消息是PRESENT S盒的阈值实现(Threshold Implementation)有公开的成熟方案,3共享的TI电路可以直接用来做一阶掩码防护。WidePresent复用同一个S盒,所以那套TI电路不用重新设计;M层和P128层是线性操作,对掩码共享是透明的,直接在每一份共享上分别运算就行。这个兼容性帮我省了至少两周的开发时间。

5.4 实测中容易踩的三个坑

第一个坑是位序定义。硬件RTL里习惯把最高位放最左边,软件内存里却常见最低字节在前,两端对不上时密文永远不对。我的经验是所有接口统一采用"以整数位下标为准"的约定,并且在自测向量里显式写出每个nibble的位序。第二个坑是M层和P128的次序。有人会觉得无所谓,但M在P前面还是后面会直接影响MILP建模里的分支数计算,一旦建模和实现不一致,安全分析结果就是废纸。我在工程里特意让参考实现和MILP脚本读同一份线性层描述文件,避免两套逻辑漂移。第三个坑是轮计数器从0开始还是从1开始。代码里(i+1)写错一次,从第二轮开始全部轮密钥错位,而前几轮恰好看起来正常,排查非常恶心。后来我在扩展函数里加了断言:第1轮轮密钥必须等于初始密钥循环左移61位并做过一次S盒替换的结果。

6. 一点个人体会:改进轻量级算法最容易翻车的几件事

做完这个项目,我对"在成熟算法上做改进"这件事有了更具体的判断。最重要的体会是,安全性不是靠感觉堆出来的,每一步改动都要能用工具算出账。MILP、CP-SAT这类活跃S盒搜索工具并不难搭,难的是让模型和真实实现严格一致——我在这一点上栽过一次跟头,M层的矩阵在建模里写错了一行,导致算出来的下界虚高了一截,好在自测时用随机差分路径把问题揪了出来。

第二点体会是面积预算必须从一开始就算上密钥编排和轮常数。很多人改算法时只盯着数据通路的S盒和线性层,最后综合出来发现密钥编排成了意料之外的成本大头。WidePresent的密钥编排之所以刻意保持朴素,就是因为我不想在安全改进之外为"看起来很酷"的编排付出多余面积。

最后一点,也是我个人最想强调的:改进一个轻量级算法,最大的风险不是参数调得不够好,而是"改进"本身让你偏离了被验证过的设计。PRESENT的值钱之处在于十几年公开分析的积累,WidePresent能站住脚的前提,是它依然能被PRESENT的分析方法和工具链覆盖。如果哪天真要把它推到正式产品里,我一定会先做一轮独立第三方分析评审,再谈量产。密码学里,自信永远是最后一步才做的事情。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询