Mosquitto 与 Paho MQTT TLS使能备忘

目录

  • Mosquitto 与 Eclipse Paho MQTT TLS使能备忘
    • 1 为什么使用MQTT
    • 2 原型系统构成
      • 2.1 服务端软件环境
      • 2.2 客户端软件环境
    • 3 使用OpenSSL构建TLS加密的MQTT传输
      • 3.1 制作根证书
      • 3.2 制作服务端证书
      • 3.3 制作客户端证书
    • 4 配置Mosquitto服务端
    • 5 使用Mosquitto客户端工具测试连接
    • 6 在Paho-mqtt中使用TLS连接
    • 7 常见问题
      • 7.1 生成的证书有问题
      • 7.2 使用口令保护了密钥导致`Mosquitto`无法读密钥内容
      • 7.3 SSL协议版本问题
      • 7.4 服务端证书中包含的域名与MQTT创建连接时使用的`Broker`域名不一致
    • 其它
    • 参考文档:

Mosquitto 与 Eclipse Paho MQTT TLS使能备忘

最近使用Mqtt协议为公司的分布式系统搭建了一个“简陋”的原型通信框架,实际使用下来效果还不错,因此打算对通信进行TLS加密,使其能够真正用于生产中。本文主要记录使用Mosquitto作为服务端及测试客户端,Paho MQTT(Python)作为客户端的使用配置下,用openssl对通信进行加密的过程,以兹备忘。本文主要参考了此文 的大致步骤,但完全遵照此文描述的步骤无法正确完成TLS配置,具体有问题的步骤及解决方法会在下文叙述。

1 为什么使用MQTT

使用Mqtt协议的初衷如下:

  • 轻量级,便于快速实现系统原型
  • 发布/订阅式的消息模型适合分布式系统和多传感器小数据量的业务场景
  • 适合事件驱动、异步消息的业务模型
  • 基于纯文本形式的消息格式适合主要传输控制信令流的业务需求
  • Mqtt协议自带的QoSQuality of Service)功能适用于极不可靠链路的实际业务场景
  • 可扩展性好,可以通过桥接多个Broker等方式灵活扩展Mqtt消息网络
  • Web友好,可通过Websocket传输,便于实现基于Web的客户端

目前普遍认为Mqtt存在的缺点有:

  • 传输层使用TCP协议,面向连接带来的多次握手的额外开销使其对于功耗要求苛刻的物联网设备并不友好
  • 协议是非RESTful风格的
  • 消息的发布订阅依赖中心化的Broker,所有通信节点都必须能够连接到Broker,这带来额外的通信开销并且限制了Mqtt网络拓扑的灵活性
  • Mqtt协议本身缺乏安全机制,需要在传输层使用TLS或用户在应用层构建相应的安全机制

参考文献:

  1. Dan Dinculeana and Xiaochun Cheng
  2. Nastase, L. Security in the Internet of Things: A Survey on Application Layer Protocols

2 原型系统构成

原型系统由服务端(Broker)和客户端构成。服务端使用Mosquitto,并且使用Mosquitto自带的命令行工具Mosquitto_pubMosquitto_sub作为客户端测试手段。远程的传感器节点客户端使用Python Paho Mqtt库编写,参考文档

2.1 服务端软件环境

  • Mqtt协议版本为v3.1.1
  • Docker宿主环境为CentOS 7.5
  • Docker版本为Docker version 1.13.1, build dded712/1.13.1
  • 服务端(Broker)为官方容器(docker.io/eclipse-mosquitto)版本v1.6.7
  • OpenSSL版本为OpenSSL 1.0.2k-fips 26 Jan 2017

2.2 客户端软件环境

  • Python版本为3.7.3
  • Paho-mqtt版本为1.4.0
  • OpenSSL版本为OpenSSL 1.1.1c 28 May 2019

3 使用OpenSSL构建TLS加密的MQTT传输

我们的原型系统采用自签名证书,更好的做法是向CA申请正式的证书或者使用自动证书生成机制例如Let’s Encrypt来制作证书。

一套自签名证书由三个部分组成:

  1. 根证书: 用来签发服务端和客户端的证书
  2. 服务端证书:Mosquitto服务程序使用
  3. 客户端证书:Mosquitto客户端以及Paho-mqtt客户端使用

以及生成证书所需的密钥和证书请求(Certificate Request)。

(以下操作均以root用户身份完成)

3.1 制作根证书

    mkdir ca && cd ca 
    openssl genrsa -out ca.key 2048
    openssl req -new -x509 -days 1800 -key ca.key -out ca.crt 

填入申请证书所需的各项请求信息。由于是自签名证书,我们自己扮演了CA所以这里的请求信息可以任意填,但要特别注意的是使用genrsa生成密钥的时候要求输入Passphrase,这时不要填入口令对密钥进行加密,这会导致Paho-mqtt无法使用密钥。直接回车将密码留空即可。完成后在目录ca下生成两个文件:ca.keyca.crt

3.2 制作服务端证书

    cd ../
    mkdir server && cd server
    # step 1
    openssl genrsa -out server.key 2048  # 这里也一样,不要用口令加密密钥
    # step 2
    openssl req -new -out server.csr -key server.key # 用上一步得到的密钥生成一个签名请求(.csr)
    # step 3
    openssl x509 -req -in server.csr -CA ../ca/ca.crt -CAkey ../ca/ca.key \
                 -CAcreatserial -out server.crt -days 1200  # 自签名,得到服务端证书

注意

第二步填入请求信息的时候,有两点很重要:

  1. 请求信息不能和前面生成根证书时填入的请求信息完全相同,这会导致SSL验证过程认为这是单证书方案并比较CA证书和服务端证书,但二者的SHA1指纹又不一致,从而导致验证失败;
  2. 服务端请求信息里Common Name (e.g. server FQDN or YOUR name)一项特别重要,必须和运行服务端的主机域名(FQDN)完全一致,否则会导致客户端尝试进行连接时SSL验证过程失败。

3.3 制作客户端证书

cd ../
    mkdir client && cd client
    # step 1
    openssl genrsa -out client.key 2048  # 这里也一样,不要用口令加密密钥
    # step 2
    openssl req -new -out client.csr -key client.key # 用上一步得到的密钥生成一个签名请求(.csr)
    # step 3
    openssl x509 -req -in client.csr -CA ../ca/ca.crt -CAkey ../ca/ca.key \
                 -CAcreatserial -out client.crt -days 1200  # 自签名,得到客户端证书

4 配置Mosquitto服务端

首先拉取mosquitto的官方docker镜像:

sudo docker pull eclipse-mosquitto:1.6.7

进入config目录修改mosquitto.conf配置:

port 8884  # 使用8884作为TLS加密传输使用的端口
protocol mqtt # 8884 端口上的通信使用MQTT协议,使用websocket可以另外开一个listener,默认端口是9001
allow_anonymous false # 不允许匿名连接,客户端需提供口令才能接入
password_file /mosquitto/config/msq_passwd 口令密文存储在文件内
require_certificate true # 验证客户端证书
#require_certificate false
tls_version tlsv1.2 #指定TLS协议版本为1.2
cafile /mosquitto/config/ca/ca.crt  # 将前面生成的根证书文件(ca.crt)拷至该路径下
keyfile /mosquitto/config/certs/server.key # 将前面生成的服务端证书和密钥拷至该路径下
certfile /mosquitto/config/certs/server.crt
use_identity_as_username false
log_dest file /mosquitto/log/mosquitto.log

配置好后使用docker-compose启动容器:

sudo docker-compose up

容器运行正常则可以看到:

CONTAINER ID   IMAGE                    COMMAND                  CREATED       STATUS      PORTS                                                  NAMES
c33f4e895e4d   eclipse-mosquitto:1.6.7  "/docker-entrypoint.…"   3 weeks ago   Up 3 weeks  0.0.0.0:8884->8884/tcp,1883/tcp,0.0.0.0:9001->9001/tcp mosquitto_mqtt_1  

5 使用Mosquitto客户端工具测试连接

在客户端主机上安装Mosquitto客户端工具:

    sudo apt-get install -y mosquitto-clients

然后编写一个发布脚本(mosquitto_lpub.sh)和订阅脚本(mosquitto_lsub.sh)来测试连接:

  • 发布脚本
#! /bin/bash
# publish a message to topic ${client_id}
client_id="$1"
shift
host="${$(hostname):-mqtt.server.com}"
mosquitto_pub -h "${host}" -t "${client_id}" -p 8884  \ 
              --cafile "/ca/ca.crt" \
              --cert "/client/client.crt" \ 
              --key "/client/client.key" \
              -u "test" -P "123456"  \
              --tls-version "tlsv1.2" \ 
              -m "$@"
  • 订阅脚本
#! /bin/bash
# subscribe a topic ${client_id}
client_id="$1"
host="${$(hostname):-mqtt.server.com}"
mosquitto_pub -h "${host}" -t "${client_id}" -p 8884  \ 
              --cafile "/ca/ca.crt" \
              --cert "/client/client.crt" \ 
              --key "/client/client.key" \
              -u "test" -P "123456"  \
              --tls-version "tlsv1.2"
  • 测试
sudo chmod +x mosquitto_lpub.sh mosquitto_lsub.sh
./mosquitto_lsub.sh  'echo-server'
# 另起一个终端
./mosquitto_lpub.sh 'echo-server' 'hello mqtt'

如果一切正常,能够在订阅端看到这条消息。

6 在Paho-mqtt中使用TLS连接

Python Paho-mqtt中使用TLS只需要提供一个SSL上下文即可:

...
import ssl
mqttc = paho.mqtt.client.Client('my_client')
...
# 构建一个SSL上下文
SSL_CTX = {
                'ssl_port': 8884,
                'ca': /ca.crt,
                'client_cert': /client/client.crt,
                'client_key': /client/client.key,
                'cert_reqs': ssl.CERT_REQUIRED,
                'tls_version': ssl.PROTOCOL_TLSv1_2,
                'ciphers': None,
                'insecure': False  # 关闭insecure选项
              }
# 设置TLS参数
mqttc.tls_set( SSL_CTX['ca'],
               certfile=SSL_CTX['client_cert'],
               keyfile=SSL_CTX['client_key'],
               cert_reqs=SSL_CTX['cert_reqs'],
               tls_version=SSL_CTX['tls_version'],
               ciphers=SSL_CTX['ciphers'])
               
# 要求验证服务端证书中域名与mqtt连接创建时输入的broker域名一致
mqttc.tls_insecure_set(SSL_CTX['insecure']) 

tls_set() API的详细说明可以参考:tls_set()

7 常见问题

在使能TLS连接时最常见的问题是SSL握手失败。引发握手失败的原因通常都要根据具体情况分析。这里仅列举几种常见的情况:

7.1 生成的证书有问题

  • 使用了不同版本的openssl工具来创建CA证书、服务端证书及客户端证书,应该使用同一个环境来生成这三类证书和相关的密钥及签名请求等。
  • 生成证书的时间和系统时间不匹配导致证书验证失败。需要保证系统时间是正常的,例如可以通过NTP服务来进行互联网授时。同时要保证证书的有效期足够长,当前系统时间是在证书有效期内。根证书的有效期应该长于用其签发的任何证书的有效期。
  • 用于生成CA证书的签名请求内容与生成服务端证书的签名请求填入的内容完全一致。这也会导致SSL握手失败。

7.2 使用口令保护了密钥导致Mosquitto无法读密钥内容

7.3 SSL协议版本问题

测试时发现使用本文第2节所列的原型系统环境,SSL协议版本必须使用v1.2,使用其它版本会报错。

7.4 服务端证书中包含的域名与MQTT创建连接时使用的Broker域名不一致

其它

自签名证书的制作过程不仅适用于MQTT通信,对于其它类型的Socket通信也是适用的。

参考文档:

Mosquitto SSL Configuration -MQTT TLS Security

你可能感兴趣的:(networking,python,mqtt,openssl,tls)