第三章: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响应的比特之间可能有相关性,直接当密钥用不够安全。
正确的做法是分三步走:
- PUF响应采集:多次读取PUF,得到一组原始响应数据
- 纠错编码:使用纠错码(比如BCH码)消除噪声影响
- 密钥提取:通过哈希函数(比如SHA-256)将稳定的PUF响应映射为密钥
我画了一张流程图,帮你理清这个逻辑:
你看这个流程,核心思想就是「临时生成,用完即焚」。密钥不在Flash里存着,只在需要的时候从PUF里算出来。攻击者就算把芯片拆了,也找不到密钥存在哪里。
3.4 实际项目中的注意事项
最后,分享几个我在实际项目中积累的经验:
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 温度漂移 | PUF响应在-40°C和85°C下不一致 | 使用温度补偿电路,增加纠错码冗余 |
| 老化效应 | 芯片使用几年后PUF响应变化 | 定期重新注册,更新辅助数据 |
| 电压波动 | 供电噪声导致PUF误码率上升 | 片内LDO稳压,增加数字滤波 |
| 布局影响 | 不同布局导致PUF独特性下降 | 使用位置约束,保持对称性 |
一句话总结:PUF不是银弹,但它提供了一种「不存储密钥」的密钥管理方式。在FPGA安全设计中,PUF + 纠错码 + 哈希函数,是目前最实用的硬件身份认证方案之一。
好了,这一章的内容就到这里。PUF这个东西,说起来简单,做起来细节很多。如果你在实际项目中遇到问题,欢迎交流讨论。
公众号:蓝海资料掘金营,微信deep3321