1. 基于预共享密钥的MQTT设备接入认证

基于预共享密钥的MQTT设备接入认证 学习卡片

为什么资源受限设备适合 TLS-PSK

在资源受限的 IoT 设备上,完整 X.509 证书链验证需要存储根证书、设备证书与私钥,还要承担非对称运算开销。TLS-PSK(Pre-Shared Key)把认证凭据压缩为「身份标识 + 对称密钥」,握手阶段用对称算法完成密钥确认,适合 MCU 级设备。本节用 Mosquitto 作为 Broker,在设备端用 paho-mqtt 的 PSK 回调完成接入,并观察错误密钥被拒绝的行为。

IoT 设备 (MCU) psk_id "dev-001" psk (32B) 对称密钥 TLS-PSK 回调 on_psk MQTT CONNECT client_id=dev-001 端口 8883 MQTT Broker psk_file dev-001:密钥 TLS-PSK 监听 8883 密钥比对 psk_id 查表 CONNACK 0x00 / 0x05 会话建立 TLS-PSK 握手与 MQTT 接入 ① ClientHello (psk_id=dev-001) ② ServerHello + PSK 密钥套件 ③ 密钥确认 (Finished) ④ 握手完成,加密通道建立 ⑤ MQTT CONNECT → CONNACK 0x00 失败分支:psk 不匹配 TLS alert bad_record_mac → 连接被拒绝

准备 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_macSSLError handshake failure
Broker 日志New client connectedbad record macpsk_id not found
MQTT CONNACKrc=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 错误在握手阶段即被拦截。