IoT TLS双向认证:mTLS设备身份验证
2026/9/5 22:49:50 网站建设 项目流程

设备连 MQTT Broker,明文传输等于裸奔。TLS 加密是标配,但单向 TLS 只验证服务器证书——服务器不知道谁在连它。如果任何人拿到 Broker 地址就能连上来,设备身份完全靠用户名密码,安全性不够。

mTLS(双向 TLS)让设备出示自己的证书,Broker 验证设备身份才允许连接。

先说结论

mTLS 适合高安全场景——工业控制、医疗设备、金融支付。每台设备一张独立证书,即使密钥泄露只影响一台。配合 EMQX 的证书认证,用户名密码都不需要。


一、PKI 证书体系

mTLS 需要一套 PKI(Public Key Infrastructure):

根CA(Root CA) ├── 服务端证书(Broker) │ └── iot-broker.crt + iot-broker.key └── 设备证书(Device) ├── device-001.crt + device-001.key ├── device-002.crt + device-002.key └── ...(每台设备一张)

根 CA 自签名,设备和服务端证书由根 CA 签发。设备连服务器时:

  1. 验证服务器证书(确认服务器身份)
  2. 出示自己的证书给服务器
  3. 服务器验证设备证书(确认设备身份)
  4. 双方协商密钥,建立加密通道

二、生成证书

用 OpenSSL 搭建私有 CA

# 创建CA目录结构mkdir-p~/pki/{certs,crl,newcerts,private}cd~/pkichmod700privatetouchindex.txtecho1000>serial# CA配置文件cat>ca.conf<<'EOF' [ ca ] default_ca = CA_default [ CA_default ] dir = ./ certs = $dir/certs crl_dir = $dir/crl new_certs_dir = $dir/newcerts database = $dir/index.txt serial = $dir/serial certificate = $dir/certs/ca.crt private_key = $dir/private/ca.key default_md = sha256 default_days = 3650 policy = policy_loose [ policy_loose ] countryName = optional stateOrProvinceName = optional organizationName = optional organizationalUnitName = optional commonName = supplied emailAddress = optional [ req ] default_bits = 2048 default_md = sha256 distinguished_name = req_distinguished_name [ req_distinguished_name ] CN = ... [ server_cert ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer:always keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth [ client_cert ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer keyUsage = critical, digitalSignature extendedKeyUsage = clientAuth EOF# 1. 生成根CA证书openssl genrsa-outprivate/ca.key4096openssl req-configca.conf-keyprivate/ca.key\-new-x509-days3650-outcerts/ca.crt\-subj"/CN=IoT Root CA"# 2. 生成Broker服务端证书openssl genrsa-outprivate/broker.key2048openssl req-configca.conf-keyprivate/broker.key\-new-outcerts/broker.csr\-subj"/CN=iot-broker.example.com"openssl ca-configca.conf-extensionsserver_cert\-incerts/broker.csr-outcerts/broker.crt\-batch# 3. 生成设备证书openssl genrsa-outprivate/device-001.key2048openssl req-configca.conf-keyprivate/device-001.key\-new-outcerts/device-001.csr\-subj"/CN=device-001"openssl ca-configca.conf-extensionsclient_cert\-incerts/device-001.csr-outcerts/device-001.crt\-batch

三、批量签发设备证书

#!/usr/bin/env python3"""批量签发设备证书"""importsubprocessimportosimportjson PKI_DIR=os.path.expanduser("~/pki")OUTPUT_DIR=os.path.expanduser("~/device_certs")defgenerate_device_cert(device_id):"""生成单台设备的证书"""key_file=f"{OUTPUT_DIR}/{device_id}.key"csr_file=f"{OUTPUT_DIR}/{device_id}.csr"crt_file=f"{OUTPUT_DIR}/{device_id}.crt"# 生成密钥subprocess.run(["openssl","genrsa","-out",key_file,"2048"],check=True)# 生成CSRsubprocess.run(["openssl","req","-config",f"{PKI_DIR}/ca.conf","-key",key_file,"-new","-out",csr_file,"-subj",f"/CN={device_id}"],check=True)# 签发证书subprocess.run(["openssl","ca","-config",f"{PKI_DIR}/ca.conf","-extensions","client_cert","-in",csr_file,"-out",crt_file,"-batch"],check=True)# 清理CSRos.remove(csr_file)return{"device_id":device_id,"cert":crt_file,"key":key_file,"serial":get_cert_serial(crt_file),}defget_cert_serial(cert_file):"""获取证书序列号"""result=subprocess.run(["openssl","x509","-in",cert_file,"-noout","-serial"],capture_output=True,text=True)returnresult.stdout.strip().split("=")[1]if__name__=="__main__":os.makedirs(OUTPUT_DIR,exist_ok=True)devices=[f"device-{i:03d}"foriinrange(1,101)]manifest=[]fordevice_idindevices:info=generate_device_cert(device_id)manifest.append(info)print(f"Generated:{device_id}(serial:{info['serial']})")withopen(f"{OUTPUT_DIR}/manifest.json","w")asf:json.dump(manifest,f,indent=2)print(f"\nGenerated{len(manifest)}device certificates")

四、EMQX 配置 mTLS

EMQX 5.0 配置证书认证

# emqx.conflisteners:ssl:default:bind:"0.0.0.0:8883"max_connections:100000ssl_options:keyfile:"etc/certs/broker.key"certfile:"etc/certs/broker.crt"cacertfile:"etc/certs/ca.crt"verify:verify_peer# 验证客户端证书fail_if_no_peer_cert:true# 没证书就拒绝连接versions:-tlsv1.3-tlsv1.2ciphers:-ECDHE-ECDSA-AES256-GCM-SHA384-ECDHE-RSA-AES256-GCM-SHA384# 认证配置authentication:-mechanism:x509clientid_as_cn:true# 用证书CN作为ClientIDcn_prefix:"device-"# CN前缀匹配

这样配置后:

  • 设备连接必须出示证书
  • 证书 CN 必须以device-开头
  • 不需要用户名密码

五、设备端连接

ESP32 ESP-IDF

#include"mqtt_client.h"// CA证书(根证书,用于验证服务器)externconstcharca_cert[]asm("_binary_ca_crt_start");// 设备证书externconstcharclient_cert[]asm("_binary_device_crt_start");// 设备密钥externconstcharclient_key[]asm("_binary_device_key_start");voidmqtt_init(void){esp_mqtt_client_config_tcfg={.uri="mqtts://broker.example.com:8883",.client_id="device-001",.cert_pem=ca_cert,// CA证书.client_cert_pem=client_cert,// 设备证书.client_key_pem=client_key,// 设备密钥.lwt_topic="device/001/status",.lwt_msg="offline",.lwt_qos=1,.lwt_retain=1,};esp_mqtt_client_handle_tclient=esp_mqtt_client_init(&cfg);esp_mqtt_client_register_event(client,ESP_EVENT_ANY_ID,mqtt_event_handler,NULL);esp_mqtt_client_start(client);}

Python 端

importsslimportpaho.mqtt.clientasmqtt client=mqtt.Client(client_id="device-001")# 配置TLScontext=ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)context.load_verify_locations("ca.crt")# CA证书context.load_cert_chain("device-001.crt",# 设备证书"device-001.key")# 设备密钥context.check_hostname=True# 验证主机名context.verify_mode=ssl.CERT_REQUIRED# 验证服务器client.tls_set_context(context)client.connect("broker.example.com",8883,60)

六、证书吊销

设备被盗或证书泄露,需要吊销证书:

# 吊销证书openssl ca-configca.conf-revokecerts/device-001.crt# 生成CRL(证书吊销列表)openssl ca-configca.conf-gencrl-outcrl/ca.crl

EMQX 配置 CRL 检查(需要插件)

authentication:-mechanism:x509crl_check:truecrl_file:"etc/crl/ca.crl"

七、证书有效期管理

证书过期后设备无法连接。管理策略:

#!/usr/bin/env python3"""证书过期检查"""importsubprocessfromdatetimeimportdatetime,timedeltadefcheck_cert_expiry(cert_file):result=subprocess.run(["openssl","x509","-in",cert_file,"-noout","-enddate"],capture_output=True,text=True)date_str=result.stdout.strip().split("=")[1]expiry=datetime.strptime(date_str,"%b %d %H:%M:%S %Y %Z")returnexpiryif__name__=="__main__":importos,glob warning_days=30now=datetime.now()forcertinglob.glob("device_certs/*.crt"):expiry=check_cert_expiry(cert)remaining=expiry-nowifremaining.days<0:print(f"EXPIRED:{cert}({-remaining.days}days ago)")elifremaining.days<warning_days:print(f"EXPIRING:{cert}({remaining.days}days)")

八、实测数据

EMQX 5.0 + ESP32,mTLS 握手:

指标单向TLSmTLS
握手时间180ms320ms
ESP32内存占用12KB18KB
传输开销无(握手后一样)
CPU峰值(握手时)35%55%
证书验证失败率N/A0%

RSA-2048 证书,ESP32 @240MHz,TLS 握手约 300-350ms。ECC 证书可以降到 150ms。


九、踩坑清单

  1. CA 安全:根 CA 私钥泄露=所有设备证书作废。CA 私钥存离线机器,只签发时拿出来
  2. 证书 CN:CN(Common Name)是设备唯一标识。用device-001而不是 IP 或 MAC
  3. 时序问题:证书有效期靠系统时间。设备时间不对会导致验证失败。用 NTP 同步
  4. 证书嵌入:ESP32 把证书嵌入固件,用EMBED_TXTFILES。每台设备固件不同,量产不便。可以用安全元件(ATECC608A)存证书
  5. hostname 验证:Python 的check_hostname=True要求证书 CN 和连接的主机名匹配。如果用 IP 连接,证书要有subjectAltName扩展
  6. TLS 版本:ESP32 的 mbedTLS 默认支持 TLSv1.2。TLSv1.3 需要新版本 mbedTLS。EMQX 端两边都开
  7. 证书续期:设备证书有效期 1 年,快到期前要批量续期。在 OTA 中推送新证书
  8. CRL 分发:CRL 文件要定期更新到 Broker。用 HTTP 方式分发 CRL 更方便(OCSP)
  9. 性能:RSA-2048 握手比 ECC-P256 慢 3 倍。大规模部署建议用 ECC 证书
  10. clientid_as_cn:EMQX 的clientid_as_cn把证书 CN 当 ClientID,简化认证逻辑。但要保证 CN 唯一

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询