Skip to content

Commit

Permalink
Add support for X.509 Client Certificates (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
pglombardo authored Jan 9, 2024
1 parent 9e89933 commit 1dde8de
Show file tree
Hide file tree
Showing 14 changed files with 341 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,14 @@ dotnet_diagnostic.IDE0028.severity = none
dotnet_diagnostic.IDE0049.severity = none
dotnet_diagnostic.IDE0053.severity = none
dotnet_diagnostic.IDE0090.severity = none
dotnet_diagnostic.IDE0300.severity = none
dotnet_diagnostic.IDE0290.severity = none
dotnet_diagnostic.SA1508.severity = none

# FIXME: CLS Compliance
dotnet_diagnostic.CS3001.severity = none
dotnet_diagnostic.CS3002.severity = none

##########################################
# Formatting Rules
# https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules
Expand Down
193 changes: 193 additions & 0 deletions Documentation/docs/how-to/client-certificates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Custom Client Certificates

The HiveMQtt client has the ability to use custom client certificates to identify itself to the MQTT broker that it connect to.

For more information on X.509 client certificates, see the following:

* [X509 Client Certificate Authentication - MQTT Security Fundamentals](https://www.hivemq.com/blog/mqtt-security-fundamentals-x509-client-certificate-authentication/)
* [How to Generate a PEM client certificate](https://docs.hivemq.com/hivemq/latest/user-guide/howtos.html#_generate_a_pem_client_certificate_e_g_mosquitto_pub_sub)

You can add one or more client certificates to the HiveMQtt client through the `HiveMQClientOptionsBuilder` class.

Adding certificates will cause the client to present these certificates to the broker upon TLS connection negotiation.

## Using X509Certificate2

```csharp
using HiveMQtt.Client.Options;
using System.Security.Cryptography.X509Certificates;

// Can pre-create a X509Certificate2 or alternatively pass a string path
// to the certificate (see below)
var clientCertificate = new X509Certificate2(
'path/to/certificate-file-1.pem');

var options = new HiveMQClientOptionsBuilder()
.WithClientCertificate(clientCertificate);
.WithClientCertificate('path/to/certificate-file-2.pem');

var client = new HiveMQttClient(options);
```

## Using Certificates with a Passwords

If your certificate and protected with a password, you can either instantiate the
`X509Certificate2` object manually and pass it to the HiveMQtt client with
`WithClientCertificate`:

```csharp
using HiveMQtt.Client.Options;
using System.Security.Cryptography.X509Certificates;

var clientCertificate = new X509Certificate2(
'path/to/certificate-with-password.pem',
'certificate-password');

var options = new HiveMQClientOptionsBuilder()
.WithClientCertificate(clientCertificate);

var client = new HiveMQttClient(options);
```

...or alternatively, just pass the string path to the certificate with the password:

```csharp
using HiveMQtt.Client.Options;
using System.Security.Cryptography.X509Certificates;


var options = new HiveMQClientOptionsBuilder()
.WithClientCertificate(
'path/to/certificate-with-password.pem',
'certificate-password'
);

var client = new HiveMQttClient(options);
```

## Security Tips

When using `X509Certificate2` in C# with TLS client certificates that require a password, it's important to handle and protect the certificate passwords securely. Here are some tips to manage certificate passwords safely:

1. **Avoid Hardcoding Passwords:** Never hardcode the certificate password directly in the source code. This can lead to security vulnerabilities, as the source code (or compiled binaries) could be accessed by unauthorized parties.

2. **Use Configuration Files:** Store the password in a configuration file separate from the codebase. Ensure this file is not checked into source control (like Git) and is only accessible by the application and authorized team members.

3. **Environment Variables:** Consider using environment variables to store certificate passwords. This is useful in cloud or containerized environments. Environment variables can be set at the operating system level or within the deployment environment, keeping sensitive data out of the code.

4. **Secure Secrets Management:** When appropriate, utilize a secrets management tool (like Azure Key Vault, AWS Secrets Manager, or HashiCorp Vault) to store and access secrets like certificate passwords. These tools provide a secure and centralized way to manage sensitive data, with features like access control, audit logs, and automatic rotation of secrets.

5. **Regular Updates and Rotation:** Regularly update and rotate certificates and passwords. This practice can limit the damage if a certificate or its password is compromised.

## Using an Environment Variable for the Certificate Password

Instead of hard-coding a password, you can use an environment variable to hold the certificate password as follows:

```csharp
using System;
using HiveMQtt.Client.Options;
using System.Security.Cryptography.X509Certificates;

var certPassword = Environment.GetEnvironmentVariable("CERT_PASSWORD");

if (string.IsNullOrEmpty(certPassword))
{
throw new InvalidOperationException(
"Certificate password not found in environment variables");
}

var options = new HiveMQClientOptionsBuilder()
.WithClientCertificate(
"path/to/certificate-with-password.pem",
certPassword
);

var client = new HiveMQttClient(options);
```

## Using a Configuration File for the Certificate Password

You can use a configuration file to store the password instead of hardcoding it into your source code. In .NET applications, this is commonly done using appsettings.json or a similar configuration file. Here's a step-by-step guide on how to implement this:


To enhance security when handling sensitive information such as a certificate password, you can use a configuration file to store the password instead of hardcoding it into your source code. In .NET applications, this is commonly done using appsettings.json or a similar configuration file. Here's a step-by-step guide on how to implement this:

### Step 1: Modify appsettings.json

Add the certificate password to your `appsettings.json` file. It's important to ensure that this file is properly secured and not included in source control (e.g., Git).

```json
{
// Other configuration settings
"CertificateSettings": {
"CertificatePath": "path/to/certificate-with-password.pem",
"CertificatePassword": "YourSecurePassword"
}
}
```

### Step 2: Create a Configuration Model

Create a simple model to represent the settings.

```csharp
public class CertificateSettings
{
public string CertificatePath { get; set; }
public string CertificatePassword { get; set; }
}
```

### Step 3: Load Configuration in Your Application

In the part of your application where you configure services, set up code to load the settings from `appsettings.json`.

```csharp
using Microsoft.Extensions.Configuration;
using System.IO;

// Assuming you are in the Startup.cs or a similar setup file
public class Startup
{
public IConfiguration Configuration { get; }

public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

// Other configurations...
}
```

### Step 4: Use the Configuration in Your Code

Now, use the configuration settings when creating the HiveMQtt client options.

```csharp
// Load certificate settings
var certSettings = new CertificateSettings();
Configuration.GetSection("CertificateSettings").Bind(certSettings);

// Use settings to initialize HiveMQtt client options
var options = new HiveMQClientOptionsBuilder()
.WithClientCertificate(
certSettings.CertificatePath,
certSettings.CertificatePassword
);

var client = new HiveMQttClient(options);
```

### Notes

A couple tips on the above example:

* Secure `appsettings.json`: Ensure this file is not exposed or checked into source control. Use file permissions to restrict access.

* Environment-Specific Settings: For different environments (development, staging, production), use environment-specific appsettings files like `appsettings.Production.json`.

## Extended Options

TLS negotiation with client certificates is based on the `X509Certificate2` class. See the [official
.NET documentation](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509certificate2?view=net-8.0) for more options and information.
10 changes: 5 additions & 5 deletions Documentation/docs/how-to/publish.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Publish

# Simple
## Simple

The simple way to publish a message is to use the following API:

Expand All @@ -23,7 +23,7 @@ For the Quality of Service, see the [QualityOfService enum](https://github.com/h

But if you want more control and extended options for a publish, see the next section.

# MQTT5PublishMessage
## MQTT5PublishMessage

The [MQTT5PublishMessage](https://github.com/hivemq/hivemq-mqtt-client-dotnet/blob/main/Source/HiveMQtt/MQTT5/Types/MQTT5PublishMessage.cs) class represents the entirety of a publish message in MQTT. If you construct this class directly, you can access all of the MQTT publish options such as `Retain`, `PayloadFormatIndicator`, `UserProperties` and so forth.

Expand All @@ -43,7 +43,7 @@ var result = await client.PublishAsync(message);

For the full details, see the source code on [MQTT5PublishMessage](https://github.com/hivemq/hivemq-mqtt-client-dotnet/blob/main/Source/HiveMQtt/MQTT5/Types/MQTT5PublishMessage.cs).

# PublishMessageBuilder
## PublishMessageBuilder

The `PublishMessageBuilder` class provides a convenient way to construct MQTT publish messages with various options and properties. It allows you to customize the topic, payload, quality of service (QoS) level, retain flag, and other attributes of the message.

Expand All @@ -60,7 +60,7 @@ await client.PublishAsync(publishMessage).ConfigureAwait(false);

By using `PublishMessageBuilder`, you can easily construct MQTT publish messages with the desired properties and options. It provides a fluent and intuitive way to customize the topic, payload, QoS level, retain flag, and other attributes of the message.

# Publish Return Value: `PublishResult`
## Publish Return Value: `PublishResult`

The `PublishAsync` method returns a `PublishResult` object.

Expand All @@ -70,7 +70,7 @@ For `QualityOfService.AtLeastOnceDelivery` (QoS level 1) and `QualityOfService.E

For ease of use, you can call `PublishResult.ReasonCode()` to retrieve the appropriate result code automatically.

# See Also
## See Also

* [MQTT5PublishMessage](https://github.com/hivemq/hivemq-mqtt-client-dotnet/blob/main/Source/HiveMQtt/MQTT5/Types/MQTT5PublishMessage.cs)
* [QualityOfService](https://github.com/hivemq/hivemq-mqtt-client-dotnet/blob/main/Source/HiveMQtt/MQTT5/Types/QualityOfService.cs)
Expand Down
8 changes: 4 additions & 4 deletions Documentation/docs/how-to/subscribe.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Subscribe

# Set Your Message Handlers First
## Set Your Message Handlers First

You can subscribe to one or many topics in MQTT. But to do so, you must first set a message handler.

Expand All @@ -27,7 +27,7 @@ client.OnMessageReceived += MessageHandler;

* See: [OnMessageReceivedEventArgs](https://github.com/hivemq/hivemq-mqtt-client-dotnet/blob/main/Source/HiveMQtt/Client/Events/OnMessageReceivedEventArgs.cs)

# Basic
## Basic

To subscribe to a single topic with a Quality of Service level, use `SubscribeAsync` as follows.

Expand All @@ -39,7 +39,7 @@ assert subscribeResult.Subscriptions.Length() == 1
assert subscribeResult.Subscriptions[0].SubscribeReasonCode == SubAckReasonCode.GrantedQoS1
```

# Using `SubscribeOptions`
## Using `SubscribeOptions`

To utilize the complete set of options for `SubscribeAsync`, create a `SubscribeOptions` object.

Expand All @@ -61,7 +61,7 @@ subscribeOptions.UserProperties.Add("Client-Geo", "-33.8688, 151.2093");
var result = await client.SubscribeAsync(subscribeOptions).ConfigureAwait(false);
```

# See Also
## See Also

* [TopicFilter](https://github.com/hivemq/hivemq-mqtt-client-dotnet/blob/main/Source/HiveMQtt/MQTT5/Types/TopicFilter.cs)
* [SubscribeOptions](https://github.com/hivemq/hivemq-mqtt-client-dotnet/blob/main/Source/HiveMQtt/Client/Options/SubscribeOptions.cs)
Expand Down
2 changes: 1 addition & 1 deletion Documentation/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ const config = {
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} HiveMQ, GmbH. Built with Docusaurus.`,
copyright: `Copyright © ${new Date().getFullYear()} HiveMQ, GmbH.`,
},
prism: {
additionalLanguages: ['csharp'],
Expand Down
6 changes: 3 additions & 3 deletions Source/HiveMQtt/Client/HiveMQClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,10 @@ public async Task<SubscribeResult> SubscribeAsync(SubscribeOptions options)
{
subAck = await taskCompletionSource.Task.WaitAsync(TimeSpan.FromSeconds(120)).ConfigureAwait(false);
}
catch (TimeoutException ex)
catch (TimeoutException)
{
// log.Error(string.Format("Connect timeout. No response received in time.", ex);
throw ex;
Logger.Error("Subscribe timeout. No response received in time.");
throw;
}
finally
{
Expand Down
67 changes: 67 additions & 0 deletions Source/HiveMQtt/Client/HiveMQClientOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
namespace HiveMQtt.Client;

using System.Security.Cryptography.X509Certificates;
using HiveMQtt.Client.Options;

/// <summary>
Expand Down Expand Up @@ -58,6 +59,7 @@ namespace HiveMQtt.Client;
public class HiveMQClientOptionsBuilder
{
private readonly HiveMQClientOptions options = new HiveMQClientOptions();
private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();

Check warning on line 62 in Source/HiveMQtt/Client/HiveMQClientOptionsBuilder.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest

Check warning on line 62 in Source/HiveMQtt/Client/HiveMQClientOptionsBuilder.cs

View workflow job for this annotation

GitHub Actions / Build-ubuntu-latest


/// <summary>
/// Sets the address of the broker to connect to.
Expand Down Expand Up @@ -130,6 +132,71 @@ public HiveMQClientOptionsBuilder WithUseTls(bool useTls)
return this;
}

/// <summary>
/// Adds an X.509 certificate to be used for client authentication. This can be called
/// multiple times to add multiple certificates.
/// </summary>
/// <param name="clientCertificate">The client X.509 certificate to be used for client authentication.</param>
/// <returns>The HiveMQClientOptionsBuilder instance.</returns>
public HiveMQClientOptionsBuilder WithClientCertificate(X509Certificate2 clientCertificate)
{
this.options.ClientCertificates.Add(clientCertificate);
return this;
}

/// <summary>
/// Adds a list of X.509 certificates to be used for client authentication.
/// </summary>
/// <param name="clientCertificates">The list of client X.509 certificates to be used for client authentication.</param>
/// <returns>The HiveMQClientOptionsBuilder instance.</returns>
public HiveMQClientOptionsBuilder WithClientCertificates(List<X509Certificate2> clientCertificates)
{
foreach (var certificate in clientCertificates)
{
this.options.ClientCertificates.Add(certificate);
}

return this;
}

/// <summary>
/// Adds an X.509 certificate to be used for client authentication.
/// </summary>
/// <param name="clientCertificatePath">The path to the client X.509 certificate to be used for client authentication.</param>
/// <param name="password">The optional password for the client X.509 certificate.</param>
/// <returns>The HiveMQClientOptionsBuilder instance.</returns>
public HiveMQClientOptionsBuilder WithClientCertificate(string clientCertificatePath, string? password = null)
{
if (File.Exists(clientCertificatePath))
{
// Check if the file is readable
try
{
using (var fileStream = File.OpenRead(clientCertificatePath))
{
// File exists and is readable.
this.options.ClientCertificates.Add(new X509Certificate2(clientCertificatePath, password));
return this;
}
}
catch (UnauthorizedAccessException)
{
Logger.Error("File exists but is not readable due to access permissions.");
throw;
}
catch (IOException)
{
Logger.Error("An I/O error occurred while trying to read the file.");
throw;
}
}
else
{
Logger.Error("File does not exist.");
throw new FileNotFoundException();
}
}

/// <summary>
/// Sets whether to use a clean start.
/// <para>
Expand Down
Loading

0 comments on commit 1dde8de

Please sign in to comment.