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

Implemented artifact integrity checks #89

Open
wants to merge 7 commits into
base: main
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
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ if (NOT CONFIG_MENDER_PLATFORM_TLS_TYPE)
else()
message(STATUS "Using custom '${CONFIG_MENDER_PLATFORM_TLS_TYPE}' platform TLS implementation")
endif()
if (NOT CONFIG_MENDER_PLATFORM_SHA_TYPE)
message(STATUS "Using default 'generic/weak' platform SHA implementation")
set(CONFIG_MENDER_PLATFORM_SHA_TYPE "generic/weak")
else()
message(STATUS "Using custom '${CONFIG_MENDER_PLATFORM_SHA_TYPE}' platform SHA implementation")
endif()

option(MENDER_MBEDTLS_ERROR_STR "Enable mbedtls error strings" OFF)

Expand Down Expand Up @@ -168,6 +174,7 @@ file(GLOB SOURCES_TEMP
"${CMAKE_CURRENT_LIST_DIR}/platform/scheduler/${CONFIG_MENDER_PLATFORM_SCHEDULER_TYPE}/src/mender-scheduler.c"
"${CMAKE_CURRENT_LIST_DIR}/platform/storage/${CONFIG_MENDER_PLATFORM_STORAGE_TYPE}/src/mender-storage.c"
"${CMAKE_CURRENT_LIST_DIR}/platform/tls/${CONFIG_MENDER_PLATFORM_TLS_TYPE}/src/mender-tls.c"
"${CMAKE_CURRENT_LIST_DIR}/platform/sha/${CONFIG_MENDER_PLATFORM_SHA_TYPE}/src/mender-sha.c"
)
if (CONFIG_MENDER_CLIENT_INVENTORY)
list(APPEND SOURCES_TEMP
Expand Down
226 changes: 210 additions & 16 deletions core/src/mender-artifact.c
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ static mender_err_t mender_artifact_read_data(mender_artifact_ctx_t *ctx, mender
*/
static mender_err_t mender_artifact_drop_file(mender_artifact_ctx_t *ctx);

#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
static mender_err_t mender_artifact_read_header(mender_artifact_ctx_t *ctx);
#endif /* CONFIG_MENDER_FULL_PARSE_ARTIFACT */

/**
* @brief Shift data after parsing
* @param ctx Artifact context
Expand All @@ -150,6 +154,108 @@ static size_t mender_artifact_round_up(size_t length, size_t incr);
*/
static mender_artifact_ctx_t *mender_artifact_ctx = NULL;

#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
/**
* @brief Get checksum entry for a file in the context
* @param ctx The mender artifact context
* @param filename The name of the file in the artifact
* @return The checksum entry or NULL on error.
* @note Since other files may be parsed before the manifest file, we need to
* create these entries in a lazy fashion.
*/
static mender_artifact_checksum_t *
mender_artifact_checksum_get_or_create(mender_artifact_ctx_t *ctx, const char *filename) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

(I know this is totally out of the current PR, fell free to ignore. It is Friday and I am feeling creative...)

Did we land on a naming convention for static (local) functions? I don't have an opinion on which convention, but I have an opinion on having one convention.

In this very file we already have static functions with no mender_artifact prefix, for example is_compressed.

If we want to keep the mender_artifact in the name, some ideas could be:

  • To add a final underscore: mender_artifact_checksum_get_or_create_
  • To add a local prefix: s_mender_artifact_checksum_get_or_create
  • To add extra underscore between module and function: mender_artifact__checksum_get_or_create
  • To ignore this comment and leave it as-is 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

With static symbols, we don't have to worry about conflicting names. Thus we can safely omit the mender_ prefix. I can fix this in a separate commit. I agree that we should stick to one convention.

assert(NULL != ctx);
assert(NULL != filename);

/* See if we already have an entry for this file */
mender_artifact_checksum_t *checksum;
for (checksum = ctx->artifact_info.checksums; NULL != checksum; checksum = checksum->next) {
if (StringEqual(checksum->filename, filename)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure about using the filename as unique ID.

A tar archive does not guarantee uniqueness of filenames, so in theory we could have more than entry with same filename but different checksum. Our mender-artifact cli tool however will prevent this to happen, so we are only talking about a manually crafted Artifact or a 3rd party tool creating the Artifact.

Can you elaborate on the motivation? Can't we use good old indexes? It is a list after all.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The goal is to compare the computed checksums to those in the manifest. The manifest contains pairs of checksums and a filenames. No reference to indexes. So I'm not sure how we could do this without changing the artifact format.

break;
}
}

if (NULL == checksum) {
/* Create new if entry not found */
checksum = (mender_artifact_checksum_t *)calloc(1, sizeof(mender_artifact_checksum_t));
if (NULL == checksum) {
mender_log_error("Unable to allocate memory");
return NULL;
}
checksum->filename = strdup(filename);
if (NULL == checksum->filename) {
mender_log_error("Unable to allocate memory");
free(checksum);
return NULL;
}
checksum->next = ctx->artifact_info.checksums;
ctx->artifact_info.checksums = checksum;

/* Start SHA-256 checksum computation (if not already started) */
Copy link
Collaborator

Choose a reason for hiding this comment

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

What do you mean with "if not already started"? At this point it is starting it unconditionally

Copy link
Collaborator Author

@larsewi larsewi Oct 4, 2024

Choose a reason for hiding this comment

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

Good catch! It's some leftovers from an earlier iteration of trying to get this thing to work. I will remove the last part ;)

if (MENDER_OK != mender_sha256_begin(&(checksum->context))) {
mender_log_error("Failed to start checksum");
Copy link
Collaborator

Choose a reason for hiding this comment

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

... for entry %s, filename

return NULL;
}
}

return checksum;
}
#endif /* CONFIG_MENDER_FULL_PARSE_ARTIFACT */

mender_err_t
mender_artifact_check_integrity(mender_artifact_ctx_t *ctx) {
assert(NULL != ctx);

#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
for (mender_artifact_checksum_t *checksum = ctx->artifact_info.checksums; NULL != checksum; checksum = checksum->next) {
unsigned char computed[MENDER_DIGEST_BUFFER_SIZE];

if (!StringEqual(checksum->filename, "version") && !StringEqual(checksum->filename, "header.tar")
&& !mender_utils_strbeginwith(checksum->filename, "data/")) {
/* Whenever we introduce a new file to the artifact manifest, we
* need to skip integrity checks for that file until it's
* implemented. Otherwise, the client will get stuck trying to
* install the artifact which may hold the new implementation. */
mender_log_warning("Skipping integrity check for artifact file '%s': Not implemented", checksum->filename);
continue;
}
Comment on lines +214 to +222
Copy link
Collaborator

Choose a reason for hiding this comment

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

This sounds insecure to me, why will it get "stuck"?

If we introduce new fields in the manifest, we will in order do

  1. Update Artifact format specification, likely publishing a new version
  2. Implement the changes in the client for the new spec.
  3. Assume the client is updated if you are sending a Artifact with the new spec

All in all I think we should gracefully fail the deployment. On a breaking change like this, the mender-artifact tool will provide a way to produce "legacy Artifacts" so that you can update your devices in the field to newer clients before sending new Artifacts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Let's discuss this 😄2😄 on Monday with a ☕

mender_log_debug("Checking integrity for artifact file '%s'", checksum->filename);

if (MENDER_OK != mender_sha256_finish(checksum->context, computed)) {
mender_log_error("Failed to finish checksum for file '%s'", checksum->filename);
checksum->context = NULL;
return MENDER_FAIL;
}
checksum->context = NULL;

if (0 != memcmp(checksum->manifest, computed, MENDER_DIGEST_BUFFER_SIZE)) {
mender_log_error("Computed checksum for file '%s' does not match manifest", checksum->filename);
#if CONFIG_MENDER_LOG_LEVEL >= MENDER_LOG_LEVEL_DBG
/* Log the mismatching checksums for debugging */
char checksum_str[(MENDER_DIGEST_BUFFER_SIZE * 2) + 1];

for (int i = 0; i < MENDER_DIGEST_BUFFER_SIZE; i++) {
if (2 != snprintf(checksum_str + (i * 2), 3, "%02hhx", checksum->manifest[i])) {
break;
}
}
mender_log_debug("%s: '%s' (manifest)", checksum->filename, checksum_str);

for (int i = 0; i < MENDER_DIGEST_BUFFER_SIZE; i++) {
if (2 != snprintf(checksum_str + (i * 2), 3, "%02hhx", computed[i])) {
break;
}
}
mender_log_debug("%s: '%s' (computed)", checksum->filename, checksum_str);
#endif /* CONFIG_MENDER_LOG_LEVEL >= MENDER_LOG_LEVEL_DBG */
return MENDER_FAIL;
}
}
#endif /* CONFIG_MENDER_FULL_PARSE_ARTIFACT */
return MENDER_OK;
}

mender_artifact_ctx_t *
mender_artifact_create_ctx(void) {

Expand Down Expand Up @@ -270,7 +376,10 @@ mender_artifact_process_data(mender_artifact_ctx_t *ctx,

/* Drop data, file is not relevant */
ret = mender_artifact_drop_file(ctx);

#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
} else if (StringEqual(ctx->file.name, "header.tar")) {
ret = mender_artifact_read_header(ctx);
#endif /* CONFIG_MENDER_FULL_PARSE_ARTIFACT */
} else {

/* Nothing to do */
Expand Down Expand Up @@ -325,7 +434,10 @@ mender_artifact_release_ctx(mender_artifact_ctx_t *ctx) {
#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
mender_utils_free_linked_list(ctx->artifact_info.provides);
mender_utils_free_linked_list(ctx->artifact_info.depends);
mender_utils_free_linked_list(ctx->artifact_info.checksums);
for (mender_artifact_checksum_t *checksum = ctx->artifact_info.checksums; NULL != checksum; checksum = checksum->next) {
free(checksum->filename);
mender_sha256_finish(checksum->context, NULL);
}
#endif
free(ctx);
}
Expand Down Expand Up @@ -440,6 +552,21 @@ mender_artifact_read_version(mender_artifact_ctx_t *ctx) {
return MENDER_OK;
}

#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
/* Get checksum entry (create one if needed) */
mender_artifact_checksum_t *checksum;
if (NULL == (checksum = mender_artifact_checksum_get_or_create(ctx, "version"))) {
/* Error already logged */
return MENDER_FAIL;
}

/* Update SHA-256 checksum */
if (MENDER_OK != mender_sha256_update(checksum->context, ctx->input.data, ctx->file.size)) {
mender_log_error("Failed to update update checksum");
return MENDER_FAIL;
}
#endif /* CONFIG_MENDER_FULL_PARSE_ARTIFACT */

/* Check version file */
if (NULL == (object = cJSON_ParseWithLength(ctx->input.data, ctx->file.size))) {
mender_log_error("Unable to allocate memory");
Expand Down Expand Up @@ -532,31 +659,40 @@ mender_artifact_read_manifest(mender_artifact_ctx_t *ctx) {
}
*next = '\0';

///* Process line */
/* Process line */
char *separator = strstr(line, " ");
if (NULL == separator) {
mender_log_error("Invalid manifest file");
return MENDER_FAIL;
}
*separator = '\0';

/* Add checksum to the list */
mender_key_value_list_t *checksum = (mender_key_value_list_t *)calloc(1, sizeof(mender_key_value_list_t));
if (NULL == checksum) {
mender_log_error("Unable to allocate memory");
const char *checksum_str = line;
larsewi marked this conversation as resolved.
Show resolved Hide resolved
const char *filename = separator + 2;

/* Useful when debugging artifact integrity check failures */
mender_log_debug("%s %s", checksum_str, filename);

/* Make sure digest is of expected length (two hex per byte) */
if ((MENDER_DIGEST_BUFFER_SIZE * 2) != strlen(checksum_str)) {
mender_log_error("Bad checksum '%s' in manifest for file '%s'", checksum_str, filename);
return MENDER_FAIL;
}
*separator = '\0';

/* Allocate memory and check if allocation was succesfull */
checksum->key = strdup(line);
checksum->value = strdup(separator + 2);
if ((NULL == checksum->key) || (NULL == checksum->value)) {
mender_log_error("Unable to allocate memory");
mender_utils_free_linked_list(checksum);
/* Get checksum entry for the file (creates one if not found) */
mender_artifact_checksum_t *checksum;
if (NULL == (checksum = mender_artifact_checksum_get_or_create(ctx, filename))) {
/* Error already logged */
return MENDER_FAIL;
}
checksum->next = ctx->artifact_info.checksums;
ctx->artifact_info.checksums = checksum;

/* Populate with manifest checksum */
for (int i = 0; i < MENDER_DIGEST_BUFFER_SIZE; i++) {
if (1 != sscanf(checksum_str + (2 * i), "%02hhx", checksum->manifest + i)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Won't we hit the same problems with sscanf on ESP32?

Copy link
Collaborator Author

@larsewi larsewi Oct 4, 2024

Choose a reason for hiding this comment

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

Worked on my machine 😉 I used the ESP32-S3-DevKitC. Could be that it is only buggy for some format specifiers. Can you try on the ESP32-Ethernet-Kit?

mender_log_error("Bad checksum '%s' in manifest for file '%s'", checksum_str, filename);
return MENDER_FAIL;
}
}

///* Move to the next line */
line = next + 1;
Expand Down Expand Up @@ -699,6 +835,33 @@ mender_artifact_read_header_info(mender_artifact_ctx_t *ctx) {
return ret;
}

#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
static mender_err_t
mender_artifact_read_header(mender_artifact_ctx_t *ctx) {
assert(NULL != ctx);

/* Check if all data have been received */
if ((NULL == ctx->input.data) || (ctx->input.length < mender_artifact_round_up(ctx->file.size, MENDER_ARTIFACT_STREAM_BLOCK_SIZE))) {
return MENDER_OK;
}

/* Get checksum entry (create one if needed) */
mender_artifact_checksum_t *checksum;
if (NULL == (checksum = mender_artifact_checksum_get_or_create(ctx, "header.tar"))) {
/* Error already logged */
return MENDER_FAIL;
}

/* Update SHA-256 checksum */
if (MENDER_OK != mender_sha256_update(checksum->context, ctx->input.data, ctx->file.size)) {
mender_log_error("Failed to update update checksum");
return MENDER_FAIL;
}

return MENDER_DONE;
}
#endif /* CONFIG_MENDER_FULL_PARSE_ARTIFACT */

#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
static mender_err_t
mender_artifact_read_type_info(mender_artifact_ctx_t *ctx) {
Expand Down Expand Up @@ -910,6 +1073,37 @@ mender_artifact_read_data(mender_artifact_ctx_t *ctx, mender_err_t (*callback)(c
size_t length
= ((ctx->file.size - ctx->file.index) > MENDER_ARTIFACT_STREAM_BLOCK_SIZE) ? MENDER_ARTIFACT_STREAM_BLOCK_SIZE : (ctx->file.size - ctx->file.index);

#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
mender_artifact_checksum_t *checksum;
{
/* The filename will be something like
* 'data/0000.tar/zephyr.signed.bin'. But the manifest will hold
* 'data/0000/zephyr.signed.bin'. Hence, we need to remove the
* '.tar' extension from the string.
*/
char filename[strlen(ctx->file.name) + 1];
strcpy(filename, ctx->file.name);

for (char *ch = strstr(filename, ".tar"); (NULL != ch) && (*ch != '\0'); ch++) {
/* Don't worry! The call to strlen() on a static string should
* be optimized out by the compiler */
Copy link
Collaborator

Choose a reason for hiding this comment

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

😁 👍

*ch = ch[strlen(".tar")];
}

/* Get checksum entry (create one if needed) */
if (NULL == (checksum = mender_artifact_checksum_get_or_create(ctx, filename))) {
/* Error already logged */
return MENDER_FAIL;
}
}

/* Update SHA-256 checksum */
if (MENDER_OK != mender_sha256_update(checksum->context, ctx->input.data, length)) {
mender_log_error("Failed to update update checksum");
return MENDER_FAIL;
}
#endif /* CONFIG_MENDER_FULL_PARSE_ARTIFACT */

/* Invoke callback */
if (MENDER_OK
!= (ret = callback(ctx->payloads.values[index].type,
Expand Down
32 changes: 17 additions & 15 deletions core/src/mender-client.c
Original file line number Diff line number Diff line change
Expand Up @@ -990,24 +990,26 @@ mender_client_update_work_function(void) {
/* Get artifact context if artifact download succeeded */
if ((NULL != mender_update_module) && (MENDER_OK == (ret = mender_artifact_get_ctx(&mender_artifact_ctx)))) {
#ifdef CONFIG_MENDER_FULL_PARSE_ARTIFACT
if (MENDER_OK == (ret = mender_check_artifact_requirements(mender_artifact_ctx, deployment))) {
if (MENDER_OK == (ret = mender_artifact_check_integrity(mender_artifact_ctx))) {
if (MENDER_OK == (ret = mender_check_artifact_requirements(mender_artifact_ctx, deployment))) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I feel like those two should be in a single function, a new one I guess.

#ifdef CONFIG_MENDER_PROVIDES_DEPENDS
/* Add the new provides to the deployment data (we need the artifact context) */
char *new_provides = NULL;
const char *artifact_name = NULL;
if (MENDER_OK == (ret = mender_prepare_new_provides(mender_artifact_ctx, &new_provides, &artifact_name))) {
if (MENDER_OK != (ret = mender_deployment_data_set_provides(mender_client_deployment_data, new_provides))) {
mender_log_error("Failed to set deployment data provides");
/* Add the new provides to the deployment data (we need the artifact context) */
char *new_provides = NULL;
const char *artifact_name = NULL;
if (MENDER_OK == (ret = mender_prepare_new_provides(mender_artifact_ctx, &new_provides, &artifact_name))) {
if (MENDER_OK != (ret = mender_deployment_data_set_provides(mender_client_deployment_data, new_provides))) {
mender_log_error("Failed to set deployment data provides");
}
/* Replace artifact_name with the one from provides */
else if (MENDER_OK != (ret = mender_deployment_data_set_artifact_name(mender_client_deployment_data, artifact_name))) {
mender_log_error("Failed to set deployment data artifact name");
}
free(new_provides);
} else {
mender_log_error("Unable to prepare new provides");
}
/* Replace artifact_name with the one from provides */
else if (MENDER_OK != (ret = mender_deployment_data_set_artifact_name(mender_client_deployment_data, artifact_name))) {
mender_log_error("Failed to set deployment data artifact name");
}
free(new_provides);
} else {
mender_log_error("Unable to prepare new provides");
}
#endif /* CONFIG_MENDER_PROVIDES_DEPENDS */
}
}
#endif /* CONFIG_MENDER_FULL_PARSE_ARTIFACT */
} else {
Expand Down
Loading