第4章:AES加密引擎在FPGA中的实现

各位同学,今天我们来聊聊AES加密引擎在FPGA上的落地。说实话,AES算法本身并不复杂,但要在FPGA上把它跑得又快又安全,这里面的门道可不少。我个人习惯把AES引擎拆成三块来看:核心运算单元、密钥扩展模块、还有模式控制逻辑。咱们一个一个说。

4.1 AES-128/256硬件加速器设计

先讲核心运算单元。AES-128和AES-256的区别,说白了就是密钥长度不同,轮数也不同。128是10轮,256是14轮。我在项目中遇到过有人直接把128的代码改成256,结果跑出来全是错的——因为轮常数和密钥调度算法都不一样。

硬件加速器的设计,我建议采用流水线结构。为什么?因为AES的每一轮运算其实很规整:SubBytes、ShiftRows、MixColumns、AddRoundKey。你可以把每一轮做成一个流水级,这样吞吐量能翻好几倍。但要注意,流水线深度越深,latency越大。如果你的应用是存储加密,latency无所谓;但如果是实时通信,就得权衡一下了。

核心设计要点:

  • S-Box实现:可以用查找表(LUT),也可以用组合逻辑。LUT简单但占资源,组合逻辑省资源但时序难做。我一般用Block RAM做S-Box,速度够快,还不占LUT。
  • MixColumns优化:这个模块可以用XOR树实现,别用乘法器。AES的域乘法在GF(2^8)上,用XOR和移位就能搞定。
  • 密钥加:就是简单的XOR,没什么好说的。但要注意,密钥加是在每一轮开始和结束时都要做的。

来看一段简化的AES-128单轮运算的Verilog代码:

module aes_round (
    input  [127:0] state_in,
    input  [127:0] round_key,
    output [127:0] state_out
);
    wire [127:0] sub_out, shift_out, mix_out;
    
    // SubBytes
    genvar i;
    generate
        for (i = 0; i < 16; i = i + 1) begin
            sbox u_sbox (
                .addr(state_in[8*i +: 8]),
                .dout(sub_out[8*i +: 8])
            );
        end
    endgenerate
    
    // ShiftRows
    assign shift_out = {sub_out[127:120], sub_out[87:80], sub_out[47:40], sub_out[7:0],
                        sub_out[95:88], sub_out[55:48], sub_out[15:8], sub_out[103:96],
                        sub_out[63:56], sub_out[23:16], sub_out[111:104], sub_out[71:64],
                        sub_out[31:24], sub_out[119:112], sub_out[79:72], sub_out[39:32]};
    
    // MixColumns (简化,实际需要GF乘法)
    assign mix_out = shift_out; // 这里用占位,实际要写XOR树
    
    // AddRoundKey
    assign state_out = mix_out ^ round_key;
endmodule

嗯,这里要注意,MixColumns我用了占位符。实际项目中,你得把每一列的4个字节做矩阵乘法。我曾经在这里踩过坑——直接用乘法器,结果时序跑不到200MHz。后来改成XOR树,轻松上300MHz。

4.2 密钥扩展模块

密钥扩展,说白了就是把用户输入的密钥,扩展成每一轮需要的轮密钥。AES-128需要11个轮密钥(第0轮到第10轮),AES-256需要15个。

我建议把密钥扩展做成独立的模块,不要和加密引擎混在一起。为什么?因为有些模式(比如GCM)需要提前知道所有轮密钥,你如果边加密边扩展,时序会很难看。

我的经验:密钥扩展可以用一个状态机来控制。每次时钟上升沿,生成一个轮密钥,存入BRAM。这样加密引擎直接从BRAM读轮密钥,互不干扰。我曾经在一个项目里把密钥扩展和加密引擎做在同一个always块里,结果调试了三天才发现是时序冲突。

密钥扩展的核心逻辑:

// AES-128密钥扩展伪代码
// 输入:key[127:0]
// 输出:w[0..43] (每32位一个字,共44个字)
w[0..3] = key[127:0];
for i = 4 to 43:
    if (i % 4 == 0):
        temp = SubWord(RotWord(w[i-1])) ^ Rcon[i/4];
        w[i] = w[i-4] ^ temp;
    else:
        w[i] = w[i-4] ^ w[i-1];

这里有个细节:RotWord是循环左移一个字节,SubWord就是查S-Box。Rcon是轮常数,AES-128的Rcon从0x01开始,每轮乘2(在GF(2^8)上)。

警告:千万别把Rcon算错了!我见过有人直接用十进制乘法,结果Rcon算出来全是错的。记住,这是在GF(2^8)上的乘法,不是普通整数乘法。AES标准里Rcon的值是固定的:0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36。

4.3 ECB/CBC/GCM模式选择

模式选择,说白了就是怎么把AES这个“黑盒子”用起来。三种模式各有各的适用场景,我分别说一下。

ECB模式

最简单,也最不安全。每个16字节块独立加密。我在项目中几乎不用ECB,除非是测试向量验证。为什么?因为同样的明文块会得到同样的密文块,这会给攻击者提供模式分析的机会。你想想看,如果一张图片用ECB加密,轮廓还能看出来,那加密还有什么意义?

CBC模式

比ECB安全多了。每个明文块先和前一个密文块XOR,再加密。需要一个初始向量(IV)。CBC的缺点是加密不能并行,因为每个块依赖前一个块。但解密可以并行。我建议在存储加密场景用CBC,比如加密SD卡里的文件。

GCM模式

这是我最推荐的模式。GCM = CTR模式 + GMAC认证。它既能加密,又能做完整性校验。说白了,就是防止别人篡改你的密文。GCM的优点是加密和解密都可以并行,而且不需要填充(padding)。

模式对比表:

模式 并行加密 并行解密 认证 是否需要填充
ECB
CBC
GCM

GCM的实现稍微复杂一点,因为它需要做GHASH(伽罗华域乘法)。但别怕,GHASH本质上就是XOR和移位,用硬件实现非常快。我建议把GHASH做成独立的流水线,和AES引擎并行跑。

避坑指南:我曾经在GCM模式里犯过一个低级错误——IV重复使用。GCM的IV(其实是nonce)绝对不能重复,否则攻击者可以恢复出认证密钥H。记住,每次加密都要用不同的nonce,哪怕只差一个bit都行。

好了,这一章的内容就到这里。AES引擎的设计,说白了就是平衡速度、面积和安全性。我个人建议先从ECB模式开始验证,确保AES核心正确,再逐步加上CBC和GCM。下一章我们会讲密钥管理,那是另一个容易踩坑的地方。