Skip to content

Commit

Permalink
[#24] Support disabling hostname verification
Browse files Browse the repository at this point in the history
The FMS Forwarder now supports a flag for disabling verification of
server hostnames during a TLS handshake.

The shell script for creating Hono client configuration has been
extended to now support configuration of a CA certificate file and
disabling hostname verification.

Fixes #24
  • Loading branch information
sophokles73 authored and eriksven committed Nov 17, 2023
1 parent ed2dc1e commit a8d0add
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 73 deletions.
144 changes: 96 additions & 48 deletions components/fms-forwarder/src/mqtt_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,14 @@

use clap::{Arg, ArgMatches, Command};
use log::{error, info, warn};
use mqtt::{
AsyncClient, ConnectOptionsBuilder, CreateOptionsBuilder, SslOptionsBuilder,
};
use mqtt::{AsyncClient, ConnectOptionsBuilder, CreateOptionsBuilder, SslOptionsBuilder};
use paho_mqtt as mqtt;
use std::{thread, time::Duration};


const PARAM_CA_PATH: &str = "ca-path";
const PARAM_DEVICE_CERT: &str = "device-cert";
const PARAM_DEVICE_KEY: &str = "device-key";
const PARAM_ENABLE_HOSTNAME_VERIFICATION: &str = "enable-hostname-verification";
const PARAM_MQTT_CLIENT_ID: &str = "mqtt-client-id";
const PARAM_MQTT_URI: &str = "mqtt-uri";
const PARAM_MQTT_USERNAME: &str = "mqtt-username";
Expand All @@ -37,20 +35,21 @@ const PARAM_TRUST_STORE_PATH: &str = "trust-store-path";

/// Adds arguments to an existing command line which can be
/// used to configure the connection to an MQTT endpoint.
///
///
/// The following arguments are being added:
///
/// | long name | environment variable | default value |
/// |---------------------|----------------------|---------------|
/// | mqtt-client-id | MQTT_CLIENT_ID | random ID |
/// | mqtt-uri | MQTT_URI | - |
/// | mqtt-username | MQTT_USERNAME | - |
/// | mqtt-password | MQTT_PASSWORD | - |
/// | device-cert | DEVICE_CERT | - |
/// | device-key | DEVICE_KEY | - |
/// | ca-path | CA_PATH | - |
/// | trust-store-path | TRUST_STORE_PATH | - |
///
///
/// | Long Name | Environment Variable | Default Value |
/// |------------------------------|------------------------------|---------------|
/// | mqtt-client-id | MQTT_CLIENT_ID | - |
/// | mqtt-uri | MQTT_URI | - |
/// | mqtt-username | MQTT_USERNAME | - |
/// | mqtt-password | MQTT_PASSWORD | - |
/// | device-cert | DEVICE_CERT | - |
/// | device-key | DEVICE_KEY | - |
/// | ca-path | CA_PATH | - |
/// | trust-store-path | TRUST_STORE_PATH | - |
/// | enable-hostname-verification | ENABLE_HOSTNAME_VERIFICATION | `true` |
///
pub fn add_command_line_args(command: Command) -> Command {
command
.arg(
Expand Down Expand Up @@ -125,53 +124,47 @@ pub fn add_command_line_args(command: Command) -> Command {
.required(false)
.env("TRUST_STORE_PATH"),
)
.arg(
Arg::new(PARAM_ENABLE_HOSTNAME_VERIFICATION)
.value_parser(clap::builder::BoolishValueParser::new())
.long(PARAM_ENABLE_HOSTNAME_VERIFICATION)
.help("Indicates whether server certificates should be matched against the hostname/IP address
used by a client to connect to the server.")
.value_name("FLAG")
.required(false)
.default_value("true")
.env("ENABLE_HOSTNAME_VERIFICATION"),
)
}

/// A connection to an MQTT endpoint.
///
///
pub struct MqttConnection {
pub mqtt_client: AsyncClient,
pub uri: String,
pub client_id: String,
}

impl MqttConnection {

/// Creates a new connection to an MQTT endpoint.
///
/// Expects to find parameters as defined by [`add_command_line_args`] in the passed
/// in *args*.
///
/// The connection returned is configured to keep trying to (re-)connect to the configured
/// MQTT endpoint.
pub async fn new(args: &ArgMatches) -> Result<Self, Box<dyn std::error::Error>> {
let mqtt_uri = args
.get_one::<String>(PARAM_MQTT_URI)
.unwrap()
.to_owned();
let client_id = args
.get_one::<String>(PARAM_MQTT_CLIENT_ID)
.unwrap_or(&"".to_string())
.to_owned();
fn get_connect_options(
args: &ArgMatches,
) -> Result<paho_mqtt::ConnectOptions, Box<dyn std::error::Error>> {
let mut ssl_options_builder = SslOptionsBuilder::new();
if let Some(path) = args.get_one::<String>(PARAM_CA_PATH) {
match ssl_options_builder.ca_path(path) {
Err(e) => {
error!("failed to set CA path on MQTT client: {e}");
return Err(Box::new(e));
}
Ok(_builder) => (),
if let Err(e) = ssl_options_builder.ca_path(path) {
error!("failed to set CA path on MQTT client: {e}");
return Err(Box::new(e));
}
}
if let Some(path) = args.get_one::<String>(PARAM_TRUST_STORE_PATH) {
match ssl_options_builder.trust_store(path) {
Err(e) => {
error!("failed to set trust store path on MQTT client: {e}");
return Err(Box::new(e));
}
Ok(_builder) => (),
if let Err(e) = ssl_options_builder.trust_store(path) {
error!("failed to set trust store path on MQTT client: {e}");
return Err(Box::new(e));
}
}
if let Some(flag) = args.get_one::<bool>(PARAM_ENABLE_HOSTNAME_VERIFICATION) {
ssl_options_builder.verify(*flag);
}

let mut connect_options_builder = ConnectOptionsBuilder::new_v3();
connect_options_builder.connect_timeout(Duration::from_secs(10));
Expand Down Expand Up @@ -215,7 +208,23 @@ impl MqttConnection {
}

connect_options_builder.ssl_options(ssl_options_builder.finalize());
let connect_options = connect_options_builder.finalize();
Ok(connect_options_builder.finalize())
}

/// Creates a new connection to an MQTT endpoint.
///
/// Expects to find parameters as defined by [`add_command_line_args`] in the passed
/// in *args*.
///
/// The connection returned is configured to keep trying to (re-)connect to the configured
/// MQTT endpoint.
pub async fn new(args: &ArgMatches) -> Result<Self, Box<dyn std::error::Error>> {
let connect_options = MqttConnection::get_connect_options(args)?;
let mqtt_uri = args.get_one::<String>(PARAM_MQTT_URI).unwrap().to_owned();
let client_id = args
.get_one::<String>(PARAM_MQTT_CLIENT_ID)
.unwrap_or(&"".to_string())
.to_owned();
info!("connecting to MQTT endpoint at {}", mqtt_uri);
match CreateOptionsBuilder::new()
.server_uri(&mqtt_uri)
Expand Down Expand Up @@ -260,3 +269,42 @@ impl MqttConnection {
);
}
}

#[cfg(test)]
mod tests {

#[test]
fn test_get_add_command_line_args_requies_uri() {
let command = super::add_command_line_args(clap::Command::new("mqtt"));
let matches = command.try_get_matches_from(vec!["mqtt"]);
assert!(matches.is_err_and(|e| e.kind() == clap::error::ErrorKind::MissingRequiredArgument));
}

#[test]
fn test_get_add_command_line_args_uses_defaults() {
let command = super::add_command_line_args(clap::Command::new("mqtt"));
let matches =
command.get_matches_from(vec!["mqtt", "--mqtt-uri", "mqtts://non-existing.host.io"]);
assert_eq!(
matches.get_one::<String>(super::PARAM_MQTT_URI).unwrap(),
"mqtts://non-existing.host.io"
);
assert!(matches
.get_one::<String>(super::PARAM_MQTT_CLIENT_ID)
.is_none());
assert!(matches
.get_one::<String>(super::PARAM_MQTT_USERNAME)
.is_none());
assert!(matches
.get_one::<String>(super::PARAM_MQTT_PASSWORD)
.is_none());
assert!(matches.get_one::<String>(super::PARAM_DEVICE_KEY).is_none());
assert!(matches
.get_one::<String>(super::PARAM_DEVICE_CERT)
.is_none());
assert!(matches.get_one::<String>(super::PARAM_CA_PATH).is_none());
assert!(matches
.get_one::<bool>(super::PARAM_ENABLE_HOSTNAME_VERIFICATION)
.unwrap());
}
}
37 changes: 29 additions & 8 deletions create-config-functions.sh
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@
# files in this directory.
#

# create file with environment variables that the FMS Forwarder running in the vehicle
# will use to configure its connection to Hono's MQTT adapter
create_common_mqtt_client_env() {
ENV_FILE_PATH=$1
URI=$2
TRUST_STORE=$3
ENABLE_HOSTNAME_VALIDATION=$4
echo "Creating MQTT client properties file ${ENV_FILE_PATH} ..."
cat <<EOF > "${ENV_FILE_PATH}"
MQTT_URI=${URI}
TRUST_STORE_PATH=${TRUST_STORE}
ENABLE_HOSTNAME_VALIDATION=${ENABLE_HOSTNAME_VALIDATION}
EOF
}

# create file with environment variables that the FMS Forwarder running in the vehicle
# will use to configure its connection to Hono's MQTT adapter
create_mqtt_client_env() {
Expand All @@ -32,12 +47,15 @@ create_mqtt_client_env() {
USERNAME=$3
PASSWORD=$4
TRUST_STORE=$5
echo "Creating MQTT client properties file ${ENV_FILE_PATH} ..."
cat <<EOF > "${ENV_FILE_PATH}"
MQTT_URI=${URI}
ENABLE_HOSTNAME_VALIDATION=$6
create_common_mqtt_client_env \
"${ENV_FILE_PATH}" \
"${URI}" \
"${TRUST_STORE}" \
"${ENABLE_HOSTNAME_VALIDATION}"
cat <<EOF >> "${ENV_FILE_PATH}"
MQTT_USERNAME=${USERNAME}
MQTT_PASSWORD=${PASSWORD}
TRUST_STORE_PATH=${TRUST_STORE}
EOF
}

Expand All @@ -49,14 +67,17 @@ create_mqtt_client_env_with_cert() {
CLIENT_CERT_PATH=$3
CLIENT_KEY_PATH=$4
TRUST_STORE=$5
echo "Creating MQTT client properties file ${ENV_FILE_PATH} ..."
ENABLE_HOSTNAME_VALIDATION=$6
create_common_mqtt_client_env \
"${ENV_FILE_PATH}" \
"${URI}" \
"${TRUST_STORE}" \
"${ENABLE_HOSTNAME_VALIDATION}"
CONFIG_DIR_FMS_FORWARDER="${ENV_FILE_PATH%/*}/fms-forwarder"
cp "${CLIENT_CERT_PATH}" "${CLIENT_KEY_PATH}" "${CONFIG_DIR_FMS_FORWARDER}"
cat <<EOF > "${ENV_FILE_PATH}"
cat <<EOF >> "${ENV_FILE_PATH}"
DEVICE_CERT=/app/config/$(basename "$CLIENT_CERT_PATH")
DEVICE_KEY=/app/config/$(basename "$CLIENT_KEY_PATH")
MQTT_URI=${URI}
TRUST_STORE_PATH=${TRUST_STORE}
EOF
}

Expand Down
54 changes: 37 additions & 17 deletions create-config-hono.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ HONO_KAFKA_BROKERS=""
HONO_KAFKA_SECURE_PORT=9094
HONO_KAFKA_USER="hono"
HONO_KAFKA_PASSWORD="hono-secret"
TRUST_STORE_PATH="/etc/ssl/certs/ca-certificates.crt"
ENABLE_HOSTNAME_VALIDATION="false"
OUT_DIR="."
PROVISION_TO_HONO=""

Expand All @@ -49,20 +51,27 @@ Usage: ${cmd_name} OPTIONS ...
OPTIONS
-h | --help Display this usage information.
-t | --tenant The identifier of the tenant to create the device for.
--tenant-ca The path to a PEM file containing an X.509 certificate which should be set as the tenant's trust anchor for authenticating devices using a client certificate.
--device-id The identifier of the device to create.
--device-pwd The password that the device needs to use for authenticating to Hono's MQTT adapter.
--device-cert The path to a PEM file containing an X.509 certificate that the device uses for authenticating to Hono's MQTT adapter.
--device-key The path to a PEM file containing the private key that the device uses for authenticating to Hono's MQTT adapter.
-H | --host The host name or IP address of Hono's device registry. [${HONO_HOST}]
-p | --port The TLS port of the Hono device registry. [${HONO_REGISTRY_PORT}]
--kafka-brokers A comma separated list of host name/IP address:port tuples of the Kafka broker(s) to consume messages from. [${HONO_HOST}:${HONO_KAFKA_SECURE_PORT}]
--kafka-user The username to use for authenticating to the Kafka broker(s) to consume messages from. [${HONO_KAFKA_USER}]
--kafka-pwd The password to use for authenticating to the Kafka broker(s) to consume messages from. [${HONO_KAFKA_PASSWORD}]
--out-dir The path to the folder to write configuration files to. [${OUT_DIR}]
--provision Also provision device information to Hono's Device Registry.
-h | --help Display this usage information.
-t | --tenant ID The identifier of the tenant to create the device for.
--tenant-ca PATH The path to a PEM file containing an X.509 certificate which should be set as the tenant's
trust anchor for authenticating devices using a client certificate.
--device-id ID The identifier of the device to create.
--device-pwd PWD The password that the device needs to use for authenticating to Hono's MQTT adapter.
--device-cert PATH The path to a PEM file containing an X.509 certificate that the device uses for authenticating
to Hono's MQTT adapter.
--device-key PATH The path to a PEM file containing the private key that the device uses for authenticating to
Hono's MQTT adapter.
-H | --host HOST The host name or IP address of Hono's device registry. [${HONO_HOST}]
-p | --port PORT The TLS port of the Hono device registry. [${HONO_REGISTRY_PORT}]
--kafka-brokers LIST A comma separated list of host name/IP address:port tuples of the Kafka broker(s) to consume
messages from. [${HONO_HOST}:${HONO_KAFKA_SECURE_PORT}]
--kafka-user USER The username to use for authenticating to the Kafka broker(s) to consume messages from. [${HONO_KAFKA_USER}]
--kafka-pwd PWD The password to use for authenticating to the Kafka broker(s) to consume messages from. [${HONO_KAFKA_PASSWORD}]
--out-dir PATH The path to the folder to write configuration files to. [${OUT_DIR}]
--provision Also provision device information to Hono's Device Registry.
--trust-store-path PATH The path to a file that contains PEM encoded trusted root certificates. [${TRUST_STORE_PATH}]
--disable-hostname-validation Disables matching of server certificates against the hostname/IP address used by a client to
connect to the server.
Examples:
Expand Down Expand Up @@ -174,12 +183,18 @@ while [[ "$1" =~ ^- && ! "$1" == "--" ]]; do case $1 in
--kafka-pwd )
shift; HONO_KAFKA_PASSWORD=$1
;;
--trust-store-path )
shift; TRUST_STORE_PATH=$1
;;
-o | --out-dir )
shift; OUT_DIR=$1
;;
--provision )
PROVISION_TO_HONO=1
;;
--disable-hostname-validation )
ENABLE_HOSTNAME_VALIDATION="false"
;;
*)
echo "Ignoring unknown option: $1"
echo "Run with flag -h for usage"
Expand Down Expand Up @@ -238,8 +253,11 @@ security.protocol=SASL_SSL
sasl.mechanism=SCRAM-SHA-512
sasl.username=${HONO_KAFKA_USER}
sasl.password=${HONO_KAFKA_PASSWORD}
ssl.ca.location=/etc/ssl/certs/ca-certificates.crt
ssl.ca.location=${TRUST_STORE_PATH}
EOF
if [[ "${ENABLE_HOSTNAME_VALIDATION}" == "false" ]]; then
echo "ssl.endpoint.identification.algorithm=" >> "${KAFKA_PROPERTIES_FILE}"
fi

# create file with environment variables that the FMS Forwarder running in the vehicle
# will use to configure its connection to Hono's MQTT adapter
Expand All @@ -250,14 +268,16 @@ if [[ -n "${HONO_DEVICE_CERT}" ]]; then
"mqtts://${HONO_HOST}:8883" \
"${HONO_DEVICE_CERT}" \
"${HONO_DEVICE_KEY}" \
"/etc/ssl/certs/ca-certificates.crt"
"${TRUST_STORE_PATH}" \
"${ENABLE_HOSTNAME_VALIDATION}"
else
create_mqtt_client_env \
"${MQTT_PROPS_FILE}" \
"mqtts://${HONO_HOST}:8883" \
"${HONO_DEVICE_ID}@${HONO_TENANT_ID}" \
"${HONO_DEVICE_PASSWORD}" \
"/etc/ssl/certs/ca-certificates.crt"
"${TRUST_STORE_PATH}" \
"${ENABLE_HOSTNAME_VALIDATION}"
fi
# create file with environment variables to be used with Docker Compose when
# starting the services:
Expand Down

0 comments on commit a8d0add

Please sign in to comment.