From ba2ee23dbe8c2836219c0fd7b9f96a47141617ca Mon Sep 17 00:00:00 2001 From: Robin van der Gracht Date: Tue, 22 Aug 2023 14:09:50 +0200 Subject: [PATCH] Add support for SSL/TLS client authentication authentication 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 --- README.md | 3 ++ config.conf.example | 5 ++++ docs/using.rst | 26 +++++++++++++++++ include/config-file.h | 3 ++ src/config-file.c | 46 +++++++++++++++++++++++++----- src/hawkbit-client.c | 66 +++++++++++++++++++++++++++++++++++++++++-- test/test_basics.py | 21 +++++++++++--- 7 files changed, 157 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1331f7ca..36201234 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 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/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..eac71fd4 100644 --- a/src/hawkbit-client.c +++ b/src/hawkbit-client.c @@ -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 && @@ -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); 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(