第三章:FPGA身份认证——物理不可克隆函数(PUF)原理与实现

大家好,欢迎来到第三章。今天聊的话题很有意思——物理不可克隆函数,简称PUF。说实话,我第一次接触这个概念时,觉得这名字起得挺玄乎的。什么叫「不可克隆」?芯片还能有自己的「指纹」?

嗯,答案是:能。而且这个指纹,是芯片出厂那一刻就刻在骨子里的。

3.1 什么是物理不可克隆函数?

先讲个我自己的经历。几年前我负责一个金融终端的项目,客户要求每台设备必须有唯一的身份标识,而且不能被破解。传统的做法是往Flash里烧一个ID,但问题来了——只要有人能读出Flash,这个ID就暴露了。更麻烦的是,攻击者可以把这个ID复制到另一台设备上,冒充合法终端。

PUF就是来解决这个问题的。它的核心思想很简单:利用芯片制造过程中不可避免的物理差异,生成一个独一无二的「指纹」。这个指纹不需要存储,每次上电都能重新生成。你想想看,攻击者就算拿到芯片,也没法复制这个指纹——因为每颗芯片的物理特性都不一样。

核心要点:PUF不是函数,而是一种物理结构。它利用工艺偏差(比如晶体管的阈值电压差异、金属线的延迟差异)来产生随机但稳定的输出。

说白了,PUF就是一个「挑战-响应」系统。你给它一个输入(挑战),它根据自身的物理特性产生一个输出(响应)。同一个挑战,在不同芯片上得到的响应完全不同。而且,这个响应是不可预测的——即使你知道制造工艺的所有参数,也没法提前算出某颗芯片的响应。

3.2 PUF在FPGA中的实现方式

FPGA上实现PUF,我个人最常用的是两种结构:基于SRAM的PUF和基于环形振荡器的PUF。咱们一个一个说。

3.2.1 SRAM PUF

这个实现起来最简单。FPGA里的Block RAM或者LUT构成的SRAM单元,在上电瞬间会进入一个不确定的状态。因为每个存储单元的晶体管阈值电压有细微差异,有的单元倾向于读「0」,有的倾向于读「1」。这个初始状态就是PUF的响应。

我在项目中用过Xilinx的Zynq系列做SRAM PUF。代码其实很简单:

// SRAM PUF 上电读取示例
// 注意:必须在配置完成后立即读取,不能先写后读
module sram_puf (
    input  wire        clk,
    input  wire        rst_n,
    output reg  [31:0] puf_data
);

    reg [31:0] sram_cell [0:1023];  // 模拟SRAM阵列
    
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // 上电复位后立即读取SRAM初始状态
            puf_data <= sram_cell[0];  // 实际项目中需要读取多个地址
        end
    end
endmodule

避坑指南:我曾经踩过一个坑——FPGA配置完成后,如果先对BRAM执行了初始化操作,PUF的原始状态就被覆盖了。正确的做法是在配置完成后、任何写操作之前,立刻读取BRAM的内容。另外,温度变化会影响SRAM PUF的稳定性,建议配合纠错码(ECC)使用。

3.2.2 环形振荡器PUF

另一种常见方案是环形振荡器PUF。它利用不同路径的传播延迟差异来生成响应。我在一个安全网关项目里用过这个方案,效果不错。

基本原理:在FPGA上实现两个完全相同的环形振荡器,理论上它们的频率应该一样。但因为工艺偏差,实际频率会有微小差异。通过比较两个振荡器的频率,就能产生一个比特的响应。

// 环形振荡器PUF - 单比特比较单元
module ro_puf_cell (
    input  wire        enable,
    output wire        puf_bit
);

    wire osc1_out, osc2_out;
    
    // 两个完全相同的环形振荡器
    ring_osc #(.STAGES(3)) ro1 (
        .enable(enable),
        .osc_out(osc1_out)
    );
    
    ring_osc #(.STAGES(3)) ro2 (
        .enable(enable),
        .osc_out(osc2_out)
    );
    
    // 比较频率,输出PUF比特
    freq_comparator comp (
        .clk_a(osc1_out),
        .clk_b(osc2_out),
        .result(puf_bit)
    );
    
endmodule

个人经验:环形振荡器PUF对布局布线非常敏感。我建议在实现时使用位置约束,确保两个振荡器的走线长度尽可能一致。否则,布线差异会淹没工艺偏差,导致PUF的独特性下降。

3.3 基于PUF的密钥生成

好了,现在我们有PUF了,怎么用它来生成密钥呢?

直接拿PUF的原始响应当密钥?不行。原因有两个:第一,PUF响应有噪声,同一颗芯片在不同温度下可能产生不同的响应;第二,PUF响应的比特之间可能有相关性,直接当密钥用不够安全。

正确的做法是分三步走:

  1. PUF响应采集:多次读取PUF,得到一组原始响应数据
  2. 纠错编码:使用纠错码(比如BCH码)消除噪声影响
  3. 密钥提取:通过哈希函数(比如SHA-256)将稳定的PUF响应映射为密钥

我画了一张流程图,帮你理清这个逻辑:

基于PUF的密钥生成流程 步骤1:PUF响应采集 多次读取PUF原始数据 步骤2:纠错编码 BCH码/Reed-Solomon码 步骤3:密钥提取 SHA-256哈希映射 辅助数据(Helper Data) • 纠错码的校验位 • 存储在非易失存储器中 • 不泄露密钥信息 最终输出 • 128/256位稳定密钥 • 温度/电压变化下稳定 • 芯片唯一,不可克隆 密钥只在需要时临时生成,使用后立即擦除 攻击者无法从非易失存储器中窃取密钥 🔒 硬件安全锚点

你看这个流程,核心思想就是「临时生成,用完即焚」。密钥不在Flash里存着,只在需要的时候从PUF里算出来。攻击者就算把芯片拆了,也找不到密钥存在哪里。

3.4 实际项目中的注意事项

最后,分享几个我在实际项目中积累的经验:

问题 现象 解决方案
温度漂移 PUF响应在-40°C和85°C下不一致 使用温度补偿电路,增加纠错码冗余
老化效应 芯片使用几年后PUF响应变化 定期重新注册,更新辅助数据
电压波动 供电噪声导致PUF误码率上升 片内LDO稳压,增加数字滤波
布局影响 不同布局导致PUF独特性下降 使用位置约束,保持对称性

一句话总结:PUF不是银弹,但它提供了一种「不存储密钥」的密钥管理方式。在FPGA安全设计中,PUF + 纠错码 + 哈希函数,是目前最实用的硬件身份认证方案之一。

好了,这一章的内容就到这里。PUF这个东西,说起来简单,做起来细节很多。如果你在实际项目中遇到问题,欢迎交流讨论。


公众号:蓝海资料掘金营,微信deep3321