diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index fdd082d4..67856403 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -59,7 +59,8 @@ jobs:
docker pull hawkbit/hawkbit-update-server
docker run -d --name hawkbit -p 8080:8080 hawkbit/hawkbit-update-server \
--hawkbit.server.security.dos.filter.enabled=false \
- --hawkbit.server.security.dos.maxStatusEntriesPerAction=-1
+ --hawkbit.server.security.dos.maxStatusEntriesPerAction=-1 \
+ --server.forward-headers-strategy=NATIVE
- name: Run test suite
run: |
diff --git a/README.md b/README.md
index 1331f7ca..a44f479f 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,9 @@ Setup target (device) configuration file:
target_name = test-target
auth_token = bhVahL1Il1shie2aj2poojeChee6ahShu
#gateway_token = bhVahL1Il1shie2aj2poojeChee6ahShu
+ #ssl_engine = pkcs11
+ #ssl_key = pkcs11:token=mytoken;object=mykey
+ #ssl_cert = /path/to/certificate.pem
bundle_download_location = /tmp/bundle.raucb
retry_wait = 60
connect_timeout = 20
@@ -98,7 +101,7 @@ Test Suite
Prepare test suite:
```shell
-$ sudo apt install libgirepository1.0-dev nginx-full
+$ sudo apt install libgirepository1.0-dev nginx-full libcairo2-dev
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install --upgrade pip
@@ -111,7 +114,8 @@ Run hawkBit docker container:
$ docker pull hawkbit/hawkbit-update-server
$ docker run -d --name hawkbit -p 8080:8080 hawkbit/hawkbit-update-server \
--hawkbit.server.security.dos.filter.enabled=false \
- --hawkbit.server.security.dos.maxStatusEntriesPerAction=-1
+ --hawkbit.server.security.dos.maxStatusEntriesPerAction=-1 \
+ --server.forward-headers-strategy=NATIVE
```
Run test suite:
diff --git a/config.conf.example b/config.conf.example
index 42acfec7..8a855787 100644
--- a/config.conf.example
+++ b/config.conf.example
@@ -20,6 +20,11 @@ auth_token = cb115a721af28f781b493fa467819ef5
# Or gateway_token can be used instead of auth_token
#gateway_token = cb115a721af28f781b493fa467819ef5
+# Or ssl key/cert locations if mTLS is used
+#ssl_engine = pkcs11
+#ssl_key = pkcs11:token=mytoken;object=mykey
+#ssl_cert = /path/to/certificate.pem
+
# Temporay file RAUC bundle should be downloaded to
bundle_download_location = /tmp/bundle.raucb
diff --git a/docs/using.rst b/docs/using.rst
index 56a4a42d..2ef6da36 100644
--- a/docs/using.rst
+++ b/docs/using.rst
@@ -6,6 +6,9 @@ Using the RAUC hawkbit Updater
Authentication
--------------
+Target token
+^^^^^^^^^^^^
+
As described on the `hawkBit Authentication page `_
in the "DDI API Authentication Modes" section, a device can be authenticated
with a security token. A security token can be either a "Target" token or a
@@ -13,6 +16,9 @@ with a security token. A security token can be either a "Target" token or a
defined in hawkBit. In the RAUC hawkBit updater's configuration file it's
referred to as ``auth_token``.
+Gateway token
+^^^^^^^^^^^^^
+
Targets can also be connected through a gateway which manages the targets
directly and as a result these targets are indirectly connected to the hawkBit
update server. The "Gateway" token is used to authenticate this gateway and
@@ -24,6 +30,26 @@ Although gateway token is very handy during development or testing, it's
recommended to use this token with care because it can be used to
authenticate any device.
+Mutual TLS with client key/certificate
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+HawkBit also offers a certificate-based authentication mechanism, also known
+as mutual TLS (mTLS), which eliminates the need to share a security token with
+the server. This is the preferred authentication mode targets connecting to
+bosch-iot-suite.com. The target needs to send a complete (self-contained)
+certificate chain along with the request which is then validated by a trusted
+reverse proxy. The certificate chain can contain multiple certificates,
+e.g. a target-specific client certificate, an intermediate certificate, and
+a root certificate. A full certificate chain is required because the reverse
+proxy only keeps fingerprints of issuer(s) certificates.
+In the RAUC hawkBit updater's configuration file the options are called
+``ssl_key`` and ``ssl_cert``. They need to be set to the target's private
+key and a full certificate chain. If a file is supplied it needs to be in PEM
+format.
+Optionally, the ``ssl_engine`` option can be set if an openssl engine
+needs to be loaded to access the private key. In that case the format of the
+value supplied to ``ssl_key`` depends on the engine configured.
+
Streaming Support
-----------------
diff --git a/include/config-file.h b/include/config-file.h
index a2df87fe..8d8cf4fe 100644
--- a/include/config-file.h
+++ b/include/config-file.h
@@ -15,6 +15,9 @@ typedef struct Config_ {
gchar* hawkbit_server; /**< hawkBit host or IP and port */
gboolean ssl; /**< use https or http */
gboolean ssl_verify; /**< verify https certificate */
+ gchar* ssl_key; /**< SSL/TLS authentication private key */
+ gchar* ssl_cert; /**< SSL/TLS client certificate */
+ gchar* ssl_engine; /**< SSL engine to use with ssl_key */
gboolean post_update_reboot; /**< reboot system after successful update */
gboolean resume_downloads; /**< resume downloads or not */
gboolean stream_bundle; /**< streaming installation or not */
diff --git a/include/hawkbit-client.h b/include/hawkbit-client.h
index 76d98218..1f2d60ce 100644
--- a/include/hawkbit-client.h
+++ b/include/hawkbit-client.h
@@ -102,6 +102,8 @@ struct on_new_software_userdata {
GSourceFunc install_complete_callback; /**< callback function to be called when installation is complete */
gchar *file; /**< downloaded new software file */
gchar *auth_header; /**< authentication header for bundle streaming */
+ gchar *ssl_key; /**< authentication key for bundle streaming */
+ gchar *ssl_cert; /**< authentication certificate for bundle streaming */
gboolean ssl_verify; /**< whether to ignore server cert verification errors */
gboolean install_success; /**< whether the installation succeeded or not (only meaningful for run_once mode!) */
};
diff --git a/include/rauc-installer.h b/include/rauc-installer.h
index f8415799..c6793a83 100644
--- a/include/rauc-installer.h
+++ b/include/rauc-installer.h
@@ -14,6 +14,8 @@
struct install_context {
gchar *bundle; /**< Rauc bundle file to install */
gchar *auth_header; /**< Authentication header for bundle streaming */
+ gchar *ssl_key; /**< SSL client authentication key */
+ gchar *ssl_cert; /**< SSL client authentication certificate */
gboolean ssl_verify; /**< Whether to ignore server cert verification errors */
GSourceFunc notify_event; /**< Callback function */
GSourceFunc notify_complete; /**< Callback function */
@@ -31,6 +33,8 @@ struct install_context {
* @param[in] bundle RAUC bundle file (.raucb) to install.
* @param[in] auth_header Authentication header on HTTP streaming installation or NULL on normal
* installation.
+ * @param[in] ssl_key Client authentication key or NULL on normal installation.
+ * @param[in] ssl_cert Client authentication certificate or NULL on normal installation.
* @param[in] ssl_verify Whether to ignore server cert verification errors.
* @param[in] on_install_notify Callback function to be called with status info during
* installation.
@@ -40,7 +44,8 @@ struct install_context {
* @return for wait=TRUE, TRUE if installation succeeded, FALSE otherwise; for
* wait=FALSE TRUE is always returned immediately
*/
-gboolean rauc_install(const gchar *bundle, const gchar *auth_header, gboolean ssl_verify,
+gboolean rauc_install(const gchar *bundle, const gchar *auth_header,
+ gchar *ssl_key, gchar *ssl_cert, gboolean ssl_verify,
GSourceFunc on_install_notify, GSourceFunc on_install_complete, gboolean wait);
#endif // __RAUC_INSTALLER_H__
diff --git a/src/config-file.c b/src/config-file.c
index d09aebca..778b5a30 100644
--- a/src/config-file.c
+++ b/src/config-file.c
@@ -241,6 +241,10 @@ Config* load_config_file(const gchar *config_file, GError **error)
gboolean key_auth_token_exists = FALSE;
gboolean key_gateway_token_exists = FALSE;
gboolean bundle_location_given = FALSE;
+ gboolean ssl_key_exists = FALSE;
+ gboolean ssl_cert_exists = FALSE;
+ gboolean ssl_auth = FALSE;
+ gboolean token_auth = FALSE;
g_return_val_if_fail(config_file, NULL);
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
@@ -255,13 +259,43 @@ Config* load_config_file(const gchar *config_file, GError **error)
error))
return NULL;
+ if (!get_key_bool(ini_file, "client", "ssl", &config->ssl, DEFAULT_SSL, error))
+ return NULL;
+ if (!get_key_bool(ini_file, "client", "ssl_verify", &config->ssl_verify,
+ DEFAULT_SSL_VERIFY, error))
+ return NULL;
+ if (config->ssl) {
+ ssl_key_exists = get_key_string(ini_file, "client", "ssl_key",
+ &config->ssl_key, NULL, NULL);
+ ssl_cert_exists = get_key_string(ini_file, "client", "ssl_cert",
+ &config->ssl_cert, NULL, NULL);
+ ssl_auth = ssl_cert_exists && ssl_key_exists;
+ if ((ssl_cert_exists || ssl_key_exists) && !ssl_auth) {
+ g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+ "Only one of 'ssl_key' and 'ssl_cert' is set");
+ return NULL;
+ }
+ get_key_string(ini_file, "client", "ssl_engine",
+ &config->ssl_engine, NULL, NULL);
+ if (config->ssl_engine && !ssl_auth) {
+ g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+ "SSL engine set without ssl_key or ssl_cert");
+ return NULL;
+ }
+ }
key_auth_token_exists = get_key_string(ini_file, "client", "auth_token",
&config->auth_token, NULL, NULL);
key_gateway_token_exists = get_key_string(ini_file, "client", "gateway_token",
&config->gateway_token, NULL, NULL);
- if (!key_auth_token_exists && !key_gateway_token_exists) {
+ token_auth = key_auth_token_exists || key_gateway_token_exists;
+ if (!token_auth && !ssl_auth) {
g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
- "Neither 'auth_token' nor 'gateway_token' set");
+ "Neither token nor ssl authentication set");
+ return NULL;
+ }
+ if (token_auth && ssl_auth) {
+ g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
+ "Both token and ssl authentication set");
return NULL;
}
if (key_auth_token_exists && key_gateway_token_exists) {
@@ -277,11 +311,6 @@ Config* load_config_file(const gchar *config_file, GError **error)
return NULL;
bundle_location_given = get_key_string(ini_file, "client", "bundle_download_location",
&config->bundle_download_location, NULL, NULL);
- if (!get_key_bool(ini_file, "client", "ssl", &config->ssl, DEFAULT_SSL, error))
- return NULL;
- if (!get_key_bool(ini_file, "client", "ssl_verify", &config->ssl_verify,
- DEFAULT_SSL_VERIFY, error))
- return NULL;
if (!get_group(ini_file, "device", &config->device, error))
return NULL;
if (!get_key_int(ini_file, "client", "connect_timeout", &config->connect_timeout,
@@ -338,6 +367,9 @@ void config_file_free(Config *config)
g_free(config->tenant_id);
g_free(config->auth_token);
g_free(config->gateway_token);
+ g_free(config->ssl_engine);
+ g_free(config->ssl_key);
+ g_free(config->ssl_cert);
g_free(config->bundle_download_location);
if (config->device)
g_hash_table_destroy(config->device);
diff --git a/src/hawkbit-client.c b/src/hawkbit-client.c
index dca518bf..7a1bfec8 100644
--- a/src/hawkbit-client.c
+++ b/src/hawkbit-client.c
@@ -217,7 +217,7 @@ static char* get_auth_header()
return g_strdup_printf("Authorization: GatewayToken %s",
hawkbit_config->gateway_token);
- g_return_val_if_reached(NULL);
+ return NULL;
}
/**
@@ -242,6 +242,66 @@ static gboolean set_auth_curl_header(struct curl_slist **headers, GError **error
return res;
}
+/**
+ * @brief Set Curl options for TLS/SSL client authentication
+ *
+ * @param[in] curl Curl handle
+ * @param[out] error Error
+ * @return TRUE if ssl authorization method set in config was set successfully,
+ * FALSE otherwise (error set)
+ */
+static gboolean set_auth_curl_ssl(CURL *curl, GError **error)
+{
+ curl_easy_setopt(curl, CURLOPT_SSLKEY, hawkbit_config->ssl_key);
+ curl_easy_setopt(curl, CURLOPT_SSLCERT, hawkbit_config->ssl_cert);
+
+ if (hawkbit_config->ssl_engine) {
+ if (curl_easy_setopt(curl, CURLOPT_SSLENGINE, hawkbit_config->ssl_engine) != CURLE_OK) {
+ g_set_error(error, RHU_HAWKBIT_CLIENT_CURL_ERROR,
+ CURLE_FAILED_INIT, "Failed to set ssl engine");
+ return FALSE;
+ }
+ curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "ENG");
+ if (curl_easy_setopt(curl, CURLOPT_SSLENGINE_DEFAULT, 1L) != CURLE_OK) {
+ g_set_error(error, RHU_HAWKBIT_CLIENT_CURL_ERROR,
+ CURLE_FAILED_INIT, "Failed to set engine as default");
+ return FALSE;
+ }
+ g_debug("Using SSL engine %s", hawkbit_config->ssl_engine);
+ }
+ return TRUE;
+}
+
+/**
+ * @brief Set Curl options for client authentication
+ *
+ * @param[in] curl Curl handle
+ * @param[out] headers curl_slist** of already set headers
+ * @param[out] error Error
+ * @return TRUE if authorization method set in config and header was added successfully,
+ * TRUE if no authorization method set, FALSE otherwise (error set)
+ */
+static gboolean set_auth_curl(CURL *curl, struct curl_slist **headers, GError **error)
+{
+ gboolean res;
+
+ // Try ssl authentication
+ if (hawkbit_config->ssl_key && hawkbit_config->ssl_cert) {
+ res = set_auth_curl_ssl(curl, error);
+ if (res) {
+ g_debug("SSL authentication set");
+ return TRUE;
+ }
+ }
+
+ // Try token authentication
+ res = set_auth_curl_header(headers, error);
+ if (res)
+ g_debug("Token authentication set");
+
+ return res;
+}
+
/**
* @brief Set common Curl options, namely user agent, connect timeout, SSL
* verify peer and SSL verify host options.
@@ -314,7 +374,7 @@ static gboolean get_binary(const gchar *download_url, const gchar *file, curl_of
curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, resume_from);
- if (!set_auth_curl_header(&headers, error))
+ if (!set_auth_curl(curl, &headers, error))
return FALSE;
// set up request headers
@@ -434,7 +494,7 @@ static gboolean rest_request(enum HTTPMethod method, const gchar *url,
if (!add_curl_header(&headers, "Accept: application/json;charset=UTF-8", error))
return FALSE;
- if (!set_auth_curl_header(&headers, error))
+ if (!set_auth_curl(curl, &headers, error))
return FALSE;
if (jsonRequestBody &&
@@ -839,6 +899,8 @@ static gpointer download_thread(gpointer data)
.install_complete_callback = install_complete_cb,
.file = hawkbit_config->bundle_download_location,
.auth_header = NULL,
+ .ssl_key = NULL,
+ .ssl_cert = NULL,
.ssl_verify = hawkbit_config->ssl_verify,
.install_success = FALSE,
};
@@ -994,6 +1056,8 @@ static gboolean start_streaming_installation(Artifact *artifact, GError **error)
.install_complete_callback = install_complete_cb,
.file = artifact->download_url,
.auth_header = auth_header,
+ .ssl_key = hawkbit_config->ssl_key,
+ .ssl_cert = hawkbit_config->ssl_cert,
.ssl_verify = hawkbit_config->ssl_verify,
.install_success = FALSE,
};
@@ -1360,6 +1424,8 @@ static gboolean hawkbit_pull_cb(gpointer user_data)
g_warning("Failed to authenticate. Check if auth_token is correct?");
if (hawkbit_config->gateway_token)
g_warning("Failed to authenticate. Check if gateway_token is correct?");
+ } else if (error->code == CURLE_SSL_CERTPROBLEM) {
+ g_warning("Failed to authenticate. Check if ssl_key/cert are correct?");
} else {
g_warning("Scheduled check for new software failed: %s (%d)",
error->message, error->code);
diff --git a/src/rauc-hawkbit-updater.c b/src/rauc-hawkbit-updater.c
index 3d65f4c3..efbf4c43 100644
--- a/src/rauc-hawkbit-updater.c
+++ b/src/rauc-hawkbit-updater.c
@@ -106,6 +106,7 @@ static gboolean on_new_software_ready_cb(gpointer data)
notify_hawkbit_install_progress = userdata->install_progress_callback;
notify_hawkbit_install_complete = userdata->install_complete_callback;
userdata->install_success = rauc_install(userdata->file, userdata->auth_header,
+ userdata->ssl_key, userdata->ssl_cert,
userdata->ssl_verify,
on_rauc_install_progress_cb,
on_rauc_install_complete_cb, run_once);
diff --git a/src/rauc-installer.c b/src/rauc-installer.c
index f9bcb949..5dacf2ce 100644
--- a/src/rauc-installer.c
+++ b/src/rauc-installer.c
@@ -147,11 +147,16 @@ static gpointer install_loop_thread(gpointer data)
context = data;
g_main_context_push_thread_default(context->loop_context);
- if (context->auth_header) {
- gchar *headers[2] = {NULL, NULL};
- headers[0] = context->auth_header;
- g_variant_dict_insert(&args, "http-headers", "^as", headers);
-
+ if (context->auth_header || (context->ssl_key && context->ssl_cert)) {
+ if (context->auth_header) {
+ gchar *headers[2] = {NULL, NULL};
+ headers[0] = context->auth_header;
+ g_variant_dict_insert(&args, "http-headers", "^as", headers);
+ }
+ if (context->ssl_key && context->ssl_cert) {
+ g_variant_dict_insert(&args, "tls-key", "s", context->ssl_key);
+ g_variant_dict_insert(&args, "tls-cert", "s", context->ssl_cert);
+ }
g_variant_dict_insert(&args, "tls-no-verify", "b", !context->ssl_verify);
}
@@ -200,7 +205,8 @@ static gpointer install_loop_thread(gpointer data)
return NULL;
}
-gboolean rauc_install(const gchar *bundle, const gchar *auth_header, gboolean ssl_verify,
+gboolean rauc_install(const gchar *bundle, const gchar *auth_header,
+ gchar *ssl_key, gchar *ssl_cert, gboolean ssl_verify,
GSourceFunc on_install_notify, GSourceFunc on_install_complete,
gboolean wait)
{
@@ -213,6 +219,8 @@ gboolean rauc_install(const gchar *bundle, const gchar *auth_header, gboolean ss
context = install_context_new();
context->bundle = g_strdup(bundle);
context->auth_header = g_strdup(auth_header);
+ context->ssl_key = ssl_key,
+ context->ssl_cert = ssl_cert,
context->ssl_verify = ssl_verify;
context->notify_event = on_install_notify;
context->notify_complete = on_install_complete;
diff --git a/test/conftest.py b/test/conftest.py
index 38b7f418..4e0dea55 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -9,7 +9,7 @@
import pytest
from hawkbit_mgmt import HawkbitMgmtTestClient, HawkbitError
-from helper import run_pexpect, available_port
+from helper import run_pexpect, available_port, run
def pytest_addoption(parser):
"""Register custom argparse-style options."""
@@ -103,8 +103,11 @@ def _adjust_config(options={'client': {}}, remove={}, add_trailing_space=False):
adjusted_config.set(section, key, value)
# remove
- for section, option in remove.items():
- adjusted_config.remove_option(section, option)
+ for section, options in remove.items():
+ if isinstance(options, str):
+ options = [options]
+ for option in options:
+ adjusted_config.remove_option(section, option)
# add trailing space
if add_trailing_space:
@@ -242,28 +245,72 @@ def nginx_config(tmp_path_factory):
http {{
access_log /dev/null;
-
+ map $ssl_client_s_dn $ssl_client_s_dn_cn {{
+ default "";
+ ~CN=(?[^,]+) $CN;
+ }}
+ {server}
+}}"""
+ http_server = """
server {{
listen {port};
listen [::]:{port};
+ {server_options}
location / {{
proxy_pass http://localhost:8080;
+
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto http;
+ proxy_set_header X-Forwarded-Port {port};
{location_options}
+ }}
+ }}"""
+ mtls_server = """
+ server {{
+ listen {port} ssl;
+ listen [::]:{port} ssl;
+
+ ssl_verify_client on;
+ ssl_verify_depth 3;
+ {server_options}
+
+ location ~*/.*/controller/ {{
+ proxy_pass http://localhost:8080;
- # use proxy URL in JSON responses
- sub_filter "localhost:$proxy_port/" "$host:$server_port/";
- sub_filter "$host:$proxy_port/" "$host:$server_port/";
- sub_filter_types application/json;
- sub_filter_once off;
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_set_header X-Forwarded-Port {port};
+ proxy_set_header X-Forwarded-Protocol https;
+
+ proxy_set_header X-Ssl-Client-Cn $ssl_client_s_dn_cn;
+
+ # These are required for clients to upload and download software.
+ proxy_request_buffering off;
+ client_max_body_size 1000m;
+ {location_options}
}}
- }}
-}}"""
+ }}"""
+
- def _nginx_config(port, location_options):
+ def _to_nginx_option(option:dict):
+ if not option:
+ return ""
+ key_values = (f'{key} {value};' for key, value in option.items())
+ return " ".join(key_values)
+
+ def _nginx_config(port, location_options, server_options=None, mtls=False):
+ server_config = mtls_server if mtls else http_server
+ server_config_str = server_config.format(
+ port=port, location_options=_to_nginx_option(location_options),
+ server_options=_to_nginx_option(server_options))
proxy_config = tmp_path_factory.mktemp('nginx') / 'nginx.conf'
- location_options = ( f'{key} {value};' for key, value in location_options.items())
- proxy_config_str = config_template.format(port=port, location_options=" ".join(location_options))
+ proxy_config_str = config_template.format(
+ port=port, server=server_config_str)
proxy_config.write_text(proxy_config_str)
return proxy_config
@@ -281,9 +328,9 @@ def nginx_proxy(nginx_config):
procs = []
- def _nginx_proxy(options):
+ def _nginx_proxy(options, server_options=None, mtls=False):
port = available_port()
- proxy_config = nginx_config(port, options)
+ proxy_config = nginx_config(port, options, server_options, mtls)
try:
proc = run_pexpect(f'nginx -c {proxy_config} -p .', timeout=None)
@@ -332,3 +379,57 @@ def partial_download_port(nginx_proxy):
'limit_rate': '70k',
}
return nginx_proxy(location_options)
+
+@pytest.fixture
+def mtls_config(tmp_path_factory):
+ class MtlsConfig:
+ def __init__(self, pki_dir_location):
+ self.pki_dir= str(pki_dir_location) + "/pki/"
+ self.ca_cert= self.pki_dir + "root-ca.crt"
+ self.ca_key= self.pki_dir + "root-ca.key"
+ self.ca_csr= self.pki_dir + "root-csr.pem"
+ self.client_cert= self.pki_dir + "client.crt"
+ self.client_key= self.pki_dir + "client.key"
+ self.issuer_hash = self. pki_dir + "issuer_hash.txt"
+
+ def client_cert_exist(self):
+ return os.path.isfile(self.client_cert)
+
+ def ca_cert_exist(self):
+ return os.path.isfile(self.ca_cert)
+
+ def get_issuer_hash(self):
+ return open(self.issuer_hash, "r").readline().strip()
+
+ return MtlsConfig(tmp_path_factory.getbasetemp())
+
+@pytest.fixture
+def mtls_certificates(mtls_config, hawkbit_target_added):
+ """
+ Generate CA cert and key if they don't exist and also generate specific client cert and key
+ for the Hawkbit controller id as Common Name.
+ """
+ def _mtls_certificates():
+ out, err, exitcode = run(f'{os.path.dirname(__file__)}/gen_pki.sh {mtls_config.pki_dir} {hawkbit_target_added}', timeout=20)
+ assert exitcode == 0
+ assert mtls_config.ca_cert_exist()
+ assert mtls_config.client_cert_exist()
+ return mtls_config
+ return _mtls_certificates
+
+@pytest.fixture
+def mtls_download_port(nginx_proxy, mtls_certificates):
+ """
+ Runs an nginx proxy. HTTPS requests are forwarded to port 8080
+ (default port of the docker hawkBit instance). Returns the port the proxy is running on. This
+ port can be set in the rauc-hawkbit-updater config to test partial downloads.
+ """
+ mtls_cert = mtls_certificates()
+ hash_issuer = mtls_cert.get_issuer_hash()
+ location_options = {"proxy_set_header X-Ssl-Issuer-Hash-1": hash_issuer}
+ server_options = {
+ "ssl_certificate": mtls_cert.ca_cert,
+ "ssl_certificate_key": mtls_cert.ca_key,
+ "ssl_client_certificate": mtls_cert.ca_cert,
+ }
+ return nginx_proxy(location_options, server_options, mtls=True)
diff --git a/test/gen_pki.sh b/test/gen_pki.sh
new file mode 100755
index 00000000..a6c3a955
--- /dev/null
+++ b/test/gen_pki.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+set -e
+CERT_DIR="${1}"
+CONTROLLER_ID="${2}"
+CA_CERT="root-ca.crt"
+CA_KEY="root-ca.key"
+CA_CSR="root-csr.pem"
+CERT_CONFIG='
+ [client]
+ basicConstraints = CA:FALSE
+ nsCertType = client, email
+ nsComment = "Local Test Client Certificate"
+ subjectKeyIdentifier = hash
+ authorityKeyIdentifier = keyid,issuer
+ keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
+ extendedKeyUsage = clientAuth
+'
+
+mkdir -p ${CERT_DIR}
+cd ${CERT_DIR}
+if [ ! -f "${CA_CERT}" ]; then
+ echo "Development CA"
+ openssl req -newkey rsa -keyout ${CA_KEY} -out ${CA_CSR} -subj "/O=Test/CN=localhost" --nodes
+ openssl req -x509 -sha256 -new -nodes -key ${CA_KEY} -days 3650 -out ${CA_CERT} -subj '/CN=localhost'
+fi
+if [ -n "${CONTROLLER_ID}" ]; then
+ openssl genrsa -out "client.key" 4096
+ openssl req -new -key "client.key" -out "client.csr" -sha256 -subj "/CN=${CONTROLLER_ID}"
+
+ openssl x509 -req -days 750 -in "client.csr" -sha256 -CA ${CA_CERT} -CAkey ${CA_KEY} -CAcreateserial -out "client.crt" -extensions client -extfile <(printf ${CERT_CONFIG})
+ openssl x509 -in client.crt -issuer_hash -noout > issuer_hash.txt
+fi
diff --git a/test/rauc_dbus_dummy.py b/test/rauc_dbus_dummy.py
index 4f949adc..63e52e58 100755
--- a/test/rauc_dbus_dummy.py
+++ b/test/rauc_dbus_dummy.py
@@ -103,7 +103,7 @@ def _get_bundle_sha1(bundle):
return sha1.hexdigest()
@staticmethod
- def _get_http_bundle_sha1(url, auth_header):
+ def _get_http_bundle_sha1(url, auth_header, cert=None, verify=True):
"""Download file from URL using HTTP range requests and compute its sha1 checksum."""
sha1 = hashlib.sha1()
headers = auth_header
@@ -112,7 +112,7 @@ def _get_http_bundle_sha1(url, auth_header):
offset = 0
while True:
headers['Range'] = f'bytes={offset}-{offset + range_size - 1}'
- r = requests.get(url, headers=headers)
+ r = requests.get(url, headers=headers, cert=cert, verify=verify)
try:
r.raise_for_status()
sha1.update(r.content)
@@ -130,20 +130,30 @@ def _check_install_requirements(self, source, args):
Check that required headers are set, bundle is accessible (HTTP or locally) and its
checksum matches.
"""
- if 'http-headers' in args:
+ if 'tls-key' in args and 'tls-cert' in args:
+ cert = (args['tls-cert'], args['tls-key'])
+ if 'tls-no-verify' in args and args['tls-no-verify']:
+ verify = False
+ elif 'tls-ca' in args:
+ verify = args['tls-ca']
+ else:
+ verify = True
+ source_sha1 = self._get_http_bundle_sha1(source, {}, cert, verify)
+
+ elif 'http-headers' in args:
assert len(args['http-headers']) == 1
[auth_header] = args['http-headers']
key, value = auth_header.split(': ', maxsplit=1)
- http_bundle_sha1 = self._get_http_bundle_sha1(source, {key: value})
- assert http_bundle_sha1 == self._get_bundle_sha1(self._bundle)
+ source_sha1 = self._get_http_bundle_sha1(source, {key: value})
# assume ssl_verify=false is set in test setup
assert args['tls-no-verify'] is True
-
else:
- # check bundle checksum matches expected checksum
- assert self._get_bundle_sha1(source) == self._get_bundle_sha1(self._bundle)
+ source_sha1 = self._get_bundle_sha1(source)
+
+ # check bundle checksum matches expected checksum
+ assert source_sha1 == self._get_bundle_sha1(self._bundle)
@property
def Operation(self):
diff --git a/test/test_basics.py b/test/test_basics.py
index f4024558..91e4608e 100644
--- a/test/test_basics.py
+++ b/test/test_basics.py
@@ -41,8 +41,8 @@ def test_config_file_non_existent():
assert out == ''
assert err.strip() == 'No such configuration file: does-not-exist.conf'
-def test_config_no_auth_token(adjust_config):
- """Test config without auth_token option in client section."""
+def test_config_no_auth(adjust_config):
+ """Test config without authentication option in client section."""
config = adjust_config(remove={'client': 'auth_token'})
out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r')
@@ -50,9 +50,9 @@ def test_config_no_auth_token(adjust_config):
assert exitcode == 4
assert out == ''
assert err.strip() == \
- "Loading config file failed: Neither 'auth_token' nor 'gateway_token' set"
+ "Loading config file failed: Neither token nor ssl authentication set"
-def test_config_multiple_auth_methods(adjust_config):
+def test_config_multiple_token_auth_methods(adjust_config):
"""Test config with auth_token and gateway_token options in client section."""
config = adjust_config({'client': {'gateway_token': 'wrong-gateway-token'}})
@@ -63,6 +63,19 @@ def test_config_multiple_auth_methods(adjust_config):
assert err.strip() == \
"Loading config file failed: Both 'auth_token' and 'gateway_token' set"
+def test_config_multiple_auth_methods(adjust_config):
+ """Test config with both token and ssl auth options in client section."""
+ config = adjust_config(
+ {'client': {'ssl': 'true', 'ssl_key': 'key', 'ssl_cert': 'cert'}}
+ )
+
+ out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r')
+
+ assert exitcode == 4
+ assert out == ''
+ assert err.strip() == \
+ "Loading config file failed: Both token and ssl authentication set"
+
def test_register_and_check_invalid_gateway_token(adjust_config):
"""Test config with invalid gateway_token."""
config = adjust_config(
diff --git a/test/test_mtls.py b/test/test_mtls.py
new file mode 100644
index 00000000..ab827ee9
--- /dev/null
+++ b/test/test_mtls.py
@@ -0,0 +1,40 @@
+import pytest
+
+from helper import run
+
+@pytest.mark.parametrize('mode', ('download','streaming'))
+def test_install_success_mtls(hawkbit, adjust_config, bundle_assigned,
+ mtls_download_port, mtls_config,
+ rauc_dbus_install_success, mode):
+ """
+ Assign bundle to target and test successful download and installation via MTLS. Make sure installation
+ result is received correctly by hawkBit.
+ """
+ config = adjust_config(
+ {'client': {
+ 'hawkbit_server': f'localhost:{mtls_download_port}',
+ "ssl": "true",
+ "ssl_key": mtls_config.client_key,
+ "ssl_cert": mtls_config.client_cert,
+ "ssl_verify": "false",
+ 'stream_bundle': 'true' if mode == 'streaming' else "false"}
+ },
+ remove={'client': ['bundle_download_location','auth_token']} if mode == 'streaming'else {'client':'auth_token'}
+ )
+
+ hawkbit.set_config("authentication.header.authority", mtls_config.get_issuer_hash())
+ hawkbit.set_config("authentication.header.enabled",True)
+
+ out, err, exitcode = run(f'rauc-hawkbit-updater -c "{config}" -r')
+
+ assert 'New software ready for download' in out
+
+ if mode == 'download':
+ assert 'Download complete' in out
+
+ assert 'Software bundle installed successfully.' in out
+ assert err == ''
+ assert exitcode == 0
+
+ status = hawkbit.get_action_status()
+ assert status[0]['type'] == 'finished'