Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SSL/TLS client authentication #169

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions config.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions docs/using.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ Using the RAUC hawkbit Updater
Authentication
--------------

Target token
^^^^^^^^^^^^

As described on the `hawkBit Authentication page <https://eclipse.dev/hawkbit/concepts/authentication/>`_
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
"Gateway" token. The "Target" security token is specific to a single target
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
Expand All @@ -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
-----------------

Expand Down
3 changes: 3 additions & 0 deletions include/config-file.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
2 changes: 2 additions & 0 deletions include/hawkbit-client.h
Original file line number Diff line number Diff line change
Expand Up @@ -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!) */
};
Expand Down
7 changes: 6 additions & 1 deletion include/rauc-installer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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.
Expand All @@ -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__
46 changes: 39 additions & 7 deletions src/config-file.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
72 changes: 69 additions & 3 deletions src/hawkbit-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/rauc-hawkbit-updater.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 14 additions & 6 deletions src/rauc-installer.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down
Loading