3. 证书链配置实战:使用 OpenSSL 生成根证书、中间证书、终端实体证书

说实话,很多搞固件安全的同行,一提到证书链就头大。觉得那是 PKI 工程师的事。但我个人认为,嵌入式工程师必须亲手走一遍这个过程。你想想看,如果连证书怎么签出来的都不清楚,怎么保证你烧进去的固件校验是安全的?

这一章,我们就用 OpenSSL 这个瑞士军刀,从零开始搭一条完整的证书链。我会把我在项目中踩过的坑、总结的习惯,都揉进去。

3.1 为什么需要三层证书链?

直接拿根证书去签固件不行吗?技术上当然可以。但我在一个量产项目上吃过亏——根证书私钥一旦泄露,整个产品线的信任体系就崩了。你只能召回所有设备,重新烧写新证书。

三层结构的好处是:

  • 根证书(Root CA):离线保存,一年用不了几次。它只用来签中间证书。
  • 中间证书(Intermediate CA):可以放在构建服务器上。万一泄露,吊销它就行,根证书还是干净的。
  • 终端实体证书(End Entity):直接签固件、签镜像。生命周期短,甚至可以每个固件版本签一个。

核心原则:根证书的私钥,永远不要出现在联网的机器上。我习惯用一台离线 Linux 虚拟机专门管根证书,用完就关机。

下面这张图,把整个信任链条画清楚了:

三层证书链信任模型 根证书 (Root CA) 自签名 · 离线保存 中间证书 (Intermediate) 由根证书签发 · 在线使用 终端实体证书 (EE) 由中间证书签发 · 签固件 固件校验时:终端证书 → 中间证书 → 根证书,逐级验证签名

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 是什么意思?
  • 固件校验时,证书链文件的拼接顺序是什么?