1. 基于预共享密钥的MQTT设备接入认证
为什么资源受限设备适合 TLS-PSK
在资源受限的 IoT 设备上,完整 X.509 证书链验证需要存储根证书、设备证书与私钥,还要承担非对称运算开销。TLS-PSK(Pre-Shared Key)把认证凭据压缩为「身份标识 + 对称密钥」,握手阶段用对称算法完成密钥确认,适合 MCU 级设备。本节用 Mosquitto 作为 Broker,在设备端用 paho-mqtt 的 PSK 回调完成接入,并观察错误密钥被拒绝的行为。
准备 Broker 的 PSK 凭据文件
Mosquitto 通过 psk_file 加载预共享密钥,每行格式为 psk_id:十六进制密钥。密钥必须是偶数长度的十六进制字符串,长度建议 32 字节(64 个十六进制字符)。下面生成一个教学用密钥并写入文件:
# 生成 32 字节随机密钥并转为十六进制
PSK_HEX=$(openssl rand -hex 32)
echo "dev-001:${PSK_HEX}" > /etc/mosquitto/psk_file
chmod 600 /etc/mosquitto/psk_file
echo "psk_id=dev-001 psk=${PSK_HEX}"
对应的 mosquitto.conf 关键配置如下,重点是 psk_file 与 use_identity_as_username:
listener 8883
psk_file /etc/mosquitto/psk_file
use_identity_as_username true
allow_anonymous false
开启
use_identity_as_username true 后,Broker 会把 TLS-PSK 握手阶段的 psk_id 直接作为 MQTT 用户名,便于后续 ACL 按设备身份授权,无需在 CONNECT 报文里重复传用户名。设备端实现 PSK 回调并接入
Python 的 paho-mqtt 通过 tls_set 的 psk 参数注册回调。回调函数接收 psk_id,返回对应的密钥字节串。下面代码可直接运行,注意把 PSK_HEX 替换为与 Broker 一致的值:
import ssl
import paho.mqtt.client as mqtt
BROKER = "192.168.1.50"
PORT = 8883
PSK_ID = b"dev-001"
PSK_HEX = "替换为与Broker一致的64位十六进制密钥"
def on_psk_callback(psk_id):
# psk_id 为 bytes,返回密钥 bytes
assert psk_id == PSK_ID, f"unexpected psk_id: {psk_id!r}"
return bytes.fromhex(PSK_HEX)
client = mqtt.Client(client_id="dev-001", protocol=mqtt.MQTTv311)
client.tls_set(
ca_certs=None,
certfile=None,
keyfile=None,
tls_version=ssl.PROTOCOL_TLS_CLIENT,
ciphers=None,
)
client.tls_set_context # 占位说明:PSK 通过 tls_set 的 psk 参数注入
client._ssl_context = None # 由 paho 内部根据 psk 参数重建
# 正确注册 PSK 回调
client.tls_set(
ca_certs=None,
certfile=None,
keyfile=None,
tls_version=ssl.PROTOCOL_TLS_CLIENT,
ciphers=None,
psk=on_psk_callback,
)
def on_connect(c, userdata, flags, rc):
print("CONNACK rc =", rc)
if rc == 0:
c.publish("dev/001/status", "online", qos=1)
c.subscribe("dev/001/cmd", qos=1)
client.on_connect = on_connect
client.connect(BROKER, PORT, keepalive=60)
client.loop_forever()
上面示例中出现了两次
tls_set 调用,实际使用时只保留带 psk= 参数的那一次即可。第一次调用仅用于说明 PSK 必须通过 psk 参数注入,否则 paho 会走默认证书校验路径并因缺少 CA 而失败。验证成功接入与失败拒绝
先启动 Broker,再运行设备端脚本。成功时终端应打印 CONNACK rc = 0,并在 Broker 日志中看到类似 New client connected from ... as dev-001 的记录。随后用 mosquitto_sub 订阅同一主题,可看到设备发布的 online 消息。
失败验证分两种典型情况:
- 密钥不匹配:把设备端
PSK_HEX改错一位,握手阶段会触发 TLS alert,设备端报ssl.SSLError: [SSL: TLSV1_ALERT_BAD_RECORD_MAC],Broker 日志出现OpenSSL Error: ... bad record mac,连接在 MQTT CONNECT 之前就被拒绝。 - psk_id 未注册:把
PSK_ID改为dev-999,Broker 在psk_file中查不到该身份,握手失败,设备端收到 TLS alert,不会产生 CONNACK。
| 观察点 | 成功 | 密钥错误 | psk_id 未注册 |
|---|---|---|---|
| 设备端异常 | 无 | SSLError bad_record_mac | SSLError handshake failure |
| Broker 日志 | New client connected | bad record mac | psk_id not found |
| MQTT CONNACK | rc=0 | 未到达 | 未到达 |
| 是否建立会话 | 是 | 否 | 否 |
判断要点:PSK 认证失败发生在 TLS 握手阶段,因此不会出现 MQTT 层的 CONNACK 返回码。若看到
rc=5(未授权),说明 TLS 已通过,问题出在 MQTT 用户名/ACL 配置,而不是 PSK 本身。可复用的接入检查清单
- Broker 的
psk_file中psk_id与设备端PSK_ID完全一致(区分大小写)。 - 密钥为偶数长度十六进制,设备端
bytes.fromhex能正确解析,且与 Broker 端逐字节相同。 - 设备端
tls_set确实传入了psk=回调,未误走证书校验路径。 - Broker 监听端口为 8883,且
allow_anonymous false已生效。 - 失败时优先看 TLS 层日志,而不是 MQTT CONNACK,因为 PSK 错误在握手阶段即被拦截。