ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Verilog实现DES加密算法:从Feistel结构到FPGA仿真验证全解析

用Verilog实现DES加密算法:从Feistel结构到FPGA仿真验证全解析 简介基于Verilog的DES加密算法工程包面向FPGA开发者和硬件安全学习者展示如何在可编程逻辑上实现经典对称加密算法。压缩包内共134个文件大小仅409KB以8个Verilog源代码文件和8个mif存储器初始化文件为核心并包含Quartus工程配置qpf/qsf/qws、编译报告rpt及readme说明便于直接打开工程或重新综合。已有1063人学习。文件完整覆盖DES核心模块、顶层设计及仿真测试具体可看到初始置换、16轮迭代的轮函数、S盒、P盒和子密钥生成等关键电路实现MIF文件用于存放S盒等查找表数据方便对照理解非线性混淆的硬件实现。借助实际工程与测试平台可快速掌握用硬件描述语言设计加密算法的思路并理解对称密码在FPGA上的并行处理优势适合课程设计、综合实践或安全通信模块的参考。 去年整理手头FPGA项目时翻出多年前用Verilog写的DES加密模块又花了一晚上把它重跑了一遍。当时数电课设把人逼疯的设计现在回看其实是一块特别好的练手材料——结构规整、位操作密集、测试向量现成非常适合验证Verilog代码风格和仿真流程。这篇就从头到尾聊一聊基于Verilog的DES加密算法怎么设计、怎么实现、怎么调通顺便把当年踩过的坑一并交代清楚。DES是最经典的对称分组密码算法分组长度64位密钥长度64位有效位56位。虽然现在做实际项目几乎没人单独用它了但在FPGA硬件实现和数字IC教学里它依然是绕不开的入门素材。原因很简单算法结构足够规整Feistel网络加16轮迭代的套路特别适合写可综合RTL而且官方和社区给出了大量可供核对的标准测试向量仿真调错非常方便。任何想从“会写计数器”过渡到“能写一个完整加密模块”的人拿DES练手都是很稳妥的选择。1. DES算法主架构和Verilog实现的底层逻辑要把DES变成Verilog代码第一件事不是急着打开编辑器而是先把算法的数据结构理清楚。DES整个流程可以压缩成三句话初始置换IP然后做16轮Feistel迭代最后做逆初始置换FP。16轮迭代里每一轮做的事情完全一样——左半部分直接变右半部分右半部分经过扩展、异或、S盒、P置换之后和左半部分相加再交换位置。1.1 从Feistel结构看硬件友好的密码骨架Feistel结构对硬件实现特别友好这是DES在当年能顺利做成芯片的关键原因。Feistel轮函数有个特点加密和解密可以用同一套硬件只是子密钥的使用顺序反过来。放到Verilog里这意味着我们只需要把轮函数做成一个可以反复调用或者用generate展开的模块加密解密的区别只在于轮密钥的排列方向。另外一个硬件友好的点在于位宽固定、无浮点、无动态循环。每一轮的数据宽度全是确定的32位变成48位、再回到32位密钥调度每轮固定左移1位或2位16轮之后停止。这种“每一步都知道自己在干什么”的算法写RTL和写C语言的体验是完全不同的——C语言有循环变量和动态索引Verilog则要把这些展开成确定的硬件逻辑。1.2 密钥调度56位有效密钥如何生成16轮子密钥DES的原始密钥是64位其中每8位有一位奇偶校验位真正参与运算的只有56位。密钥调度的流程是这样的64位密钥经过PC-1置换去掉8个校验位得到56位。56位分成C0和D0两个28位的部分。每一轮根据轮数对C、D分别循环左移第1、2、9、16轮移动1位其余轮移动2位。左右合并后经过PC-2置换输出该轮的48位子密钥。在Verilog里实现密钥调度最经典的做法是用两组28位寄存器存C和D然后用拼接运算符完成循环移位。比如当需要循环左移1位时直接写{C[26:0], C[27]}这种拼接综合出来的逻辑就是一段简单的布线和移位资源开销极低。1.3 为什么说这个项目适合拿来验证Verilog基本功很多初学者一上来就折腾AES或者国密算法我不是很推荐。AES的S盒是有限域求逆计算出来的处理不好整个逻辑就崩了DES的S盒和置换表都是预先定义好的属于典型的“查表型”算法。查表在Verilog里怎么做无非就是case语句、function函数、或者ROM三种方式。每一种都对应着不同的RTL风格练的正是“用硬件思维做查找”的基本功。再加上DES整个流程里有大量的位宽调整、拼接、置换操作写一遍下来对Verilog的向量操作、参数化模块、generate语法、仿真testbench这一整套体系都有了实打实的理解。这个项目的性价比真的很高。2. RTL模块设计与实现从顶层接口到轮函数拆解我这次重写的时候特意让模块层次清晰一些顶层top只负责例化和状态控制密钥调度单独一个模块轮函数单独一个模块。S盒也不写死在轮函数里而是以function的形式独立出来。这样每个模块都能单独仿真调试效率高很多。2.1 顶层模块接口规划与内部状态控制顶层模块的接口是这么设计的module des_top #( parameter MODE_ENCRYPT 1b1, parameter MODE_DECRYPT 1b0 )( input wire clk, input wire rst_n, input wire start, input wire [1:0] mode, // 1: encrypt, 0: decrypt input wire [63:0] key, input wire [63:0] data_in, output reg [63:0] data_out, output reg busy, output reg done );这里的关键设计点是mode信号。刚才说过DES的解密和加密轮函数完全一样只是子密钥的顺序反过来。所以顶层内部只需要一个逻辑根据mode决定是从密钥调度的第0轮开始取子密钥还是从第15轮倒着取。这个设计意味着轮函数模块本身不需要知道自己在做加密还是解密代码复用的程度很高。状态控制方面我采用了简单的有限状态机状态IDLE、LOAD、ROUND_CAL、FINISH。ROUND_CAL状态下用一个3位或4位计数器记录当前轮数每轮在时钟上升沿完成一次运算并更新数据寄存器。由于DES是16轮迭代4位计数器完全够用。2.2 密钥扩展模块循环左移与PC-2置换的实现密钥调度Strongly建议单独成一个模块因为它的端口是64位原始密钥输入、16个48位子密钥输出。为了便于顶层按顺序取用子密钥我选择把所有子密钥一次性生成并寄存下来用二维数组或者16个独立的48位寄存器来存。循环左移逻辑的写法非常讲究。如果按常规思路写一个always块去判断轮数再移动1位还是2位代码会显得很啰嗦。更推荐的做法是直接把移位数写成一个查表逻辑// shift_amt: 1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1 for rounds 1-16 wire [1:0] shift_amt; always (*) begin case (round_cnt) 4d0, 4d1, 4d8, 4d15: shift_amt 2d1; default: shift_amt 2d2; endcase end然后每次计算子密钥时用拼接语法完成循环左移C_next (shift_amt 2d1) ? {C[26:0], C[27]} : {C[25:0], C[27:26]}; D_next (shift_amt 2d1) ? {D[26:0], D[27]} : {D[25:0], D[27:26]};PC-2置换本质上是一个48位的拼接赋值把C和D中选定的位按顺序拼起来即可。这类置换用wire直接实现最方便不要放进always块里。写成组合逻辑的wire赋值综合工具会把它们优化成纯布线网络不产生任何触发器。2.3 轮函数模块E扩展、S盒、P置换的完整数据通路轮函数是DES的核心数据通路输入是32位的右半部分R和48位的子密钥K输出是32位的F函数结果。整个数据通路分四步第一步32位的R经过E扩展变成48位。E扩展的规则是把R的某些位重复使用实现方式就是一个48位的wire赋值。第二步扩展后的48位数据和子密钥做按位异或得到48位中间值。第三步把48位中间值分成8个6位的组每组送进一个S盒得到一个4位结果8组拼起来共32位。第四步这32位经过P置换输出最终结果。轮函数在Verilog里的框架大致如下// E扩展 wire [47:0] exp_r; assign exp_r {R[31:28], R[3:0], R[27:24], R[7:4], ...}; // 异或 wire [47:0] xor_result; assign xor_result exp_r ^ round_key; // 8个S盒 wire [3:0] s_out [0:7]; s_box_inst s0 (.addr(xor_result[47:42]), .data(s_out[0])); s_box_inst s1 (.addr(xor_result[41:36]), .data(s_out[1])); // ... 省略中间例化 ... s_box_inst s7 (.addr(xor_result[5:0]), .data(s_out[7])); // P置换后输出 assign f_out {s_out[6], s_out[3], ...};E扩展和P置换不需要额外存储直接在assign里写位置对应关系就行。我建议初学者把标准文档里的E盒、P盒表打印出来对着表写接线写完再对照检查一遍。这块的错大多数不是逻辑错误而是表抄错了。2.4 S盒实现的三种方案与资源对比S盒是整个DES里唯一一块非线性逻辑也是资源占比最大的部分。我在不同版本的代码里试过三种实现方式各有利弊实现方式代码风格资源消耗调试方便度适用场景case语句一个case分支对应一个输入值综合为LUT逻辑较省资源最高仿真波形里可以直接看绝大多数场景function函数用function封装S盒查询代码简洁取决于综合器优化中可读性好代码复用要求高的项目ROM例化用case或$readmemh定义S盒占用Block RAM或分布式RAM低需要额外初始化文件大规模复用、频繁换密钥的场景我个人最推荐第二种function。比如S1盒可以写成function [3:0] s1; input [5:0] addr; begin case (addr) 6d0: s1 4d14; 6d1: s1 4d4; 6d2: s1 4d13; 6d3: s1 4d1; // 写满64个分支 default: s1 4d0; endcase end endfunction用function的好处有两个第一8个S盒可以复制结构只是内部查表数据不同代码维护起来非常清晰第二function是组合逻辑的标准写法综合工具对它很友好调度和优化都比较顺畅。至于case分支过多的问题交给综合工具处理就好它会把64分支的case自动优化成LUT结构。3. 仿真验证用标准测试向量把每个环节钉死写完RTL之后进入仿真验证阶段。这个阶段最重要的不是一个testbench跑通了就算完而是要把DES的标准测试向量一条条跑过去。我在这个环节吃过不少亏有几次输出只差一位看起来“差不多对”实际上整个逻辑链路里藏着致命问题。3.1 标准测试向量的选择两个必须掌握的基准用例DES算法的标准测试向量有很多但我强烈建议先跑通下面这两个用例明文64位密钥64位期望密文64位用例A0123456789ABCDEF133457799BBCDFF185E813540F0AB405用例B全零向量000000000000000000000000000000008CA64DE9C1B123A7用例A是《应用密码学》里经典的逐轮演示数据中间每一轮的C、D、子密钥、左右半区都有公开的中间值可以核对。如果你的程序能正确跑出这个用例大概率主链路没问题。用例B则是全零下最容易核对状态的向量也常用于排查位序问题。需要特别注意字节序问题。DES标准中所有数据的最高有效位在左但很多编程语言的加密库默认使用小端字节序。用Verilog写DES时如果我们把data_in[63:0]对应到标准文档里的1,2,3,...64位那么data_in[63]就是标准位1。这个顺序一旦搞混输出结果就会错得找不着北。3.2 testbench设计与Modelsim快速仿真流程testbench设计上我习惯先写成单次激励型输入一组明文和密钥等它输出结果后自动结束。仿真通过后再改成循环激励或多组向量依次测试。这里是一个基础的testbench骨架timescale 1ns/1ps module tb_des_top; reg clk; reg rst_n; reg start; reg [1:0] mode; reg [63:0] key; reg [63:0] data_in; wire [63:0] data_out; wire busy; wire done; initial begin clk 0; forever #10 clk ~clk; // 50MHz end initial begin rst_n 0; start 0; #30 rst_n 1; #20 // 标准用例A mode 2b01; // encrypt key 64h133457799BBCDFF1; data_in 64h0123456789ABCDEF; start 1; #20 start 0; wait(done 1b1); #100; if (data_out 64h85E813540F0AB405) $display(Test A PASSED); else $display(Test A FAILED, got %h, data_out); #100; $finish; end des_top u_des_top ( .clk (clk), .rst_n (rst_n), .start (start), .mode (mode), .key (key), .data_in (data_in), .data_out(data_out), .busy (busy), .done (done) ); endmodule仿真流程上如果用的Vivado直接在Behavioral Simulation里跑就行用ModelSim的话记得先vlib work vlog *.v vsim work.tb_des_top。这里有个经验仿真时间建议给足$finish不要设太早DES完成一整个加密至少需要16个周期再加上开始前和结束后的状态切换20个时钟周期是最低限度保险起见给到30个以上。3.3 时序收敛与资源占用把设计跑在100MHz以上DES模块综合后的性能上限取决于两个地方第一个是单个轮函数组合逻辑的深度第二个是子密钥生成逻辑的时序。对于FPGA实现我实测在某中端芯片上不流水线迭代式实现大概能跑到120MHz左右资源占用不到500个LUT加一小块分布式RAM。这个成绩已经够课程设计和大多数小型应用用了。如果想跑更高频率最简单的办法是做两级流水寄存在S盒输出后插一级寄存器把轮函数拆成“E扩展异或”和“S盒P置换”两段。代价是增加了16×32位寄存器的开销但时序可以提升大约30%到40%。对于70MHz左右的项目这个优化不需要做保持代码简洁更重要。4. 高发问题与调试技巧实录写DES最容易出问题的其实就几类这里把高发问题、现象和解决办法整理成一个速查表方便大家在调试时快速定位。常见问题典型现象原因分析排查与解决输出与标准向量完全不一致密文全错毫无规律初始置换IP或逆初始置换IP写错逐位核对IP表防止抄错位置从某轮开始数据错乱前几轮对后面全乱密钥调度循环左移位数搞错核对第1、2、9、16轮的移位数是否为1位输出差一位或者个别字节错像是错的又好像接近S盒行/列索引取值搞错S盒的6位输入首尾两位决定行号中间4位决定列号仿真正常综合后变慢/卡死综合报告出现大量告警case分支覆盖不全、产生latch给case补default分支加密输出对但解密不对解密输出和明文不一致子密钥使用顺序没有反过来检查mode逻辑解密时subkey[0]要换成subkey[15]4.1 最容易踩的坑位序和字节序问题位序和字节序是DES调试中最隐蔽也最打击人的问题。很多初学者在写E扩展时直接把R[31:28]接到输出高位自己觉得很自然但标准文档里的位数定义是从1到64、从左到右的。这意味着如果你的RTL定义data_in[63]是标准位1那么E扩展的高6位应该对应标准位32、1、2、3、4、5映射到Verilog向量上就是R[0], R[31], R[30], R[29], R[28], R[27]。这种反直觉的映射经常让人头皮发麻。我的个人习惯是写一个wire [63:0] in_std先做一次位反转或者映射拼接把输入转换到和标准表一致的位序然后再用标准表的1、2、3来写逻辑。这样虽然多了一层wire但后面查错时心理负担小很多。4.2 Latch告警和初始化状态问题仿真和综合如果出现意外结果首先要看告警。常见的一种是“inferring latch”原因是always块里某个条件分支没有赋值。DES里用了大量组合逻辑一个if缺了else综合工具就会推断出锁存器结果自然不对。另一种情况是寄存器未复位。DES算法内部那些存C、D、左右半区的寄存器如果复位信号没有全部接到rst_n上仿真时初始值是X一路异或下去全是X。最好的做法是顶层模块例化时检查所有子模块的复位端口是否连通或者干脆在顶层用generate来例化减少手写出错概率。4.3 关于DES安全性的一点实话和后续扩展建议最后聊一点实话。DES早就不适合在真实业务场景里做加密了56位密钥在现代计算能力下暴力搜索完全可行而且学术界也有很多成熟的攻击手段。现在各大加密库默认都是AES或国密算法DES更多的意义在于教学演示和理解Feistel结构的历史价值。如果是做毕业设计或课程设计完全可以在DES基础上扩展成三重DES密钥长度提升到112位或168位安全性好很多同时硬件结构几乎不变只需要把DES模块例化三次并调整密钥拼接顺序。另外如果想继续往硬件方向深入下一步很适合做AES。AES的S盒实现方法和DES的S盒完全不是一个层级它需要在有限域上做乘法逆元这对Verilog编码能力是很好的进阶训练。而且AES的字节替换和列混合混洗操作也比DES更有挑战性。从DES到AES是一条非常顺滑的学习路径。最后说一下我个人的体会DES这个项目虽然老但每一遍重写都会有新的理解。当年我第一次写的时候被S盒的64个case分支搞得焦头烂额后来学会用function封装代码清爽了不止一个档次再后来理解了位序映射的重要性才算真正把DES吃透了。如果你也在用Verilog写DES建议动手前先把IP、FP、E、P四张表打印出来放旁边对照着写这样能少走很多弯路。本文还有配套的精品资源点击获取
返回列表