Skip to content

Instantly share code, notes, and snippets.

@jkoplo
Last active February 6, 2023 05:26
Show Gist options
  • Save jkoplo/bd60cfe1a02c6e13b0a2d753289ae00f to your computer and use it in GitHub Desktop.
Save jkoplo/bd60cfe1a02c6e13b0a2d753289ae00f to your computer and use it in GitHub Desktop.
MQTTNet to AWS IoT - Core
using MQTTnet;
using MQTTnet.Client.Options;
using Oocx.ReadX509CertificateFromPem;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MQTTNet_AWS
{
class Program
{
private static RootCertificateTrust rootCertificateTrust;
private static string certificateAuthorityCertPEMString;
private static string deviceCertPEMString;
private static string devicePrivateCertPEMString;
static async Task Main(string[] args)
{
// Create a new MQTT client.
var factory = new MqttFactory();
var mqttClient = factory.CreateMqttClient();
var broker = "<AWS-IoT-Endpoint>";
var port = 8883;
deviceCertPEMString = File.ReadAllText(@"C:\xxxx-certificate.pem.crt");
devicePrivateCertPEMString = File.ReadAllText(@"C:\xxxx-private.pem.key");
certificateAuthorityCertPEMString = File.ReadAllText(@"C:\AmazonRootCA1.pem");
//Converting from PEM to X509 certs in C# is hard
//Load the CA certificate
//https://gist.github.com/ChrisTowles/f8a5358a29aebcc23316605dd869e839
var certBytes = Encoding.UTF8.GetBytes(certificateAuthorityCertPEMString);
var signingcert = new X509Certificate2(certBytes);
//Load the device certificate
//Use Oocx.ReadX509CertificateFromPem to load cert from pem
var reader = new CertificateFromPemReader();
X509Certificate2 deviceCertificate = reader.LoadCertificateWithPrivateKeyFromStrings(deviceCertPEMString, devicePrivateCertPEMString);
//This is a helper class to allow verifying a root CA separately from the Windows root store
rootCertificateTrust = new RootCertificateTrust();
rootCertificateTrust.AddCert(signingcert);
// Certificate based authentication
List<X509Certificate> certs = new List<X509Certificate>
{
signingcert,
deviceCertificate
};
//Set things up for our MQTTNet client
//NOTE: AWS does NOT support will topics or retained messages
//If you attempt to use either, it will disconnect with little explanation
MqttClientOptionsBuilderTlsParameters tlsOptions = new MqttClientOptionsBuilderTlsParameters();
tlsOptions.Certificates = certs;
tlsOptions.SslProtocol = System.Security.Authentication.SslProtocols.Tls12;
tlsOptions.UseTls = true;
tlsOptions.AllowUntrustedCertificates = true;
tlsOptions.CertificateValidationHandler += rootCertificateTrust.VerifyServerCertificate;
var options = new MqttClientOptionsBuilder()
.WithTcpServer(broker, port)
.WithClientId("mqttnet-ID")
.WithTls(tlsOptions)
.Build();
await mqttClient.ConnectAsync(options, CancellationToken.None);
var message = new MqttApplicationMessageBuilder()
.WithTopic("test")
.WithPayload("Hello World")
.Build();
await mqttClient.PublishAsync(message, CancellationToken.None);
}
}
/// <summary>
/// Verifies certificates against a list of manually trusted certs.
/// If a certificate is not in the Windows cert store, this will check that it's valid per our internal code.
/// </summary>
internal class RootCertificateTrust
{
X509Certificate2Collection certificates;
internal RootCertificateTrust()
{
certificates = new X509Certificate2Collection();
}
/// <summary>
/// Add a trusted certificate
/// </summary>
/// <param name="x509Certificate2"></param>
internal void AddCert(X509Certificate2 x509Certificate2)
{
certificates.Add(x509Certificate2);
}
/// <summary>
/// This matches the delegate signature expected for certificate verification for MQTTNet
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
internal bool VerifyServerCertificate(MqttClientCertificateValidationCallbackContext arg) => VerifyServerCertificate(new object(), arg.Certificate, arg.Chain, arg.SslPolicyErrors);
/// <summary>
/// This matches the delegate signature expected for certificate verification for M2MQTT
/// </summary>
/// <param name="sender"></param>
/// <param name="certificate"></param>
/// <param name="chain"></param>
/// <param name="sslPolicyErrors"></param>
/// <returns></returns>
internal bool VerifyServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None) return true;
X509Chain chainNew = new X509Chain();
var chainTest = chain;
chainTest.ChainPolicy.ExtraStore.AddRange(certificates);
// Check all properties
chainTest.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
// This setup does not have revocation information
chainTest.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
// Build the chain
var buildResult = chainTest.Build(new X509Certificate2(certificate));
//Just in case it built with trust
if (buildResult) return true;
//If the error is something other than UntrustedRoot, fail
foreach (var status in chainTest.ChainStatus)
{
if (status.Status != X509ChainStatusFlags.UntrustedRoot)
{
return false;
}
}
//If the UntrustedRoot is on something OTHER than the GreenGrass CA, fail
foreach (var chainElement in chainTest.ChainElements)
{
foreach (var chainStatus in chainElement.ChainElementStatus)
{
if (chainStatus.Status == X509ChainStatusFlags.UntrustedRoot)
{
var found = certificates.Find(X509FindType.FindByThumbprint, chainElement.Certificate.Thumbprint, false);
if (found.Count == 0) return false;
}
}
}
return true;
}
}
}
@jkoplo
Copy link
Author

jkoplo commented Dec 2, 2021

Hmm, getting a timeout exception is a bit odd. One downside of MQTT is that there's not much built into the 3.1 version that allows a broker to give a client an error message. Because of this, a lot of security issues manifest as the server just dropping the client's connection and the client getting a timeout or a disconnect error message.

The code above is definitely known-good. That said AWS IoT Core security can be really complicated. Depending on how you have your Things and their policies set up you could accidentally not allow the Thing to connect at all. Or you might have your policy set to only allow connections from that Thing when using a specific clientID or you aren't allowing publish on that topic or a bunch of other things that end up just giving you a timeout error.

You might try first testing the connection with OpenSSL and your certs - you can find plenty of guides on that online. That will help confirm that the TLS and mutual auth is all correct before even trying to establish the MQTT protocol communications.

@vincentskooi
Copy link

Thanks for the sample code! The code works with MQTTnet v3.0.13

For MQTTnet v4.1.0.247
Change:
internal bool VerifyServerCertificate(MqttClientCertificateValidationCallbackContext arg) => VerifyServerCertificate(new object(), arg.Certificate, arg.Chain, arg.SslPolicyErrors);
to:
internal bool VerifyServerCertificate(MqttClientCertificateValidationEventArgs arg) => VerifyServerCertificate(new object(), arg.Certificate, arg.Chain, arg.SslPolicyErrors);

MQTTnet.Client.Options namespace is no longer available.

@benlongo
Copy link

I believe AWS does support retained messages now

@jkoplo
Copy link
Author

jkoplo commented Feb 6, 2023

I believe AWS does support retained messages now

Looks like it!
https://aws.amazon.com/about-aws/whats-new/2021/08/aws-iot-core-supports-mqtt-retained-messages/

When I was first setting things up between .NET and AWS IoT they were unsupported. And since they're an option on subscribe, and subscribing is often the first thing you do after connect, it caused me a lot of heartache to just get an immediate disconnect - I assumed it was some topic permission or other connection issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment