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设计要点:

  1. 地址选择:只使用芯片内部SRAM,不要用外部存储器
  2. 上电时序:确保所有SRAM单元同时上电,避免顺序影响
  3. 稳定位筛选:多次上电测试,只保留每次都稳定的位
  4. 纠错码:必须配合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是个很有意思的技术,它让“密钥存储”这个老大难问题有了新解法。下一章我们聊聊侧信道攻击防护,那又是另一个精彩的话题。