4、证书与CA体系:X.509证书结构、证书颁发机构(CA)的信任链模型
说到固件防篡改,证书体系是绕不开的核心。说白了,你固件里放的那个签名,到底是谁签的?凭什么信它?这就得聊到X.509证书和CA的信任链了。
我刚开始做嵌入式安全那会儿,觉得证书就是个文件。后来被坑过一次——有个客户拿了个自签名的证书来,说“这多方便,省得找CA花钱”。结果产品上线后,升级包被中间人替换了,固件直接变砖。嗯,从那以后,我对证书链的敬畏心就上来了。
4.1 X.509证书长什么样?
X.509是国际电信联盟(ITU-T)定义的标准。目前最常用的是V3版本。你打开一个.pem或者.der文件,里面其实是一堆ASN.1编码的数据。但咱们不用管底层编码细节,只看它包含哪些关键字段。
| 字段 | 说明 | 我的经验 |
|---|---|---|
| 版本号 | V1、V2、V3,现在基本都用V3 | V3才支持扩展项,别用V1 |
| 序列号 | CA分配的唯一编号 | 吊销时靠它查,别重复 |
| 签名算法 | 比如sha256WithRSAEncryption | 别用SHA1,已经不安全了 |
| 颁发者 | 签发这张证书的CA名称 | 链式校验时逐级比对 |
| 有效期 | notBefore 和 notAfter | 固件里一定要检查时间 |
| 主体 | 证书持有者的身份信息 | 可以是设备ID或厂商名 |
| 公钥 | 算法+公钥值 | 这就是你验签用的东西 |
| 扩展项 | Key Usage、Basic Constraints等 | CA证书必须标记 |
| 签名值 | CA用私钥对整个证书的签名 | 防伪造的关键 |
核心要点:证书的本质,就是把一个公钥绑定到一个身份上,再由一个可信的第三方(CA)来担保这个绑定关系。
4.2 CA的信任链模型
你想想看,如果每个设备都自己签自己的证书,那跟没签有什么区别?所以我们需要一个层级结构——信任链。
典型的信任链分三层:
- 根CA(Root CA)——最顶层的信任锚点。它的证书是自签名的。我习惯把根CA的证书直接烧死在设备的只读存储区里,谁都改不了。
- 中间CA(Intermediate CA)——由根CA签发,用来给终端实体发证书。这样做的好处是:即使中间CA的私钥泄露,根CA还能撤销它,不用重新烧设备。
- 终端实体证书(End-Entity Certificate)——就是设备或者固件签名用的证书。由中间CA签发。
校验的时候,从终端证书开始,一级一级往上找颁发者,直到找到设备里预置的根证书。如果整条链都能验证通过,那就说明这个证书是可信的。
我的习惯:在固件里只放根CA证书,不放中间CA和终端证书。升级包携带完整的证书链,设备自己校验。这样根证书几乎不用更新,安全性高很多。
4.3 证书链校验流程
实际项目中,证书链校验不是简单比对几个名字就完事的。我总结了一套标准流程,你照着做基本不会出问题:
- 步骤1:解析证书链——从固件签名包里提取出证书列表,按顺序排好。终端证书在第一个,根证书在最后一个。
- 步骤2:逐级验证签名——用上一级证书的公钥,验证下一级证书的签名。说白了就是:父证验子证。
- 步骤3:检查有效期——每一张证书的当前时间必须在notBefore和notAfter之间。我遇到过设备RTC电池没电,时间回到1970年,证书校验直接失败。后来加了NTP同步才解决。
- 步骤4:检查吊销状态——通过CRL或OCSP确认证书没有被吊销。嵌入式环境里,我建议用CRL离线列表,OCSP需要联网,很多设备不具备条件。
- 步骤5:检查扩展项——CA证书必须标记Basic Constraints: CA=True。终端证书不能有CA标志。Key Usage也要对得上。
- 步骤6:确认信任锚——链的最后一站必须是设备里预置的根证书。如果根证书是自签名的,那它的颁发者就是自己,签名也是自己验自己。
曾经踩过的坑:有一次我忘了检查Basic Constraints,结果有人拿一张终端证书当CA证书用,签了一堆子证书出来。虽然签名算法都对,但权限完全越界了。从那以后,我把扩展项检查写成了硬性规则。
4.4 信任链模型SVG图
下面这张图展示了典型的3层信任链结构。你可以看到从根CA到设备证书的签发关系,以及校验时的反向验证路径。
4.5 自签名证书 vs CA签发证书
很多小团队图省事,直接用自签名证书。我个人建议:开发测试阶段可以用自签名,但量产产品必须走CA签发。为什么?
| 对比项 | 自签名证书 | CA签发证书 |
|---|---|---|
| 信任基础 | 自己信自己 | 第三方背书 |
| 吊销能力 | 无法吊销 | 支持CRL/OCSP |
| 私钥泄露风险 | 必须重新烧录所有设备 | 仅吊销泄露的证书 |
| 适用场景 | 内部测试、原型验证 | 量产产品、安全升级 |
特别注意:自签名证书没有上级CA,所以它的“颁发者”和“主体”是同一个。校验时如果发现颁发者==主体,且签名能用自己的公钥验过,那就是自签名。但千万别把自签名证书当根CA用——根CA虽然也是自签名,但它有CA扩展标记,而且整个信任链的设计是围绕它展开的。
4.6 嵌入式环境下的证书存储
讲到这里,我得说说实际部署时的一些细节。嵌入式设备的存储空间有限,证书不能随便放。
- 根证书:烧死在只读分区(如BootROM或OTP)。我习惯用DER格式,比PEM省空间。一个2048位RSA根证书大约占用1.2KB。
- 中间证书:放在固件升级包里,不预置在设备中。校验时临时加载到RAM里。
- 终端证书:同样放在升级包里。私钥绝对不能出现在固件里,必须放在安全元件(SE)或TEE中。
我曾经见过一个产品,把私钥硬编码在固件里,还加了注释“// private key, do not leak”。结果固件被反编译,私钥直接暴露。嗯,那个产品后来召回了。
省空间的小技巧:如果证书链比较长,可以只保留必要的字段。比如去掉证书里的“颁发者唯一标识”和“主体唯一标识”这两个几乎没人用的字段,能省下几十字节。但签名值、公钥、有效期这些核心字段一个都不能少。
4.7 证书链校验的代码骨架
最后,我贴一段伪代码,展示证书链校验的核心逻辑。实际项目中你可能会用mbedTLS或OpenSSL的API,但原理是一样的。
// 证书链校验伪代码
bool verify_certificate_chain(cert_chain_t *chain, cert_t *root_ca) {
// 1. 从终端证书开始,逐级向上
for (int i = 0; i < chain->count - 1; i++) {
cert_t *child = &chain->certs[i];
cert_t *parent = &chain->certs[i + 1];
// 2. 用父证书的公钥验子证书的签名
if (!verify_signature(child->tbs_cert, child->sig,
parent->public_key)) {
return false; // 签名验证失败
}
// 3. 检查有效期
if (!check_validity(child)) {
return false; // 证书过期或未生效
}
// 4. 检查CA扩展标记
if (i < chain->count - 2) { // 非终端证书必须标记为CA
if (!child->is_ca) {
return false; // 中间证书缺少CA标记
}
}
}
// 5. 最后一级必须是根证书
cert_t *last = &chain->certs[chain->count - 1];
if (!compare_cert(last, root_ca)) {
return false; // 信任锚不匹配
}
return true;
}
这段代码虽然简单,但涵盖了核心校验逻辑。你想想看,如果每一步都严格检查,攻击者想伪造一张证书几乎是不可能的——除非他拿到了CA的私钥。
好了,证书和CA体系就聊到这儿。记住一句话:信任链的强度,取决于最薄弱的那一环。而最薄弱的环节,往往是人的疏忽。