Crate gneiss_mqtt_aws

Source
Expand description

This crate provides a builder API for creating MQTT clients that connect to AWS IoT Core, an AWS-managed message broker that supports both MQTT5 and MQTT311. This crate depends on gneiss-mqtt, which contains the MQTT client implementations.

IoT Core supports three different ways to securely establish and authenticate an MQTT connection:

  • MQTT over mTLS - via an X509 certificate (registered with AWS IoT Core) and its associated private key
  • MQTT over Websockets - sign the websocket upgrade request with AWS credentials using the Sigv4 signing algorithm
  • MQTT with Custom Authentication - invoke an AWS Lambda with data fields passed via the MQTT username and password fields of the Connect packet

This crate’s builder does all the dirty work for each of these connection methods, letting you just supply the required data.

§Feature Flags

Gneiss-mqtt supports two different MQTT clients:

  • tokio - An asynchronous client that executes on a Tokio async runtime. Intended for users comfortable with async programming in Rust.
  • threaded - A callback-based client that executes on a background thread. Intended for users that do not wish to deal with the additional complexity (or dependencies) of async programming in Rust.

Client support is controlled by enabling one or more of these crate features:

  • tokio-rustls: Enables the tokio client and establishing connections using the rustls TLS implementation.
  • tokio-native-tls: Enables the tokio client and establishing connections using the native-tls TLS implementation.
  • tokio-websockets: Enables the tokio client and transport over websockets.
  • threaded-rustls: Enables the thread-based client and establishing connections using the rustls TLS implementation.
  • threaded-native-tls: Enables the thread-based client and establishing connections using the native-tls TLS implementation.

AWS IoT Core requires TLS to connect and the crate will not build unless at least one TLS feature is enabled.

§Usage

To use this crate, you’ll first need to add it and gneiss-mqtt to your project’s Cargo.toml, enabling a TLS implementation feature as well.

Enabling the tokio client:

[dependencies]
gneiss-mqtt = { version = "...", features = [ "tokio-rustls" ] }
gneiss-mqtt-aws = { version = "...", features = [ "tokio-rustls" ] }
tokio = { version = "1", features = ["full"] }

Enabling the thread-based client:

[dependencies]
gneiss-mqtt = { version = "...", features = [ "threaded-rustls" ] }
gneiss-mqtt-aws = { version = "...", features = [ "threaded-rustls" ] }

Standalone examples for each connection method can be found in the project repository examples folder.

§Example: Connect to AWS IoT Core via mTLS with a tokio-based client

You’ll need to create and register an X509 device certificate with IoT Core and associate an IAM permission policy that allows IoT Core connections. See X509 Certificates and AWS IoT Core for guidance on this process.

To create a client and connect:

use gneiss_mqtt::client::AsyncClient;
use gneiss_mqtt_aws::AwsClientBuilder;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let endpoint = "<your AWS IoT Core endpoint>";
    let cert_path = "<path to your X509 certificate>";
    let key_path = "<path to the certificate's private key>";

    let client =
        AwsClientBuilder::new_direct_with_mtls_from_fs(endpoint, cert_path, key_path, None)?
            .build_tokio()?;

    // Once started, the client will recurrently maintain a connection to the endpoint until
    // stop() is invoked
    client.start(None)?;

    // <do stuff with the client>

    Ok(())
}

§Example: Connect to AWS IoT Core via Websockets with a tokio-based client

You’ll need to configure your runtime environment to source AWS credentials whose IAM policy allows IoT usage. This crate uses the AWS SDK for Rust to source the credentials necessary to sign the websocket upgrade request. Consult AWS documentation for more details.

To create a client and connect:

use gneiss_mqtt::client::AsyncClient;
use gneiss_mqtt_aws::{AwsClientBuilder, WebsocketSigv4OptionsBuilder};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    use gneiss_mqtt_aws::WebsocketSigv4Options;let endpoint = "<your AWS IoT Core endpoint>";
    let signing_region = "<AWS region for endpoint>";

    // Creating a default credentials provider chain is an async operation
    let sigv4_options = WebsocketSigv4OptionsBuilder::new(signing_region).await.build();

    let client =
        AwsClientBuilder::new_websockets_with_sigv4(endpoint, sigv4_options, None)?
            .build_tokio()?;

    // Once started, the client will recurrently maintain a connection to the endpoint until
    // stop() is invoked
    client.start(None)?;

    // <do stuff with the client>

    Ok(())
}

§Example: Connect to AWS IoT Core via AWS IoT Custom Authentication with the thread-based client

Custom authentication is an AWS IoT Core specific way to perform authentication without using certificates or http request signing. Instead, an AWS Lambda is invoked to decide whether or not a connection is allowed. The lambda function can access CONNECT packet fields (username and password) to determine whether the connection should be allowed and what permissions it should have. See the custom authentication documentation for step-by-step instructions in how to set up the AWS resources (authorizer, Lambda, etc…) to perform custom authentication.

Once the necessary AWS resources have been set up, you can easily create clients for each of the two supported custom authentication modes:

  • Unsigned Custom Authentication - Anyone can invoke the authorizer’s lambda if they know its ARN. This is not recommended for production since it is not protected from external abuse that may run up your AWS bill.
  • Signed Custom Authentication - Your Lambda function will only be invoked (and billed) if the Connect packet includes the cryptographic signature (based on an IoT Core registered public key) of a controllable value. Recommended for production.

§Unsigned Custom Authentication

For an unsigned custom authorizer (for testing/internal purposes only, not recommended for production):

use gneiss_mqtt::client::SyncClient;
use gneiss_mqtt_aws::{AwsClientBuilder, AwsCustomAuthOptions};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let endpoint = "<your AWS IoT Core endpoint>";
    let authorizer_name = "<name of the authorizer you want to invoke>";
    let username = "<username value to pass to the authorizer>"; // only necessary if the authorizer's lambda uses it
    let password = "<password value to pass to the authorizer>".as_bytes(); // only necessary if the authorizer's lambda uses it

    let mut custom_auth_options_builder = AwsCustomAuthOptions::builder_unsigned(
        Some(authorizer_name)
    );

    custom_auth_options_builder.with_username(username);
    custom_auth_options_builder.with_password(password);

    // In the common case, you will not need a root CA certificate
    let client =
        AwsClientBuilder::new_direct_with_custom_auth(endpoint, custom_auth_options_builder.build(), None)?
            .build_threaded()?;

    // Once started, the client will recurrently maintain a connection to the endpoint until
    // stop() is invoked
    client.start(None)?;

    // <do stuff with the client>

    Ok(())
}

§Signed Custom Authentication

For a signed custom authorizer (recommended for production):

use gneiss_mqtt::client::SyncClient;
use gneiss_mqtt_aws::{AwsClientBuilder, AwsCustomAuthOptions};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let endpoint = "<your AWS IoT Core endpoint>";
    let authorizer_name = "<name of the authorizer you want to invoke>";
    let authorizer_token_key_name = "<key name registered with the signing authorizer that indicates the name of the field whose value will contain the `authorizer_token_key_value`>";
    let authorizer_token_key_value = "<An arbitrary value.  The (Base64-encoded) signature of this value (using the private key of the public key associated with the authorizer) must be included as a separate field>";
    let authorizer_signature = "<URI-encoded Base64-encoded signature for `authorizer_token_key_value` signed by the private key of the public key associated with the authorizer>";
    let username = "<username value to pass to the authorizer>"; // only necessary if the authorizer's lambda uses it
    let password = "<password value to pass to the authorizer>".as_bytes(); // only necessary if the authorizer's lambda uses it

    let mut custom_auth_options_builder = AwsCustomAuthOptions::builder_signed(
        Some(authorizer_name),
        authorizer_signature,
        authorizer_token_key_name,
        authorizer_token_key_value
    );

    custom_auth_options_builder.with_username(username);
    custom_auth_options_builder.with_password(password);

    // In the common case, you will not need a root CA certificate
    let client =
        AwsClientBuilder::new_direct_with_custom_auth(endpoint, custom_auth_options_builder.build(), None)?
            .build_threaded()?;

    // Once started, the client will recurrently maintain a connection to the endpoint until
    // stop() is invoked
    client.start(None)?;

    // <do stuff with the client>

    Ok(())
}

You must be careful with the encodings of authorizer, authorizer_signature, and authorizer_token_key_name. Because custom authentication is supported over HTTP, these values must be URI-safe. It is up to you to URI encode them if necessary. In general, authorizer and authorizer_token_key_name are fixed when you create the authorizer resource and so it is straightforward to determine if you need to encode them or not. authorizer_signature should always be URI encoded.

§MQTT Client Configuration

The above examples skip all client configuration in favor of defaults. There are many configuration details that may be of interest depending on your use case. These options are controlled by structures in the gneiss-mqtt crate, via the with_client_options and with_connect_options methods on the AwsClientBuilder. Further details can be found in the relevant sections of the gneiss-mqtt docs:

§Frequently Asked Questions

See FAQ

Structs§

AwsClientBuilder
A builder object that allows for easy MQTT client construction for all supported connection methods to AWS IoT Core.
AwsCustomAuthOptions
A struct that holds all relevant details needed to perform custom authentication with AWS IoT Core.
AwsCustomAuthOptionsBuilder
Builder type for AwsCustomAuthOptions
WebsocketSigv4Optionstokio-websockets
Struct holding all configuration relevant to connecting an MQTT client to AWS IoT Core over websockets using a Sigv4-signed websocket handshake for authentication
WebsocketSigv4OptionsBuildertokio-websockets
A builder type that configures all relevant AWS signing options for connecting over websockets using Sigv4 request signing.

Enums§

TlsImplementation
This enumeration allows the user to override the default TLS implementation in the unfortunate case that they are forced to build the crate with multiple TLS implementations enabled.