MQTT为了加强安全性,是支持Tls的安全模式连接的。在安全连接模式下,默认的服务端口由1883变更为8883,且使用CA证书来保证通讯双方的安全性。
MQTTX官方开源了一个多种编程语言的客户端连接Demo,其中C#版本的是基于MQTTnet库的,项目地址如下:
https://github.com/emqx/MQTT-Client-Examples/tree/master/mqtt-client-Csharp/MqttNetTLSClient
Client代码如下:
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Packets;
using MQTTnet.Protocol;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace MqttClient
{
public static class Client
{
public static async Task TlsClientWithCA(string caFile)
{
var mqttFactory = new MqttFactory();
X509Certificate2 cacert = new X509Certificate2(File.ReadAllBytes(caFile));
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("192.168.1.139", 8883)
.WithCredentials("emqx", "emqx123")
.WithTls(
new MqttClientOptionsBuilderTlsParameters()
{
UseTls = true,
SslProtocol = System.Security.Authentication.SslProtocols.Tls12,
CertificateValidationHandler = (certContext) =>
{
X509Chain chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
chain.ChainPolicy.VerificationTime = DateTime.Now;
chain.ChainPolicy.UrlRetrievalTimeout = new TimeSpan(0, 0, 0);
chain.ChainPolicy.CustomTrustStore.Add(cacert);
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
// convert provided X509Certificate to X509Certificate2
var x5092 = new X509Certificate2(certContext.Certificate);
return chain.Build(x5092);
}
})
.Build();
mqttClient.ApplicationMessageReceivedAsync += e =>
{
Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###");
Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}");
Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment)}");
Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}");
Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}");
Console.WriteLine();
return Task.CompletedTask;
};
using (var timeout = new CancellationTokenSource(5000))
{
await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token);
Console.WriteLine("The MQTT client is connected.");
}
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/2");
})
.Build();
await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
Console.WriteLine("MQTT client subscribed to topic.");
Console.WriteLine("Press enter to exit.");
Console.ReadLine();
}
}
public static async Task TlsClientWithCert(string caFile, string pfx, string password)
{
var mqttFactory = new MqttFactory();
var cacert = X509Certificate.CreateFromCertFile(caFile);
cacert = new X509Certificate2(cacert.Export(X509ContentType.SerializedCert));
var clientCert = new X509Certificate2(pfx, password);
var newCert = new X509Certificate2(clientCert.Export(X509ContentType.SerializedCert));
using (var mqttClient = mqttFactory.CreateMqttClient())
{
var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer("192.168.1.139", 8883)
.WithCredentials("emqx", "emqx123")
.WithTls(
new MqttClientOptionsBuilderTlsParameters()
{
UseTls = true,
SslProtocol = System.Security.Authentication.SslProtocols.Tls12,
CertificateValidationHandler = (o) =>
{
return true;
},
Certificates = new List<X509Certificate>() {
cacert, newCert
}
})
.Build();
mqttClient.ApplicationMessageReceivedAsync += e =>
{
Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###");
Console.WriteLine($"+ Topic = {e.ApplicationMessage.Topic}");
Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment)}");
Console.WriteLine($"+ QoS = {e.ApplicationMessage.QualityOfServiceLevel}");
Console.WriteLine($"+ Retain = {e.ApplicationMessage.Retain}");
Console.WriteLine();
return Task.CompletedTask;
};
using (var timeout = new CancellationTokenSource(5000))
{
await mqttClient.ConnectAsync(mqttClientOptions, timeout.Token);
Console.WriteLine("The MQTT client is connected.");
}
var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(
f =>
{
f.WithTopic("mqttnet/samples/topic/2");
})
.Build();
await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
Console.WriteLine("MQTT client subscribed to topic.");
Console.WriteLine("Press enter to exit.");
Console.ReadLine();
}
}
}
}
主程序代码如下:
namespace MqttClient
{
class Program
{
static void Main(string[] args)
{
// 自签名证书单向认证
// MqttClient.Client.TlsClientWithCA("E:/certs/cacert.pem").Wait();
// 双向认证
MqttClient.Client.TlsClientWithCert(@"E:/certs/cacert.pem", @"E:/certs/client.pfx","123456").Wait();
}
}
}
在证书连接模式下,我们一般可以获取到的是ca.crt、client.crt和client.key,并不能直接得到pem和pfx文件,这时需要转换使用openssl来转换。
openssl下载地址:
Win32/Win64 OpenSSL Installer for Windows - Shining Light Productions (slproweb.com)
在openssl的命令行下,执行两个转换语句,即可实现pem和pfx的转换。
openssl x509 -inform PEM -in ca.crt -outform PEM -out ca.pem
openssl pkcs12 -export -out client.pfx -inkey client.key -in client.crt -certfile ca.crt
也可以不调整ca,直接生成pfx
openssl pkcs12 -export -out client.pfx -inkey client.key -in client.crt -certfile ca.crt
这时对应的启动语句变成如下形式:
// 双向认证
MqttClient.Client.TlsClientWithCert(@"E:/certs/ca.crt", @"E:/certs/client.pfx","123456").Wait();
密码“123456”是在生成pfx文件时输入的,具体密码策略由服务器端预定。
这个连接过程,最需要注意的是生成pfx时,使用的-certfile ca.crt与第一个参数一致