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 option to authenticate via client certificates updated #166

Open
wants to merge 17 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
4 changes: 3 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ 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 \
--hawkbit.dmf.rabbitmq.enabled=false \
--server.forward-headers-strategy=NATIVE

- name: Run test suite
run: |
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Setup target (device) configuration file:
target_name = test-target
auth_token = bhVahL1Il1shie2aj2poojeChee6ahShu
#gateway_token = bhVahL1Il1shie2aj2poojeChee6ahShu
#client_cert = /path/to/client_certificate.pem
#client_key = /path/to/client_certificate.key
bundle_download_location = /tmp/bundle.raucb
retry_wait = 60
connect_timeout = 20
Expand Down Expand Up @@ -98,7 +100,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-dev
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ pip install --upgrade pip
Expand All @@ -111,7 +113,10 @@ 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 \
--hawkbit.dmf.rabbitmq.enabled=false \
--server.forward-headers-strategy=NATIVE \
--hawkbit.artifact.url.protocols.download-http.protocol=<https or http>
```

Run test suite:
Expand Down
6 changes: 6 additions & 0 deletions config.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ low_speed_rate = 0
# reboot after a successful update
post_update_reboot = false

# path to client certificate
client_cert =

# path to private key of client certificate
client_key =

# debug, info, message, critical, error, fatal
log_level = message

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

Authentication
--------------
Authentication via tokens
^^^^^^^^^^^^^^^^^^^^^^^^^

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
Expand All @@ -24,6 +26,70 @@ 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.

Authentication via Certificates
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
As can be seen in the system configuration settings of hawkBit, there is a
third option to authenticate the targets. An "Allow targets to authenticate via
a certificate authenticated by a reverse proxy" option. To use this
authentication method a TLS reverse proxy server needs to be set up.
The client and reverse proxy server perform a "SSL-handshake" that means the
client validates the server certificate of the reverse proxy server with its
list of trusted certificates.

The clients request has:

- to have a TLS connection to the reverse proxy server (`ssl` config option must be true)
- to contain the client certificate
- to have the common name of the server certificate match the server
name set in the configuration file as "hawkbit_server"

The purpose of the reverse proxy is to:

- disband the TLS connection
- check if sent client certificate is valid
- extract the common name and fingerprint of the client certificate
- forward the common name and fingerprint as HTTP headers to the
hawkBit server

When the hawkBit server receives the request it checks if:

- sent common name matches with the controller ID of the target
- sent fingerprint(s) matches the expected fingerprint(s) which is set
in the system configuration settings of hawkBit

The client certificate will only be used if a valid path
to a certificate and its key is given in the configuration file.
You can use token and certficate authentication mutualy, with the certificate being
used to authenticate to the reverse proxy and the token to authenticate to
hawkbit.

If the CA of the reverse proxy server is untrusted set ``ssl_verify`` to ``false``.

Here an example of how the configuration file might look like:

| [client]
| hawkbit_server = CN_server_certificate:443
| ssl = true
| ssl_verify = true
| tenant_id = DEFAULT
| target_name = test-target
| client_cert = /path/to/client_certificate.pem
| client_key = /path/to/client_certificate.key
| bundle_download_location = /tmp/bundle.raucb
| retry_wait = 60
| connect_timeout = 20
| timeout = 60
| log_level = debug
| post_update_reboot = false
|
| [device]
| product = Terminator
| model = T-1000
| serialnumber = 8922673153
| hw_revision = 2
| key1 = value
| key2 = value

Streaming Support
-----------------

Expand Down
2 changes: 2 additions & 0 deletions include/config-file.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ typedef struct Config_ {
gchar* tenant_id; /**< hawkBit tenant id */
gchar* controller_id; /**< hawkBit controller id*/
gchar* bundle_download_location; /**< file to download rauc bundle to */
gchar* client_cert; /**< path to the client certificate */
gchar* client_key; /**< path to the key of the client certificate */
int connect_timeout; /**< connection timeout */
int timeout; /**< reply timeout */
int retry_wait; /**< wait between retries */
Expand Down
1 change: 1 addition & 0 deletions include/hawkbit-client.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ struct on_new_software_userdata {
gchar *auth_header; /**< authentication header 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!) */
gboolean streaming_install; /**< whether the installation is a streaming install or not */
};

/**
Expand Down
3 changes: 2 additions & 1 deletion include/rauc-installer.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct install_context {
GMainLoop *mainloop; /**< The installation GMainLoop */
GMainContext *loop_context; /**< GMainContext for the GMainLoop */
gboolean keep_install_context; /**< Whether the installation thread should free this struct or keep it */
gboolean streaming_install; /**< Whether the installation is a streaming install or not */
};

/**
Expand All @@ -40,7 +41,7 @@ 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, gboolean ssl_verify, gboolean streaming_install,
GSourceFunc on_install_notify, GSourceFunc on_install_complete, gboolean wait);

#endif // __RAUC_INSTALLER_H__
47 changes: 37 additions & 10 deletions src/config-file.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include "config-file.h"
#include <glib/gtypes.h>
#include <stdlib.h>
#include <unistd.h>
#include <glib/gstdio.h>


static const gint DEFAULT_CONNECTTIMEOUT = 20; // 20 sec.
Expand Down Expand Up @@ -241,6 +243,9 @@ 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 key_client_cert_exists = FALSE;
gboolean key_client_key_exists = FALSE;
gboolean client_cert_auth = FALSE;

g_return_val_if_fail(config_file, NULL);
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
Expand All @@ -259,16 +264,9 @@ Config* load_config_file(const gchar *config_file, GError **error)
&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) {
g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
"Neither 'auth_token' nor 'gateway_token' set");
return NULL;
}
if (key_auth_token_exists && key_gateway_token_exists) {
g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
"Both 'auth_token' and 'gateway_token' set");
return NULL;
}
key_client_cert_exists = get_key_string(ini_file, "client", "client_cert", &config->client_cert, NULL, NULL);

key_client_key_exists = get_key_string(ini_file, "client", "client_key", &config->client_key, NULL, NULL);

if (!get_key_string(ini_file, "client", "target_name", &config->controller_id, NULL,
error))
Expand Down Expand Up @@ -324,6 +322,33 @@ Config* load_config_file(const gchar *config_file, GError **error)
"'bundle_download_location' is required if 'stream_bundle' is disabled");
return NULL;
}
if (key_client_key_exists && key_client_cert_exists) {
client_cert_auth = TRUE;
if (!config->ssl) {
g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
"'ssl' config option must be true for client certificate authentication");
return NULL;
}
if (g_access(config->client_cert, F_OK|R_OK)!=0) {
g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
"Can't read client_cert: %s",config->client_cert);
return NULL;
}
else if (g_access(config->client_key, F_OK|R_OK)!=0) {
g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
"Can't read client_key: %s",config->client_key);
return NULL;
}
}
if (!key_auth_token_exists && !key_gateway_token_exists && !(client_cert_auth)) {
g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE, "Neither a token nor client certificate are set!");
return NULL;
}
else if (key_auth_token_exists && key_gateway_token_exists) {
g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_INVALID_VALUE,
"Both 'auth_token' and 'gateway_token' set");
return NULL;
}

return g_steal_pointer(&config);
}
Expand All @@ -338,6 +363,8 @@ void config_file_free(Config *config)
g_free(config->tenant_id);
g_free(config->auth_token);
g_free(config->gateway_token);
g_free(config->client_cert);
g_free(config->client_key);
g_free(config->bundle_download_location);
if (config->device)
g_hash_table_destroy(config->device);
Expand Down
17 changes: 16 additions & 1 deletion src/hawkbit-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ static char* get_auth_header()
return g_strdup_printf("Authorization: GatewayToken %s",
hawkbit_config->gateway_token);

if (hawkbit_config->client_cert && hawkbit_config->client_key)
return NULL;

g_return_val_if_reached(NULL);
}

Expand Down Expand Up @@ -244,7 +247,7 @@ static gboolean set_auth_curl_header(struct curl_slist **headers, GError **error

/**
* @brief Set common Curl options, namely user agent, connect timeout, SSL
* verify peer and SSL verify host options.
* verify peer, SSL verify host options, and client certificate.
*
* @param[in] curl Curl handle
*/
Expand All @@ -256,6 +259,12 @@ static void set_default_curl_opts(CURL *curl)
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, hawkbit_config->connect_timeout);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, hawkbit_config->ssl_verify ? 1L : 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, hawkbit_config->ssl_verify ? 1L : 0L);

if (hawkbit_config->client_cert && hawkbit_config->client_key) {
curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM");
curl_easy_setopt(curl, CURLOPT_SSLCERT, hawkbit_config->client_cert);
curl_easy_setopt(curl, CURLOPT_SSLKEY, hawkbit_config->client_key);
Comment on lines +265 to +266
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existence of these files was never checked. What happens if they don't exist?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I add a check 118b83d

}
}

/**
Expand Down Expand Up @@ -418,6 +427,8 @@ static gboolean rest_request(enum HTTPMethod method, const gchar *url,
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fetch_buffer);

g_debug("Request method: %s url: %s",HTTPMethod_STRING[method], url);

if (jsonRequestBody) {
g_autoptr(JsonGenerator) generator = json_generator_new();
g_autoptr(JsonNode) req_root = json_builder_get_root(jsonRequestBody);
Expand Down Expand Up @@ -841,6 +852,7 @@ static gpointer download_thread(gpointer data)
.auth_header = NULL,
.ssl_verify = hawkbit_config->ssl_verify,
.install_success = FALSE,
.streaming_install = FALSE
};
g_autoptr(GError) error = NULL, feedback_error = NULL;
g_autofree gchar *msg = NULL, *sha1sum = NULL;
Expand Down Expand Up @@ -996,6 +1008,7 @@ static gboolean start_streaming_installation(Artifact *artifact, GError **error)
.auth_header = auth_header,
.ssl_verify = hawkbit_config->ssl_verify,
.install_success = FALSE,
.streaming_install = TRUE
};

// installation might already be canceled
Expand Down Expand Up @@ -1360,6 +1373,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?");
if (hawkbit_config->client_cert && hawkbit_config->client_key)
g_warning("Failed to authenticate. Check client certificate and client private key");
} 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 @@ -107,6 +107,7 @@ static gboolean on_new_software_ready_cb(gpointer data)
notify_hawkbit_install_complete = userdata->install_complete_callback;
userdata->install_success = rauc_install(userdata->file, userdata->auth_header,
userdata->ssl_verify,
userdata->streaming_install,
on_rauc_install_progress_cb,
on_rauc_install_complete_cb, run_once);

Expand Down
7 changes: 6 additions & 1 deletion src/rauc-installer.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ static gpointer install_loop_thread(gpointer data)
headers[0] = context->auth_header;
g_variant_dict_insert(&args, "http-headers", "^as", headers);

g_variant_dict_insert(&args, "tls-no-verify", "b", !context->ssl_verify);
} else if (context->streaming_install) {
gchar *headers[2] = {NULL, NULL};
g_variant_dict_insert(&args, "http-headers", "^as", headers);
g_variant_dict_insert(&args, "tls-no-verify", "b", !context->ssl_verify);
}

Expand Down Expand Up @@ -200,7 +204,7 @@ 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, gboolean ssl_verify, gboolean streaming_install,
GSourceFunc on_install_notify, GSourceFunc on_install_complete,
gboolean wait)
{
Expand All @@ -220,6 +224,7 @@ gboolean rauc_install(const gchar *bundle, const gchar *auth_header, gboolean ss
context->loop_context = loop_context;
context->status_result = 2;
context->keep_install_context = wait;
context->streaming_install = streaming_install;

// unref/free previous install thread by joining it
if (thread_install)
Expand Down
Loading