Verilog实现DES加密算法:FPGA RTL设计、仿真与排坑指南
简介基于Verilog的DES加密算法实现包面向FPGA开发者和信息安全方向学习者演示如何用硬件描述语言在可编程逻辑器件上搭建经典对称加密链路。资源共134个文件压缩后约409KB其中8个v源码文件对应DES核心模块、顶层模块与仿真测试平台8个mif文件用于初始化存储或查表其余为Quartus工程配置、综合报告和备份记录便于完整还原工程环境。已有1063人学习下载。内容覆盖初始置换、轮函数、S盒、P盒以及密钥生成等关键步骤仿真文件可对照已知明文和密钥验证加解密结果。借助FPGA并行处理能力该设计适合实时加密场景也可作为后续集成AES或其他安全算法的起点帮助开发者快速掌握硬件加密实现与调试方法。 最近在整理加密相关的FPGA实验发现用Verilog实现DES加密算法仍然是个很经典的练习项目。不管是数字IC设计入门、FPGA课程设计还是准备面试一版能在Modelsim里跑通的DES加密RTL代码含金量都相当高。这篇博文我从算法拆解、RTL设计、仿真验证到排坑经验完整讲一遍我的实现过程。1. 整体设计思路为什么用Verilog实现DES怎么拆架构1.1 算法选型背景DESData Encryption Standard虽然是老算法放在今天安全性已经不太够看了——56位密钥在现代算力面前暴力破解并非不可能实际工程里更推荐AES或者3DES。但是从学习和数字电路设计的角度看DES依然是绝佳的入门素材。它的Feistel结构非常规整轮函数重复度高置换表都是固定查表逻辑天然适合用硬件描述语言来实现。用Verilog做DES加密核心价值在于三点第一是理解密码算法在硬件上如何落地比如S盒怎么用组合逻辑实现、轮密钥怎么同步生成第二是训练状态机和时序控制能力DES需要16轮迭代必然要用有限状态机来调度第三是直接对标工程实践比如SPI/I2C接口加密、DDR存储加密都可以复用这套DES核心模块。如果你刷过“轮询仲裁器”、“SPI协议”、“按键消抖”这些Verilog练习DES就是一个把这些基本功串起来的综合项目。1.2 模块划分方案我设计的时候没有把DES写成一个巨大的单模块而是拆分成了四个部分顶层控制器负责接收明文和密钥启动加密流程输出密文用状态机驱动整个流程。密钥扩展模块从64位初始密钥中提取56位有效密钥生成16轮48位子密钥。轮函数模块执行扩展置换、与子密钥异或、S盒替换、P置换。S盒模块8个4进4出的查表组合逻辑。这种拆分的好处是每个模块都可以单独仿真验证调试的时候能快速定位问题。比如S盒写错了单独测S盒模块就能暴露轮密钥算错了单独拉出密钥扩展的波形就能对比。2. DES算法核心细节与硬件映射2.1 一轮Feistel结构到底在做什么DES的核心结构是Feistel网络64位数据分成左32位L和右32位R每一轮做的事情是L_new RR_new L ^ F(R, K)其中F函数就是DES的轮函数也是整个算法里最有意思的部分。F函数分四步第一步把32位的R做扩展置换E表扩展成48位。这一步的唯一目的就是让R和48位的子密钥能按位异或。第二步扩展后的48位和这一轮的48位子密钥异或。第三步异或结果分成8组每组6位输入到8个S盒中。每个S盒把6位输入映射成4位输出具体映射规则就是S盒表。这里要注意S盒的6位输入中最高位和最低位组成行号00到11中间4位组成列号0000到1111查表得到4位输出。第四步8个S盒输出合并成32位再做一次P置换这个P置换是一个简单的位重排。如果你用生活化类比来理解E扩展是“扩宽处理通道”异或是“掺入密钥”S盒是“非线性揉捏”——这一套组合构成了DES的混淆和扩散。2.2 密钥扩展的细节初始密钥是64位但实际上只有56位参与运算另外8位是奇偶校验位每个字节的第8位。密钥扩展流程第一步把64位密钥做PC-1置换剔除8个校验位得到56位。第二步56位分为C028位和D028位。第三步每轮把C和D分别左移1位或2位左移位数由移位表决定。第1、2、9、16轮左移1位其余轮左移2位。第四步将移位后的56位连接做PC-2置换得到48位子密钥K1到K16。常规的做法是先把密钥扩展做完把16轮子密钥存到寄存器数组里然后再开始16轮加密迭代。也有节省资源的在线生成方式——每一轮边用边算但那种实现需要来回存储C和D的中间状态状态机复杂度上升我觉得对于学习项目不值得。FPGA上寄存器够用的话预计算16个子密钥是更清晰的做法。2.3 以状态机为核心的时序设计前面提到16轮迭代要状态机调度我的状态划分是这样的IDLE空闲状态等待start信号拉高。KEY_EXPAND如果采用预计算方式这个状态拉高密钥扩展模块的使能信号等待扩展完成。ROUND_0到ROUND_15每一轮用一个状态或者用一个计数器在ROUND状态里循环16次。我采用的是后一种方案计数器循环节省状态数量。OUTPUT输出密文。每轮操作需要几个时钟周期取决于你是用组合逻辑做轮函数还是用寄存器暂存中间结果。我在设计时轮函数是纯组合逻辑一轮只需要一个时钟周期就能完成。加上密钥预计算的时间整个加密耗时大约20个周期对于学习和验证完全够用。2.4 对DES安全性的简单说明热搜里有个问题很直接“DES有被攻破吗”。我的看法是算法层面DES没有被数学方法彻底攻破但56位密钥空间今天已经不安全了。1998年电子前沿基金会用专用设备在56小时内破解过DES如今GPU集群可能几分钟就能暴力穷举。所以DES只适合学习、兼容旧系统、或者对安全性要求不高的场合。做项目时如果需求方问起直接推荐AES或国密SM4。3. 核心代码实现从顶层到子模块的完整写法3.1 顶层模块接口设计module des_top( input wire clk, input wire rst_n, input wire start, input wire [63:0] plaintext, input wire [63:0] key, // 64bit key包含8bit校验位 output reg [63:0] ciphertext, output reg done );顶层模块基本就是串起整个流程。start是外部触发信号拉高一个周期开始加密。plaintext和key在start拉高时被锁存后续流程中使用内部寄存器防止外部数据变化影响加密过程。done拉高时ciphertext有效外部可以在done上升沿读取密文。3.2 密钥扩展模块module key_expand( input wire clk, input wire rst_n, input wire en, input wire [63:0] key_in, output reg key_done, output reg [47:0] subkeys [15:0] // 16轮子密钥 );这个模块的核心操作是PC-1、循环左移、PC-2。PC-1和PC-2都是固定置换在Verilog里最直接的方式是用assign拼接或for循环重排也可以用function封装。// PC-1置换64bit - 56bit wire [55:0] perm_key; assign perm_key[55] key_in[56]; // ... 按PC-1表逐位映射循环左移的子模块可以用简单的移位拼接实现// C寄存器28bit循环左移1位 C {C[26:0], C[27]}; // C寄存器28bit循环左移2位 C {C[25:0], C[27:26]};3.3 轮函数模块轮函数内部包括E扩展、异或、S盒、P置换。我会把S盒单独拉出来写因为S盒是纯查表逻辑每个S盒16×4的存储结构。以S盒为例写法如下function [3:0] s_box1; input [5:0] data; reg [1:0] row; reg [3:0] col; begin row {data[5], data[0]}; // 最高位和最低位 col data[4:1]; // 中间4位 case ({row, col}) 6b000000: s_box1 4d14; // S1表内容逐行列写入 // ... 完整16x4映射 endcase end endfunction实际工程中8个S盒最好用case语句分别实现或者用一个多维存储reg [3:0] sbox_mem [7:0][15:0][3:0]统一查表。前者的好处是综合时每个S盒独立时序清晰后者的好处是代码量少。我倾向于拆开写排错方便。3.4 状态机主控代码localparam IDLE 4d0; localparam KEY_EXPAND 4d1; localparam ENCRYPT 4d2; localparam OUTPUT 4d3; reg [3:0] state; reg [3:0] round_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; round_cnt 4d0; done 1b0; end else begin case (state) IDLE: begin done 1b0; if (start) begin L_reg plaintext[63:32]; R_reg plaintext[31:0]; state KEY_EXPAND; end end KEY_EXPAND: begin if (key_done) state ENCRYPT; end ENCRYPT: begin if (round_cnt 4d15) begin state OUTPUT; end else begin round_cnt round_cnt 1b1; end end OUTPUT: begin ciphertext {R_reg, L_reg}; // 最后一轮交换后需再交换一次 done 1b1; state IDLE; end endcase end end这段代码主要是在控制加密的节奏。要注意最后一轮输出有个经典坑DES在16轮结束后左右两半还需要再交换一次再经过逆初始置换IP^-1才是密文。很多初学者在这里漏掉交换或漏掉逆置换。3.5 Testbench与仿真验证仿真验证在所有Verilog项目中都是重中之重。DES的testbench我建议分两步走第一步跑一条已知明文。比如用标准测试向量明文01123456789abcde十六进制密钥133457799bbcdff1已知密文是85e813540f0ab405。这个向量是DES标准文档里的可以直接用来验证整体流程对不对。第二步跑批量随机向量。用$random生成若干组明文和密钥用Python的pycryptodome库离线算出标准密文然后逐组比对RTL输出。跑100组随机向量基本上任何时序或状态控制的问题都能暴露出来。Testbench中关键的一行initial begin // 送入明文和密钥 plaintext 64h0123456789abcdef; key 64h133457799bbcdff1; start 1b1; #20 start 1b0; // 等待done信号 (posedge done); if (ciphertext 64h85e813540f0ab405) $display(Test PASSED); else $display(Test FAILED, expected 85e813540f0ab405, got %h, ciphertext); end如果你搜索过“modelsim如何仿真verilog文件”或“verilog仿真数据存成文件”会发现很多技巧。比如把密文结果写到文本文件里integer fd; initial fd $fopen(cipher_out.txt, w); always (posedge done) begin $fwrite(fd, %h\n, ciphertext); end这在你批量比对的时候特别好用丢给Python脚本做自动diff。4. 常见问题与排查技巧实录4.1 仿真结果不对八成是置换表从1开始还是从0开始的问题我调试DES时最大的坑就是置换表位序。DES标准文档的置换表是从1开始计位的而Verilog的数组下标是0开始。比如IP表第一个数是58意思是将明文的第58位搬到第1位。在Verilog中写assign时必须写成input_data[57]赋值给output_data[0]这里的57就是58减1。如果中间有任何一个置换表位序搞错结果必然错误。排查方法是逐模块验证先用Python脚本实现相同的置换逻辑把关键词拿去对比网上现成的开源DES代码确认置换位序理解无误再写RTL。4.2 最后一轮向左交换的细节DES在Feistel最后一轮不会交换左右两半因为交换后两轮迭代变成了一个恒等变换让加密和解密可以共用一个结构。而标准输出前要恢复交换后的输出再把左右拼接后过逆初始置换。通俗地说16轮迭代结束后拿到的L16和R16输出查表时用的顺序是{R16, L16}不是在状态机里简单地把L和R原样送到输出。4.3 状态机卡在某个状态不动这种问题一般出在使能信号没使能或者握手信号没拉低。比如KEY_EXPAND状态里key_expand模块只有当en1时才工作如果en信号在start拉低后也跟着拉低了密钥扩展模块计算到一半停止key_done永远不拉高状态机就卡死了。处理方法是让en信号保持在start拉高后的整个KEY_EXPAND状态期间有效或者设计成key_done拉高后自动清除en。4.4 Modelsim启动报错热搜里有一条“fatal error in launcher: unable to create process using”这其实是典型的Modelsim和Python或其它软件的环境变量冲突问题常见于Windows系统。排查思路是查看环境变量PATH里是否有多条Modelsim路径或者bin目录配置错误直接修复PATH即可。如果只是仿真工程本身跑不起来优先检查modelsim.ini工程文件路径和库路径是否匹配。4.5 对比处理器实现时的性能参考在FPGA上实现DES速度取决于主频和算法展开方式。基础16轮串行状态机在50MHz时钟下大约需要400ns完成一次加密如果做流水线展开可以把吞吐率提到100Mbps以上。比较激进的方案是16轮全展开、中间插流水寄存器这样每个时钟周期都能稳定输出一个64位加密块。对于课程设计来说串行状态机已经完全够用如果追求更漂亮的资源报告和时序报告流水线展开值得一写。5. 验证脚本和自动化比对流程5.1 生成标准密文脚本用Python生成随机测试向量的思路非常简单from Crypto.Cipher import DES import os key os.urandom(8) data os.urandom(8) des DES.new(key, DES.MODE_ECB) cipher des.encrypt(data) print(key.hex(), data.hex(), cipher.hex())跑上几组把结果整理成一个文本文件用$readmemh或者文本解析的方式读入testbench一组一组比对。需要注意DES模式下明文和密钥都要8字节对齐刚好是64位和硬件接口完全匹配。5.2 Verilog任务封装比对流程task check_des; input [63:0] test_plain; input [63:0] test_key; input [63:0] expect_cipher; begin plaintext test_plain; key test_key; start 1b1; #10 start 1b0; wait(done); if (ciphertext expect_cipher) $display(PASS: plain%h key%h cipher%h, test_plain, test_key, ciphertext); else $display(FAIL: plain%h key%h exp%h got%h, test_plain, test_key, expect_cipher, ciphertext); end endtask有了这个任务测试代码会清爽很多可读性也强。跑上百条向量就是在initial块里逐行调用任务而已。5.3 防盗链验证思路解密链路闭环严格来说只验证加密方向还不充分。因为DES是对称算法同一个密钥下解密是加密的逆过程所以可以再加一步用加密模块的结构逆推解密模块然后在testbench里先加密后解密比对原始明文。这样整个链路就闭环了对状态机设计也是一种反推验证。如果解密后的明文和最初明文不一致问题往往出在轮函数或子密钥顺序上。经验小结DES这个项目我在不同时期写过三轮。第一轮是在校期间做课程设计只顾着跑通标准向量第二轮工作了做Cryptography相关的FPGA预研开始关注代码风格、模块复用和时序收敛第三轮是给团队带新人索性做了一套带自动比对脚本的实验代码。三轮下来最大的变化是不再追求一次写出一大坨RTL而是先画状态流、再拆小模块、最后逐模块仿真通过后再组装。这套方法论在AES、SM4上同样适用甚至在实现Cache、UART、SPI Slave这些常规数字逻辑时也一样好使。如果你正在做类似的Verilog加密项目可以按这个顺序推进先跑通单一标准向量再做随机向量批量回归最后加一个解密闭环验证。每一步都稳了再开始折腾优化面积还是优化吞吐率的问题。DES虽然老但把它的轮函数、置换表、状态机捋顺了数字IC的综合和时序分析功底会扎实不少。本文还有配套的精品资源点击获取