3. OpenSSL 工具速览与 CA 搭建

说实话,很多搞 MQTT 的人,最后都栽在证书上。不是协议多难,而是 OpenSSL 这把瑞士军刀,太多人只会用其中一两招。今天我们就把它摊开来看清楚。

我个人习惯,不管项目多急,都会先搭一套私有根 CA。为什么?因为你想想看,生产环境里如果每台设备都去申请付费证书,成本高不说,管理起来也头疼。自己搭一个 Root CA,一劳永逸。

3.1 OpenSSL 核心命令速览

OpenSSL 是个命令行工具箱。我把它分成三类,你记牢这三类就够了:

类别 常用命令 一句话说明
密钥生成 genrsa / ecparam 生成 RSA 或 ECC 私钥
证书操作 req / x509 / ca 生成 CSR、自签证书、颁发证书
验证与查看 verify / s_client 验证证书链、测试 TLS 握手

我在项目中遇到过最典型的场景:现场工程师拿着一个 .pem 文件,不知道是公钥还是私钥。其实用一行命令就能看清——openssl x509 -in file.pem -text -noout,如果是证书,会打印出完整信息;如果是私钥,会报错。这招很实用。

小技巧: 不确定文件格式时,先用 openssl asn1parse -in file.pem 看看 ASN.1 结构,能快速判断是密钥、证书还是 CSR。

3.2 配置文件解析——openssl.cnf

很多人觉得 OpenSSL 的配置文件又臭又长。嗯,确实。但你不需要全看懂,只需要抓住几个关键段。

我一般会准备一个精简版的配置文件,放在项目目录下。核心结构是这样的:

[ req ]
default_bits        = 2048
distinguished_name  = req_distinguished_name
string_mask         = utf8only
prompt              = no

[ req_distinguished_name ]
countryName             = CN
stateOrProvinceName     = Beijing
localityName            = Haidian
organizationName        = MyIoT
organizationalUnitName  = MQTT Gateway
commonName              = mqtt-gateway.local
emailAddress            = admin@myiot.com

[ v3_ca ]
basicConstraints       = critical, CA:TRUE
keyUsage               = critical, keyCertSign, cRLSign
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer

[ v3_req ]
basicConstraints       = CA:FALSE
keyUsage               = critical, digitalSignature, keyEncipherment
extendedKeyUsage       = serverAuth, clientAuth
subjectAltName         = @alt_names

[ alt_names ]
DNS.1 = mqtt-gateway.local
DNS.2 = localhost
IP.1  = 192.168.1.100

这里我重点说三个坑:

  • commonName:以前浏览器还认 CN,现在 Chrome 只认 SAN。但 MQTT 客户端很多还依赖 CN,所以我两个都填。
  • CA:TRUE vs CA:FALSE:根证书必须写 CA:TRUE,服务器证书必须写 CA:FALSE。写反了,整个证书链就废了。
  • subjectAltName:MQTT 网关如果通过 IP 连接,一定要加 IP.1。我曾经遇到一个项目,所有配置都对,就是忘了加 SAN,结果客户端一直报证书验证失败。查了整整一下午。
注意: 生产环境中,default_bits 建议用 4096。虽然生成慢一点,但安全性更高。IoT 设备如果性能受限,至少也要 2048。

3.3 搭建私有根 CA(Root CA)

好,现在我们来动手。搭建私有根 CA,说白了就是三步:生成密钥、自签证书、准备目录结构。

3.3.1 准备目录结构

我习惯用这个结构,清晰又好管理:

my-ca/
├── certs/          # 存放已颁发的证书
├── crl/            # 证书吊销列表
├── newcerts/       # OpenSSL 自动生成的新证书
├── private/        # CA 私钥(权限 600)
├── index.txt       # 证书数据库
└── serial          # 序列号文件

创建命令:

mkdir -p my-ca/{certs,crl,newcerts,private}
chmod 700 private
touch my-ca/index.txt
echo 1000 > my-ca/serial

这里有个细节:serial 文件初始值建议从 1000 开始,这样以后颁发的证书编号整齐,方便追溯。

3.3.2 生成根 CA 密钥

openssl genrsa -aes256 -out my-ca/private/ca.key.pem 4096
chmod 400 my-ca/private/ca.key.pem

为什么要加 -aes256?因为根 CA 私钥是整条信任链的命根子。一旦泄露,你颁发的所有证书都得作废。加个密码保护,至少多一层防线。

关键点: 根 CA 私钥的密码一定要记牢!我见过有人把密码存在桌面上的 txt 文件里,那跟没加密有什么区别?建议用密码管理器,或者物理保管。

3.3.3 自签根证书

openssl req -x509 -new \
  -key my-ca/private/ca.key.pem \
  -out my-ca/certs/ca.cert.pem \
  -days 3650 \
  -config openssl.cnf \
  -extensions v3_ca

这里 -days 3650 是 10 年。根证书有效期可以长一些,因为你不经常换它。但服务器证书我一般只给 1 年,到期就换,减少风险。

验证一下:

openssl x509 -in my-ca/certs/ca.cert.pem -text -noout | grep -E "Subject:|Issuer:|CA:"

如果看到 Subject 和 Issuer 相同,且 CA:TRUE,说明根证书自签成功。

3.3.4 完整的根 CA 搭建流程

我把整个流程画成了一张图,方便你对照操作:

私有根 CA 搭建流程 1. 准备目录结构 mkdir / chmod / touch 2. 生成 CA 私钥 genrsa -aes256 4096 3. 自签根证书 req -x509 -extensions v3_ca 输出产物 ca.key.pem(私钥) + ca.cert.pem(根证书) 根 CA 的后续用途 ▶ 为 MQTT 服务器签发 server.cert.pem ▶ 为客户端设备签发 client.cert.pem ▶ 验证整个证书链的信任关系 ⚠ 安全红线:ca.key.pem 必须离线保管,权限设为 400

3.4 验证你的根 CA

搭建完别急着用。先自检一下:

# 1. 检查证书基本信息
openssl x509 -in my-ca/certs/ca.cert.pem -noout -subject -issuer -dates

# 2. 检查是否为 CA 证书
openssl x509 -in my-ca/certs/ca.cert.pem -noout -purpose | grep "CA"

# 3. 自验证(根证书自己验证自己)
openssl verify -CAfile my-ca/certs/ca.cert.pem my-ca/certs/ca.cert.pem

如果第三步返回 ca.cert.pem: OK,恭喜你,根 CA 搭建成功。

我的习惯: 每次搭建完根 CA,我会把 ca.cert.pem 分发到所有需要信任它的设备上,然后写一个简单的测试脚本,用 openssl s_client 验证 TLS 握手是否正常。这一步能提前暴露 90% 的配置问题。

3.5 避坑指南

这些年我踩过的坑,列出来给你参考:

  • 路径问题:OpenSSL 默认找 /etc/ssl/openssl.cnf,如果你用自定义配置,一定要用 -config 指定路径。我曾经因为忘了加这个参数,结果 OpenSSL 用了系统默认配置,颁发的证书 SAN 全丢了。
  • 权限问题:私钥文件权限一定要 400 或 600。有一次我图省事用了 644,结果被安全扫描工具报高危漏洞,被客户要求整改。
  • 时间问题:证书验证依赖系统时间。如果设备时间不准,证书会报“未生效”或“已过期”。IoT 设备尤其要注意 NTP 同步。
  • 序列号重复:serial 文件如果被误修改,可能导致颁发重复序列号的证书。建议每次颁发前检查一下 index.txt 和 serial 的一致性。