| /* Copyright 2017 The Chromium OS Authors. All rights reserved. |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| /* Rollback protection logic. */ |
| |
| #include "common.h" |
| #include "console.h" |
| #include "flash.h" |
| #include "hooks.h" |
| #include "host_command.h" |
| #ifdef CONFIG_MPU |
| #include "mpu.h" |
| #endif |
| #include "rollback.h" |
| #include "sha256.h" |
| #include "system.h" |
| #include "task.h" |
| #include "trng.h" |
| #include "util.h" |
| |
| /* Console output macros */ |
| #define CPRINTS(format, args...) cprints(CC_SYSTEM, format, ## args) |
| |
| /* Number of rollback regions */ |
| #define ROLLBACK_REGIONS 2 |
| |
| /* |
| * Note: Do not change this structure without also updating |
| * common/firmware_image.S .image.ROLLBACK section. |
| */ |
| struct rollback_data { |
| int32_t id; /* Incrementing number to indicate which region to use. */ |
| int32_t rollback_min_version; |
| #ifdef CONFIG_ROLLBACK_SECRET_SIZE |
| uint8_t secret[CONFIG_ROLLBACK_SECRET_SIZE]; |
| #endif |
| /* cookie must always be last, as it validates the rest of the data. */ |
| uint32_t cookie; |
| }; |
| |
| /* We need at least 2 erasable blocks in the rollback region. */ |
| BUILD_ASSERT(CONFIG_ROLLBACK_SIZE >= ROLLBACK_REGIONS*CONFIG_FLASH_ERASE_SIZE); |
| BUILD_ASSERT(sizeof(struct rollback_data) <= CONFIG_FLASH_ERASE_SIZE); |
| |
| static uintptr_t get_rollback_offset(int region) |
| { |
| return CONFIG_ROLLBACK_OFF + region * CONFIG_FLASH_ERASE_SIZE; |
| } |
| |
| /* |
| * When MPU is available, read rollback with interrupts disabled, to minimize |
| * time protection is left open. |
| */ |
| static void lock_rollback(void) |
| { |
| #ifdef CONFIG_ROLLBACK_MPU_PROTECT |
| mpu_lock_rollback(1); |
| interrupt_enable(); |
| #endif |
| } |
| |
| static void unlock_rollback(void) |
| { |
| #ifdef CONFIG_ROLLBACK_MPU_PROTECT |
| interrupt_disable(); |
| mpu_lock_rollback(0); |
| #endif |
| } |
| |
| static int read_rollback(int region, struct rollback_data *data) |
| { |
| uintptr_t offset; |
| int ret = EC_SUCCESS; |
| |
| offset = get_rollback_offset(region); |
| |
| unlock_rollback(); |
| if (flash_read(offset, sizeof(*data), (char *)data)) |
| ret = EC_ERROR_UNKNOWN; |
| lock_rollback(); |
| |
| return ret; |
| } |
| |
| /* |
| * Get the most recent rollback information. |
| * |
| * @data: Returns most recent rollback data block. The data is filled |
| * with zeros if no valid rollback block is present |
| * |
| * Return most recent region index on success (>= 0, or 0 if no rollback |
| * region is valid), negative value on error. |
| */ |
| static int get_latest_rollback(struct rollback_data *data) |
| { |
| int region; |
| int min_region = -1; |
| int max_id = -1; |
| |
| for (region = 0; region < ROLLBACK_REGIONS; region++) { |
| struct rollback_data tmp_data; |
| |
| if (read_rollback(region, &tmp_data)) |
| return -1; |
| |
| /* Check if not initialized or invalid cookie. */ |
| if (tmp_data.cookie != CROS_EC_ROLLBACK_COOKIE) |
| continue; |
| |
| if (tmp_data.id > max_id) { |
| min_region = region; |
| max_id = tmp_data.id; |
| } |
| } |
| |
| if (min_region >= 0) { |
| if (read_rollback(min_region, data)) |
| return -1; |
| } else { |
| min_region = 0; |
| memset(data, 0, sizeof(*data)); |
| } |
| |
| return min_region; |
| } |
| |
| int32_t rollback_get_minimum_version(void) |
| { |
| struct rollback_data data; |
| |
| if (get_latest_rollback(&data) < 0) |
| return -1; |
| |
| return data.rollback_min_version; |
| } |
| |
| #ifdef CONFIG_ROLLBACK_SECRET_SIZE |
| int rollback_get_secret(uint8_t *secret) |
| { |
| struct rollback_data data; |
| uint8_t first; |
| int i = 0; |
| |
| if (get_latest_rollback(&data) < 0) |
| return EC_ERROR_UNKNOWN; |
| |
| /* Check that secret is not full of 0x00 or 0xff */ |
| first = data.secret[0]; |
| if (first == 0x00 || first == 0xff) { |
| for (i = 1; i < sizeof(data.secret); i++) { |
| if (data.secret[i] != first) |
| goto good; |
| } |
| return EC_ERROR_UNKNOWN; |
| } |
| |
| good: |
| memcpy(secret, data.secret, sizeof(data.secret)); |
| return EC_SUCCESS; |
| } |
| #endif |
| |
| int rollback_lock(void) |
| { |
| int ret; |
| |
| /* Already locked */ |
| if (flash_get_protect() & EC_FLASH_PROTECT_ROLLBACK_NOW) |
| return EC_SUCCESS; |
| |
| CPRINTS("Protecting rollback"); |
| |
| /* This may do nothing if WP is not enabled, or RO is not protected. */ |
| ret = flash_set_protect(EC_FLASH_PROTECT_ROLLBACK_AT_BOOT, -1); |
| |
| if (!(flash_get_protect() & EC_FLASH_PROTECT_ROLLBACK_NOW) && |
| flash_get_protect() & EC_FLASH_PROTECT_ROLLBACK_AT_BOOT) { |
| /* |
| * If flash protection is still not enabled (some chips may |
| * be able to enable it immediately), reboot. |
| */ |
| cflush(); |
| system_reset(SYSTEM_RESET_HARD | SYSTEM_RESET_PRESERVE_FLAGS); |
| } |
| |
| return ret; |
| } |
| |
| #ifdef CONFIG_ROLLBACK_UPDATE |
| |
| #ifdef CONFIG_ROLLBACK_SECRET_SIZE |
| static int add_entropy(uint8_t *dst, const uint8_t *src, |
| uint8_t *add, unsigned int add_len) |
| { |
| #ifdef CONFIG_SHA256 |
| BUILD_ASSERT(SHA256_DIGEST_SIZE == CONFIG_ROLLBACK_SECRET_SIZE); |
| struct sha256_ctx ctx; |
| uint8_t *hash; |
| #ifdef CONFIG_ROLLBACK_SECRET_LOCAL_ENTROPY_SIZE |
| uint8_t extra; |
| int i; |
| #endif |
| |
| SHA256_init(&ctx); |
| SHA256_update(&ctx, src, CONFIG_ROLLBACK_SECRET_SIZE); |
| SHA256_update(&ctx, add, add_len); |
| #ifdef CONFIG_ROLLBACK_SECRET_LOCAL_ENTROPY_SIZE |
| /* Add some locally produced entropy */ |
| for (i = 0; i < CONFIG_ROLLBACK_SECRET_LOCAL_ENTROPY_SIZE; i++) { |
| if (!board_get_entropy(&extra, 1)) |
| return 0; |
| SHA256_update(&ctx, &extra, 1); |
| } |
| #endif |
| hash = SHA256_final(&ctx); |
| |
| memcpy(dst, hash, CONFIG_ROLLBACK_SECRET_SIZE); |
| #else |
| #error "Adding entropy to secret in rollback region requires SHA256." |
| #endif |
| return 1; |
| } |
| #endif /* CONFIG_ROLLBACK_SECRET_SIZE */ |
| |
| /** |
| * Update rollback block. |
| * |
| * @param next_min_version Minimum version to update in rollback block. Can |
| * be a negative value if entropy is provided (in |
| * that case the current minimum version is kept). |
| * @param entropy Entropy to be added to rollback block secret |
| * (can be NULL, in that case no entropy is added). |
| * @param len entropy length |
| * |
| * @return EC_SUCCESS on success, EC_ERROR_* on error. |
| */ |
| static int rollback_update(int32_t next_min_version, |
| uint8_t *entropy, unsigned int length) |
| { |
| /* |
| * When doing flash_write operation, the data needs to be in blocks |
| * of CONFIG_FLASH_WRITE_SIZE, pad rollback_data as required. |
| */ |
| uint8_t block[CONFIG_FLASH_WRITE_SIZE * |
| DIV_ROUND_UP(sizeof(struct rollback_data), |
| CONFIG_FLASH_WRITE_SIZE)]; |
| struct rollback_data *data = (struct rollback_data *)block; |
| BUILD_ASSERT(sizeof(block) >= sizeof(*data)); |
| uintptr_t offset; |
| int region; |
| |
| if (flash_get_protect() & EC_FLASH_PROTECT_ROLLBACK_NOW) |
| return EC_ERROR_ACCESS_DENIED; |
| |
| /* Initialize the rest of the block. */ |
| memset(&block[sizeof(*data)], 0xff, sizeof(block)-sizeof(*data)); |
| |
| region = get_latest_rollback(data); |
| |
| if (region < 0) |
| return EC_ERROR_UNKNOWN; |
| |
| #ifdef CONFIG_ROLLBACK_SECRET_SIZE |
| if (entropy) { |
| /* Do not accept to decrease the value. */ |
| if (next_min_version < data->rollback_min_version) |
| next_min_version = data->rollback_min_version; |
| } else |
| #endif |
| { |
| /* Do not accept to decrease the value. */ |
| if (next_min_version < data->rollback_min_version) |
| return EC_ERROR_INVAL; |
| |
| /* No need to update if version is already correct. */ |
| if (next_min_version == data->rollback_min_version) |
| return EC_SUCCESS; |
| } |
| |
| /* Use the other region. */ |
| region = (region + 1) % ROLLBACK_REGIONS; |
| |
| offset = get_rollback_offset(region); |
| |
| data->id = data->id + 1; |
| data->rollback_min_version = next_min_version; |
| #ifdef CONFIG_ROLLBACK_SECRET_SIZE |
| /* |
| * If we are provided with some entropy, add it to secret. Otherwise, |
| * data.secret is left untouched and written back to the other region. |
| */ |
| if (entropy) { |
| if (!add_entropy(data->secret, data->secret, entropy, length)) |
| return EC_ERROR_UNCHANGED; |
| } |
| #endif |
| data->cookie = CROS_EC_ROLLBACK_COOKIE; |
| |
| /* Offset should never be part of active image. */ |
| if (system_unsafe_to_overwrite(offset, CONFIG_FLASH_ERASE_SIZE)) |
| return EC_ERROR_UNKNOWN; |
| |
| if (flash_erase(offset, CONFIG_FLASH_ERASE_SIZE)) |
| return EC_ERROR_UNKNOWN; |
| |
| if (flash_write(offset, sizeof(block), block)) |
| return EC_ERROR_UNKNOWN; |
| |
| return EC_SUCCESS; |
| } |
| |
| int rollback_update_version(int32_t next_min_version) |
| { |
| return rollback_update(next_min_version, NULL, 0); |
| } |
| |
| int rollback_add_entropy(uint8_t *data, unsigned int len) |
| { |
| return rollback_update(-1, data, len); |
| } |
| |
| static int command_rollback_update(int argc, char **argv) |
| { |
| int32_t min_version; |
| char *e; |
| |
| if (argc < 2) |
| return EC_ERROR_PARAM_COUNT; |
| |
| min_version = strtoi(argv[1], &e, 0); |
| |
| if (*e || min_version < 0) |
| return EC_ERROR_PARAM1; |
| |
| return rollback_update_version(min_version); |
| } |
| DECLARE_CONSOLE_COMMAND(rollbackupdate, command_rollback_update, |
| "min_version", |
| "Update rollback info"); |
| |
| #ifdef CONFIG_ROLLBACK_SECRET_SIZE |
| static int command_rollback_add_entropy(int argc, char **argv) |
| { |
| int len; |
| |
| if (argc < 2) |
| return EC_ERROR_PARAM_COUNT; |
| |
| len = strlen(argv[1]); |
| |
| return rollback_add_entropy(argv[1], len); |
| } |
| DECLARE_CONSOLE_COMMAND(rollbackaddent, command_rollback_add_entropy, |
| "data", |
| "Add entropy to rollback block"); |
| |
| #ifdef CONFIG_RNG |
| static int add_entropy_action; |
| static int add_entropy_rv = EC_RES_UNAVAILABLE; |
| |
| static void add_entropy_deferred(void) |
| { |
| uint8_t rand[CONFIG_ROLLBACK_SECRET_SIZE]; |
| int repeat = 1; |
| |
| /* |
| * If asked to reset the old secret, just add entropy multiple times, |
| * which will ping-pong between the blocks. |
| */ |
| if (add_entropy_action == ADD_ENTROPY_RESET_ASYNC) |
| repeat = ROLLBACK_REGIONS; |
| |
| init_trng(); |
| do { |
| rand_bytes(rand, sizeof(rand)); |
| if (rollback_add_entropy(rand, sizeof(rand)) != EC_SUCCESS) { |
| add_entropy_rv = EC_RES_ERROR; |
| goto out; |
| } |
| } while (--repeat); |
| |
| add_entropy_rv = EC_RES_SUCCESS; |
| out: |
| exit_trng(); |
| } |
| DECLARE_DEFERRED(add_entropy_deferred); |
| |
| static int hc_rollback_add_entropy(struct host_cmd_handler_args *args) |
| { |
| const struct ec_params_rollback_add_entropy *p = args->params; |
| |
| switch (p->action) { |
| case ADD_ENTROPY_ASYNC: |
| case ADD_ENTROPY_RESET_ASYNC: |
| if (add_entropy_rv == EC_RES_BUSY) |
| return EC_RES_BUSY; |
| |
| add_entropy_action = p->action; |
| add_entropy_rv = EC_RES_BUSY; |
| hook_call_deferred(&add_entropy_deferred_data, 0); |
| |
| return EC_RES_SUCCESS; |
| |
| case ADD_ENTROPY_GET_RESULT: |
| return add_entropy_rv; |
| } |
| |
| return EC_RES_INVALID_PARAM; |
| } |
| DECLARE_HOST_COMMAND(EC_CMD_ADD_ENTROPY, |
| hc_rollback_add_entropy, |
| EC_VER_MASK(0)); |
| #endif /* CONFIG_RNG */ |
| #endif /* CONFIG_ROLLBACK_SECRET_SIZE */ |
| #endif /* CONFIG_ROLLBACK_UPDATE */ |
| |
| static int command_rollback_info(int argc, char **argv) |
| { |
| int region, ret, min_region; |
| int32_t rw_rollback_version; |
| struct rollback_data data; |
| |
| min_region = get_latest_rollback(&data); |
| |
| if (min_region < 0) |
| return EC_ERROR_UNKNOWN; |
| |
| rw_rollback_version = system_get_rollback_version(SYSTEM_IMAGE_RW); |
| |
| ccprintf("rollback minimum version: %d\n", data.rollback_min_version); |
| ccprintf("RW rollback version: %d\n", rw_rollback_version); |
| |
| for (region = 0; region < ROLLBACK_REGIONS; region++) { |
| struct rollback_data data; |
| |
| ret = read_rollback(region, &data); |
| if (ret) |
| return ret; |
| |
| ccprintf("rollback %d: %08x %08x %08x", |
| region, data.id, data.rollback_min_version, |
| data.cookie); |
| #ifdef CONFIG_ROLLBACK_SECRET_SIZE |
| if (!system_is_locked()) { |
| /* If system is unlocked, show some of the secret. */ |
| ccprintf(" [%02x..%02x]", data.secret[0], |
| data.secret[CONFIG_ROLLBACK_SECRET_SIZE-1]); |
| } |
| #endif |
| if (min_region == region) |
| ccprintf(" *"); |
| ccprintf("\n"); |
| } |
| |
| return EC_SUCCESS; |
| } |
| DECLARE_SAFE_CONSOLE_COMMAND(rollbackinfo, command_rollback_info, |
| NULL, |
| "Print rollback info"); |