Skip to content

Commit

Permalink
Add support for SSL/TLS client authentication authentication
Browse files Browse the repository at this point in the history
Add support for mutual TLS authentication. This is the preferred method
of authentication for bosch-iot-suite and the only one that allows you to
keep the authenticator in a (f)TPM.

Optionally, an Openssl engine can be configured if required for access to
the ssl private key.

Signed-off-by: Robin van der Gracht <[email protected]>
  • Loading branch information
rvdgracht committed Jan 31, 2024
1 parent 76f5a1d commit ba2ee23
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 13 deletions.
3 changes: 3 additions & 0 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
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
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
66 changes: 64 additions & 2 deletions src/hawkbit-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -1360,6 +1420,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
21 changes: 17 additions & 4 deletions test/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,18 @@ 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')

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'}})

Expand All @@ -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(
Expand Down

0 comments on commit ba2ee23

Please sign in to comment.