3. 证书链配置实战:使用 OpenSSL 生成根证书、中间证书、终端实体证书
说实话,很多搞固件安全的同行,一提到证书链就头大。觉得那是 PKI 工程师的事。但我个人认为,嵌入式工程师必须亲手走一遍这个过程。你想想看,如果连证书怎么签出来的都不清楚,怎么保证你烧进去的固件校验是安全的?
这一章,我们就用 OpenSSL 这个瑞士军刀,从零开始搭一条完整的证书链。我会把我在项目中踩过的坑、总结的习惯,都揉进去。
3.1 为什么需要三层证书链?
直接拿根证书去签固件不行吗?技术上当然可以。但我在一个量产项目上吃过亏——根证书私钥一旦泄露,整个产品线的信任体系就崩了。你只能召回所有设备,重新烧写新证书。
三层结构的好处是:
- 根证书(Root CA):离线保存,一年用不了几次。它只用来签中间证书。
- 中间证书(Intermediate CA):可以放在构建服务器上。万一泄露,吊销它就行,根证书还是干净的。
- 终端实体证书(End Entity):直接签固件、签镜像。生命周期短,甚至可以每个固件版本签一个。
核心原则:根证书的私钥,永远不要出现在联网的机器上。我习惯用一台离线 Linux 虚拟机专门管根证书,用完就关机。
下面这张图,把整个信任链条画清楚了:
3.2 准备工作:OpenSSL 环境与目录结构
我习惯用一个干净的目录来管理证书生命周期。每次新项目,我都会复制这个模板:
mkdir -p cert-chain/{root,intermediate,ee,archive}
cd cert-chain
# 根证书相关
mkdir -p root/{certs,crl,newcerts,private}
echo 1000 > root/serial
touch root/index.txt
# 中间证书相关
mkdir -p intermediate/{certs,crl,newcerts,csr,private}
echo 1000 > intermediate/serial
touch intermediate/index.txt
# 终端证书相关
mkdir -p ee/{certs,csr,private}
我的习惯:serial 文件从 1000 开始,而不是 01。这样在查看证书序列号时,一眼就能看出是第几批生成的证书。曾经有个同事从 01 开始,结果序列号冲突,排查了半天。
3.3 生成根证书(Root CA)
根证书是信任的锚点。它的私钥必须足够强,我至少用 4096 位 RSA。2048 位虽然目前还安全,但嵌入式设备生命周期长,三五年后呢?
先创建根证书的 OpenSSL 配置文件 root/openssl.cnf:
[ ca ]
default_ca = CA_default
[ CA_default ]
database = root/index.txt
serial = root/serial
private_key = root/private/ca.key.pem
certificate = root/certs/ca.cert.pem
new_certs_dir = root/newcerts
default_md = sha256
policy = policy_strict
[ policy_strict ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name
string_mask = utf8only
prompt = no
[ req_distinguished_name ]
countryName = CN
stateOrProvinceName = Beijing
localityName = Beijing
organizationName = SecureBoot Labs
organizationalUnitName = Root CA
commonName = SecureBoot Root CA 2025
然后生成根密钥和证书:
# 生成根私钥
openssl genrsa -out root/private/ca.key.pem 4096
chmod 400 root/private/ca.key.pem
# 生成根证书(自签名)
openssl req -config root/openssl.cnf \
-key root/private/ca.key.pem \
-new -x509 -days 7300 -sha256 \
-extensions v3_ca \
-out root/certs/ca.cert.pem
# 查看根证书内容
openssl x509 -in root/certs/ca.cert.pem -text -noout | head -20
注意:根证书有效期我设了 20 年(7300 天)。但中间证书和终端证书不要这么长。中间证书一般 5 年,终端证书 1-2 年就够了。这样即使终端证书私钥泄露,影响范围也有限。
3.4 生成中间证书(Intermediate CA)
中间证书由根证书签发。这里有个关键点:中间证书的扩展属性里必须标记为 CA,但 pathlen:0 表示它不能再签发下级 CA。这是安全边界。
中间证书的配置文件 intermediate/openssl.cnf:
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
prompt = no
[ req_distinguished_name ]
countryName = CN
stateOrProvinceName = Beijing
organizationName = SecureBoot Labs
organizationalUnitName = Intermediate CA
commonName = SecureBoot Intermediate CA 2025
生成步骤:
# 1. 生成中间证书私钥
openssl genrsa -out intermediate/private/intermediate.key.pem 2048
chmod 400 intermediate/private/intermediate.key.pem
# 2. 生成证书签名请求(CSR)
openssl req -config intermediate/openssl.cnf \
-key intermediate/private/intermediate.key.pem \
-new -sha256 \
-out intermediate/csr/intermediate.csr.pem
# 3. 用根证书签发中间证书
openssl ca -config root/openssl.cnf \
-extensions v3_intermediate_ca \
-days 1825 -notext -md sha256 \
-in intermediate/csr/intermediate.csr.pem \
-out intermediate/certs/intermediate.cert.pem
# 4. 验证中间证书
openssl verify -CAfile root/certs/ca.cert.pem \
intermediate/certs/intermediate.cert.pem
嗯,这里要注意。如果验证报错,八成是 v3_intermediate_ca 扩展没定义。你需要在根证书的配置文件里加上:
[ v3_intermediate_ca ]
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
3.5 生成终端实体证书(End Entity)
终端证书才是真正用来签固件的。它不能是 CA,不能签发其他证书。我习惯在证书的 extendedKeyUsage 里明确写上 codeSigning,这样固件校验时可以检查用途是否匹配。
# 生成终端证书私钥
openssl genrsa -out ee/private/ee.key.pem 2048
chmod 400 ee/private/ee.key.pem
# 生成 CSR
openssl req -new -sha256 \
-key ee/private/ee.key.pem \
-subj "/C=CN/ST=Beijing/O=SecureBoot Labs/CN=firmware-signer-001" \
-out ee/csr/ee.csr.pem
# 用中间证书签发终端证书
openssl ca -config intermediate/openssl.cnf \
-extensions v3_ee \
-days 365 -notext -md sha256 \
-in ee/csr/ee.csr.pem \
-out ee/certs/ee.cert.pem
中间证书的配置文件里,需要定义 v3_ee 扩展:
[ v3_ee ]
basicConstraints = critical, CA:false
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, codeSigning
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
3.6 构建完整的证书链文件
在固件校验时,设备需要拿到完整的证书链,才能逐级验证。我习惯把根证书、中间证书、终端证书拼接成一个 chain.pem 文件:
# 顺序:终端证书 → 中间证书 → 根证书
cat ee/certs/ee.cert.pem \
intermediate/certs/intermediate.cert.pem \
root/certs/ca.cert.pem > chain.pem
# 验证证书链
openssl verify -CAfile root/certs/ca.cert.pem \
-untrusted intermediate/certs/intermediate.cert.pem \
ee/certs/ee.cert.pem
验证结果应该是:ee/certs/ee.cert.pem: OK
如果报错,别慌。我曾经遇到过一次,原因是中间证书的 basicConstraints 没设 pathlen:0,导致 OpenSSL 认为中间证书还能再签 CA,验证逻辑就乱了。
3.7 证书链的常见问题与避坑
| 问题 | 现象 | 解决办法 |
|---|---|---|
| 证书顺序错误 | 设备校验时提示证书链不完整 | chain.pem 必须从终端证书开始,根证书结尾 |
| 密钥长度不匹配 | 签名验证通过,但固件校验失败 | 确保签名算法与密钥长度一致(如 SHA256 + RSA2048) |
| 扩展属性缺失 | OpenSSL 验证时报 X509_V_ERR | 检查 keyUsage 和 extendedKeyUsage 是否设置 |
| 时间不同步 | 证书提示已过期或未生效 | 设备 RTC 时间必须在证书有效期内 |
我曾经踩过的坑:有一次把根证书的 keyUsage 设成了 digitalSignature 而不是 keyCertSign。结果根证书签出来的中间证书,OpenSSL 死活不认。折腾了两小时才发现是根证书自己少了签发证书的权限。自签名证书也得守规矩啊。
3.8 小结
这一章我们亲手搭了一条完整的证书链。从根证书到中间证书,再到终端实体证书,每一步都跑通了。你现在应该能回答这几个问题了:
- 为什么根证书要离线保存?
- 中间证书的
pathlen:0是什么意思? - 固件校验时,证书链文件的拼接顺序是什么?