目录

Mosquitto证书双向验证

使用openssl生成自签名证书并配置Mosquitto双向验证

Openssl 自签CA证书

生成根证书和密钥

1
2
3
openssl req -new -x509 -newkey rsa:2048 \
-keyout caKey.pem -out caCrt.pem -days 365 \
-subj "/C=CN/ST=BJ/L=BJ/O=ZJ/OU=ROOT/CN=localhost/emailAddress=hello@world.com"

客户端生成密钥及证书签名请求

1
2
3
4
5
6
7
8
9
# mosquitto server 所需证书
openssl req -new -newkey rsa:2048 \
-keyout hubKey.pem -out hub.csr \
-subj "/C=CN/ST=BJ/L=BJ/O=ZJ/OU=HUB/CN=localhost/emailAddress=hello@world.com"

# 设备所需证书
openssl req -new -newkey rsa:2048 \
-keyout fakeMqttKey.pem -out fakeMqtt.csr \
-subj "/C=CN/ST=BJ/L=BJ/O=ZJ/OU=FakeMqtt/CN=localhost/emailAddress=hello@world.com"

使用 CA 根证书签发客户端证书

将两个csr文件上传至CA服务器后,由CA服务器执行签发

1
2
3
4
openssl x509 -req -days 365 \
-in hub.csr -out hubCrt.pem \
-CA caCrt.pem -CAkey caKey.pem \
-CAcreateserial

同理对 fakeMqtt.csr 进行签发

1
2
3
openssl x509 -req -days 365 \
-in fakeMqtt.csr -out fakeMqttCrt.pem \
-CA caCrt.pem -CAkey caKey.pem

验证生成的证书

1
2
openssl verify -CAfile caCrt.pem hubCrt.pem
openssl verify -CAfile caCrt.pem fakeMqttCrt.pem

TODO:证书吊销 CRL

此时目录如下(测试时CA服务和客户端位于同一目录):

1
2
3
4
5
6
7
8
9
> tree .
├── caCrt.pem
├── caCrt.srl
├── caKey.pem
├── fakeMqttCrt.pem
├── fakeMqttKey.pem
├── fakeMqttKeyNoPass.pem
├── hubCrt.pem
└── hubKey.pem

配置Mosquitto

启用双向验证

原始配置文件注释比较多,因此可以新建一份配置文件用于配置

1
2
# 查看可配置的选项
cat /usr/local/etc/mosquitto/mosquitto.conf

新建 hub.conf 配置文件,其中3个file文件选项指向自己创建的密钥文件:

1
2
3
4
5
6
port 1883
cafile certs/caCrt.pem
certfile certs/hubCrt.pem
keyfile certs/hubKey.pem
require_certificate true
use_identity_as_username true

启动

1
mosquitto -c ./hub.conf

订阅发布测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 订阅
mosquitto_sub -h localhost -p 1883 -t /t \
--cafile certs/caCrt.pem \
--cert certs/fakeMqttCrt.pem \
--key certs/fakeMqttKey.pem

# 发布
mosquitto_pub -h localhost -p 1883 -t /t -m "hello" \
--cafile certs/caCrt.pem \
--cert certs/fakeMqttCrt.pem \
--key certs/fakeMqttKey.pem

Go测试

Golang中的私钥读取无法传入密码选项,因此可以不添加密码或使用以下命令附加密码至pem文件

1
openssl rsa -in fakeMqttKey.pem -out fakeMqttKeyNoPass.pem -passin pass:your_password
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package main

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"time"

	mqtt "github.com/eclipse/paho.mqtt.golang"
)

func NewTLSConfig() *tls.Config {
	// Import trusted certificates from CAfile.pem.
	// Alternatively, manually add CA certificates to
	// default openssl CA bundle.
	certPool := x509.NewCertPool()
	pemCerts, err := ioutil.ReadFile("certs/caCrt.pem")
	if err == nil {
		certPool.AppendCertsFromPEM(pemCerts)
	}

	// Import client certificate/key pair
	cert, err := tls.LoadX509KeyPair(
		"certs/fakeMqttCrt.pem",
		"certs/fakeMqttKeyNoPass.pem")
	if err != nil {
		panic(err)
	}

	// Just to print out the client certificate..
	cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
	if err != nil {
		panic(err)
	}
	fmt.Println(cert.Leaf)

	// Create tls.Config with desired tls properties
	return &tls.Config{
		// RootCAs = certs used to verify server cert.
		RootCAs: certPool,
		// ClientAuth = whether to request cert from server.
		// Since the server is set up for SSL, this happens
		// anyways.
		ClientAuth: tls.NoClientCert,
		// ClientCAs = certs used to validate client cert.
		ClientCAs: nil,
		// InsecureSkipVerify = verify that cert contents
		// match server. IP matches what is in cert etc.
		InsecureSkipVerify: true,
		// Certificates = list of certs client sends to server.
		Certificates: []tls.Certificate{cert},
	}
}

var f mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
	fmt.Printf("TOPIC: %s\n", msg.Topic())
	fmt.Printf("MSG: %s\n", msg.Payload())
}

func main() {
	tlsconfig := NewTLSConfig()

	opts := mqtt.NewClientOptions()
	opts.AddBroker("ssl://localhost:1883")
	opts.SetClientID("ssl-sample").SetTLSConfig(tlsconfig)
	opts.SetDefaultPublishHandler(f)

	// Start the connection
	c := mqtt.NewClient(opts)
	if token := c.Connect(); token.Wait() && token.Error() != nil {
		panic(token.Error())
	}

	c.Subscribe("/go-mqtt/sample", 0, nil)

	i := 0
	for range time.Tick(time.Duration(1) * time.Second) {
		if i == 5 {
			break
		}
		text := fmt.Sprintf("this is msg #%d!", i)
		c.Publish("/go-mqtt/sample", 0, false, text)
		i++
	}

	c.Disconnect(250)
}