2. 物理不可克隆函数(PUF):芯片的“指纹”技术
大家好,我是老张。今天我们来聊聊PUF——物理不可克隆函数。这玩意儿说白了,就是给每颗芯片一个独一无二的“指纹”。
我在做安全芯片那几年,最头疼的问题就是密钥存储。传统方式把密钥写在Flash里,黑客总有办法读出来。后来接触到PUF,我才发现——原来密钥可以不用“存”,而是“生”出来的。
2.1 PUF原理:为什么芯片会有“个性”?
先讲个故事。你想想看,同一批晶圆,同一个光刻机,出来的芯片理论上应该一模一样对吧?
但现实不是这样的。制造过程中,掺杂浓度、氧化层厚度、线宽这些参数,都会有微小的随机偏差。这些偏差小到什么程度?纳米级别。但就是这些偏差,让每颗芯片的物理特性变得独一无二。
PUF的核心思想,就是利用这些不可控的制造偏差,生成一个唯一且不可克隆的“响应”。
PUF的三个核心特性:
- 唯一性:不同芯片,即使设计完全相同,PUF响应也不同
- 不可克隆性:即使拿到设计文件,也无法制造出完全一样的PUF
- 不可预测性:给定一个激励,你无法提前知道响应是什么
嗯,这里要注意。PUF不是加密算法,它更像一个物理“函数”。你给它一个输入(激励),它返回一个输出(响应)。但这个输出不是算出来的,而是物理特性决定的。
2.2 基于SRAM的PUF设计:最经典的实现方式
在所有PUF类型中,SRAM PUF是我用得最多的。为什么?因为它不需要额外工艺,标准CMOS就能做。
SRAM单元大家都知道,6个晶体管组成一个存储位。上电瞬间,每个单元会随机进入0或1状态。理论上概率各50%。但实际呢?由于制造偏差,有些单元天生偏向0,有些偏向1。
我在项目中遇到过一个问题:有些SRAM单元的上电值不稳定,每次都不一样。这会导致PUF响应出错。后来怎么解决的?我们加了一个“稳定位筛选”步骤。
SRAM PUF设计要点:
- 地址选择:只使用芯片内部SRAM,不要用外部存储器
- 上电时序:确保所有SRAM单元同时上电,避免顺序影响
- 稳定位筛选:多次上电测试,只保留每次都稳定的位
- 纠错码:必须配合ECC使用,因为PUF输出会有噪声
来看一个简单的SRAM PUF读取代码示例:
// SRAM PUF 上电值读取示例
#define PUF_START_ADDR 0x20000000
#define PUF_SIZE 1024 // 1024字节
uint8_t puf_response[PUF_SIZE];
void read_sram_puf(void) {
volatile uint8_t *sram_ptr = (uint8_t *)PUF_START_ADDR;
// 关键:必须在SRAM被写入之前读取
for(int i = 0; i < PUF_SIZE; i++) {
puf_response[i] = sram_ptr[i];
}
// 稳定位筛选:重复读取3次
uint8_t temp[3][PUF_SIZE];
for(int round = 0; round < 3; round++) {
// 需要先掉电再上电
power_cycle_sram();
for(int i = 0; i < PUF_SIZE; i++) {
temp[round][i] = sram_ptr[i];
}
}
// 只保留三次都一致的位
for(int i = 0; i < PUF_SIZE; i++) {
if(temp[0][i] == temp[1][i] && temp[1][i] == temp[2][i]) {
puf_response[i] = temp[0][i];
} else {
puf_response[i] = 0xFF; // 标记为不稳定位
}
}
}
⚠️ 重要提醒:
我曾经犯过一个错误——在读取PUF之前,Bootloader已经初始化了SRAM。结果读出来的全是0。记住:PUF读取必须在任何SRAM写入操作之前完成!
2.3 PUF在密钥生成中的应用:从物理指纹到安全密钥
好了,现在我们有了PUF响应。但原始响应不能直接用做密钥。为什么?两个原因:
- 噪声问题:每次上电,PUF响应可能有1-5%的位翻转
- 熵分布不均:有些位偏向0或1,不是完全随机
所以我们需要一个“密钥提取”流程。我个人习惯用下面这个架构:
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | PUF原始响应 | 从SRAM读取的上电值,约1024字节 |
| 2 | 稳定位筛选 | 去除不稳定位,保留约80%的位 |
| 3 | 熵提取 | 使用哈希函数(如SHA-256)提取均匀分布的熵 |
| 4 | 纠错编码 | 使用BCH码或Reed-Solomon码,容忍位错误 |
| 5 | 密钥派生 | 通过KDF生成最终密钥(AES-128/256) |
这里有个关键点:辅助数据(Helper Data)的存储。纠错码需要一些辅助信息来恢复密钥。这些数据可以存在Flash里,但必须保证:
- 辅助数据本身不泄露密钥信息
- 每次使用PUF时,辅助数据都要重新生成
实际项目中的密钥生成流程:
// 伪代码:PUF密钥生成
uint8_t puf_raw[1024];
uint8_t helper_data[256];
uint8_t aes_key[32];
// 1. 读取PUF
read_sram_puf(puf_raw);
// 2. 生成辅助数据(注册阶段)
generate_helper_data(puf_raw, helper_data);
// 3. 存储辅助数据到Flash
flash_write(HELPER_ADDR, helper_data, 256);
// 4. 密钥重构(每次上电)
read_sram_puf(puf_raw);
flash_read(HELPER_ADDR, helper_data, 256);
reconstruct_key(puf_raw, helper_data, aes_key);
// 5. 现在aes_key就是稳定的128位密钥
你可能会问:辅助数据存在Flash里,黑客拿到怎么办?
嗯,这个问题问得好。辅助数据本身是随机数,不包含密钥信息。它只是用来纠正PUF噪声的。即使黑客拿到辅助数据,也无法反推出密钥。这就是PUF的妙处——密钥从不出现在存储介质中。
我的经验之谈:
曾经有个客户要求密钥必须达到256位安全性。我们用了1024字节的SRAM PUF,经过筛选和熵提取后,实际可用熵只有约512位。再用SHA-256压缩到256位,安全余量很足。但要注意:PUF的熵率通常只有30-50%,设计时要留够余量。
2.4 避坑指南
最后,分享几个我踩过的坑:
- 温度漂移:PUF响应会随温度变化。我在-40°C和85°C下测过,位翻转率能差3倍。所以一定要做全温区测试。
- 老化效应:芯片用久了,PUF特性会漂移。建议每半年重新校准一次辅助数据。
- 电压敏感:供电电压波动会影响PUF稳定性。我习惯在PUF读取时用内部LDO稳压。
- 攻击防护:PUF本身抗物理攻击,但读取接口要加防护。比如用金属屏蔽层覆盖PUF区域。
好了,这一章就到这里。PUF是个很有意思的技术,它让“密钥存储”这个老大难问题有了新解法。下一章我们聊聊侧信道攻击防护,那又是另一个精彩的话题。