blob: 10367fb236a057a4d45bfa6fa3dafef31c933d96 [file] [log] [blame]
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
#include "sw/device/silicon_creator/lib/boot_data.h"
#include <stddef.h>
#include <stdint.h>
#include "sw/device/lib/base/hardened.h"
#include "sw/device/lib/base/memory.h"
#include "sw/device/silicon_creator/lib/drivers/flash_ctrl.h"
#include "sw/device/silicon_creator/lib/drivers/hmac.h"
#include "sw/device/silicon_creator/lib/error.h"
#include "flash_ctrl_regs.h" // Generated.
#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"
static_assert(kBootDataEntriesPerPage ==
FLASH_CTRL_PARAM_BYTES_PER_PAGE / sizeof(boot_data_t),
"Number of boot data entries per page is incorrect");
static_assert(sizeof(boot_data_t) % FLASH_CTRL_PARAM_BYTES_PER_WORD == 0,
"Size of `boot_data_t` must be a multiple of flash word size.");
static_assert(!(FLASH_CTRL_PARAM_BYTES_PER_PAGE &
(FLASH_CTRL_PARAM_BYTES_PER_PAGE - 1)),
"Size of a flash page must be a power of two.");
static_assert(!(sizeof(boot_data_t) & (sizeof(boot_data_t) - 1)),
"Size of `boot_data_t` must be a power of two.");
OT_ASSERT_MEMBER_SIZE(boot_data_t, is_valid, FLASH_CTRL_PARAM_BYTES_PER_WORD);
static_assert(offsetof(boot_data_t, is_valid) %
FLASH_CTRL_PARAM_BYTES_PER_WORD ==
0,
"`is_valid` must be flash word aligned.");
/**
* Boot data flash info pages.
*/
static const flash_ctrl_info_page_t kPages[2] = {
kFlashCtrlInfoPageBootData0,
kFlashCtrlInfoPageBootData1,
};
/**
* Computes the SHA-256 digest of a boot data entry.
*
* The region covered by this digest starts immediately after the `identifier`
* field and ends at the end of the entry.
*
* @param boot_data A boot data entry.
* @param[out] digest Digest of the boot data entry.
* @return The result of the operation.
*/
static void boot_data_digest_compute(const void *boot_data,
hmac_digest_t *digest) {
enum {
kDigestRegionOffset = sizeof((boot_data_t){0}.digest),
kDigestRegionSize = sizeof(boot_data_t) - kDigestRegionOffset,
};
static_assert(offsetof(boot_data_t, digest) == 0,
"`digest` must be the first field of `boot_data_t`.");
hmac_sha256_init();
hmac_sha256_update((const char *)boot_data + kDigestRegionOffset,
kDigestRegionSize);
hmac_sha256_final(digest);
}
/**
* Checks whether a boot data entry is empty.
*
* @param boot_data A buffer that holds a boot data entry. Must be word aligned.
* @return Whether the entry is empty.
*/
static hardened_bool_t boot_data_is_empty(const void *boot_data) {
for (size_t i = 0; i < kBootDataNumWords; ++i) {
if (read_32(boot_data) != kBootDataEmptyWordValue) {
return kHardenedBoolFalse;
}
boot_data = (char *)boot_data + sizeof(uint32_t);
}
return kHardenedBoolTrue;
}
/**
* Returns the `identifier` of the boot data entry at the given page and
* index after masking it with the words of its `is_valid` field.
*
* This function can be used to quickly determine if an entry can be empty or
* valid. Due to the values chosen for valid and invalid entries,
* `masked_identifier` will be `kBootDataEmptyWordValue` for entries that can be
* empty, `kBootDataIdentifier` for entries that are not invalidated, and `0`
* for invalidated entries.
*
* @param page A boot data page.
* @param index Index of the entry to read in the given page.
* @param[out] masked_identifier Identifier masked with the words of `is_valid`.
* @return The result of the operation.
*/
static rom_error_t boot_data_sniff(flash_ctrl_info_page_t page, size_t index,
uint32_t *masked_identifier) {
static_assert(kBootDataValidEntry == UINT64_MAX,
"is_valid must be UINT64_MAX for valid entries.");
static_assert(kBootDataInvalidEntry == 0,
"is_valid must be 0 for invalid entries.");
enum {
kIsValidOffset = offsetof(boot_data_t, is_valid),
kIdentifierOffset = offsetof(boot_data_t, identifier),
};
static_assert(
kIdentifierOffset - kIsValidOffset == sizeof((boot_data_t){0}.is_valid),
"is_valid and identifier must be consecutive.");
*masked_identifier = 0;
uint32_t buf[3];
const uint32_t offset = index * sizeof(boot_data_t) + kIsValidOffset;
RETURN_IF_ERROR(flash_ctrl_info_read(page, offset, 3, buf));
*masked_identifier = buf[0] & buf[1] & buf[2];
return kErrorOk;
}
/**
* Reads the boot data entry at the given page and index.
*
* @param page A boot data page.
* @param index Index of the entry to read in the given page.
* @param[out] boot_data A buffer that will hold the entry.
* @return The result of the operation.
*/
static rom_error_t boot_data_entry_read(flash_ctrl_info_page_t page,
size_t index, boot_data_t *boot_data) {
const uint32_t offset = index * sizeof(boot_data_t);
return flash_ctrl_info_read(page, offset, kBootDataNumWords, boot_data);
}
/**
* Populates the boot data entry at the given page and index.
*
* This function writes the new entry in two transactions skipping over the
* `is_valid` field so that the entry can be invalidated later. If `erase` is
* `kHardenedBoolTrue`, this function erases the given page before writing the
* new entry. This function also also verifies the newly written entry by
* reading it back. Reads, writes, and erases (if applicable) must be enabled
* for the given page before this function is called, see
* `boot_data_entry_write()`.
*
* @param page A boot data page.
* @param index Index of the entry to write in the given page.
* @param boot_data Entry to write.
* @param erase Whether to erase the page before writing the entry.
* @return The result of the operation.
*/
static rom_error_t boot_data_entry_write_impl(flash_ctrl_info_page_t page,
size_t index,
const boot_data_t *boot_data,
hardened_bool_t erase) {
// This function assumes the following layout for the first three fields.
OT_ASSERT_MEMBER_OFFSET(boot_data_t, digest, 0);
OT_ASSERT_MEMBER_OFFSET(boot_data_t, is_valid, 32);
OT_ASSERT_MEMBER_OFFSET(boot_data_t, identifier, 40);
if (erase == kHardenedBoolTrue) {
RETURN_IF_ERROR(flash_ctrl_info_erase(page, kFlashCtrlEraseTypePage));
}
// Write digest
const uint32_t offset = index * sizeof(boot_data_t);
RETURN_IF_ERROR(
flash_ctrl_info_write(page, offset, kHmacDigestNumWords, boot_data));
// Write the rest of the entry, skipping over `is_valid`.
enum {
kSecondWriteOffsetBytes = offsetof(boot_data_t, identifier),
kSecondWriteOffsetWords = kSecondWriteOffsetBytes / sizeof(uint32_t),
kSecondWriteNumWords = kBootDataNumWords - kSecondWriteOffsetWords,
};
RETURN_IF_ERROR(flash_ctrl_info_write(
page, offset + kSecondWriteOffsetBytes, kSecondWriteNumWords,
(const char *)boot_data + kSecondWriteOffsetBytes));
// Check.
boot_data_t written;
RETURN_IF_ERROR(
flash_ctrl_info_read(page, offset, kBootDataNumWords, &written));
if (memcmp(&written, boot_data, sizeof(boot_data_t)) != 0) {
return kErrorBootDataWriteCheck;
}
return kErrorOk;
}
/**
* Handles access permissions and populates the boot data entry at the given
* page and index.
*
* This function wraps the actual implementation to enable and disable reads,
* writes, and erases (if applicable) for the given page, see
* `boot_data_page_entry_write_impl()`.
*
* @param page A boot data page.
* @param index Index of the entry to write in the given page.
* @param boot_data Entry to write.
* @param erase Whether to erase the page before writing the entry.
* @return The result of the operation.
*/
static rom_error_t boot_data_entry_write(flash_ctrl_info_page_t page,
size_t index,
const boot_data_t *boot_data,
hardened_bool_t erase) {
flash_ctrl_info_perms_set(page, (flash_ctrl_perms_t){
.read = kHardenedBoolTrue,
.write = kHardenedBoolTrue,
.erase = erase,
});
rom_error_t error = boot_data_entry_write_impl(page, index, boot_data, erase);
flash_ctrl_info_perms_set(page, (flash_ctrl_perms_t){
.read = kHardenedBoolFalse,
.write = kHardenedBoolFalse,
.erase = kHardenedBoolFalse,
});
return error;
}
/**
* Invalidates the boot data entry at the given page and index.
*
* This function handles write permissions for the given page and sets the
* `is_valid` field of the given entry to `kBootDataInvalidEntry` which will
* cause the digest checks to fail in subsequent reads.
*
* This function must be called only after the new entry is successfully
* written since writes can potentially be interrupted.
*
* @param page A boot data page.
* @param index Index of the entry to invalidate in the given page.
* @return The result of the operation.
*/
static rom_error_t boot_data_entry_invalidate(flash_ctrl_info_page_t page,
size_t index) {
// Assertions for the assumptions below.
OT_ASSERT_MEMBER_SIZE(boot_data_t, is_valid, 8);
static_assert(kBootDataInvalidEntry == 0,
"Unexpected kBootDataInvalidEntry value.");
const uint32_t offset =
index * sizeof(boot_data_t) + offsetof(boot_data_t, is_valid);
const uint32_t val[2] = {0, 0};
flash_ctrl_info_perms_set(page, (flash_ctrl_perms_t){
.read = kHardenedBoolFalse,
.write = kHardenedBoolTrue,
.erase = kHardenedBoolFalse,
});
rom_error_t error = flash_ctrl_info_write(page, offset, 2, val);
flash_ctrl_info_perms_set(page, (flash_ctrl_perms_t){
.read = kHardenedBoolFalse,
.write = kHardenedBoolFalse,
.erase = kHardenedBoolFalse,
});
return error;
}
/**
* A struct that stores some information about the first empty and last valid
* entries in a flash info page.
*/
typedef struct boot_data_page_info {
/**
* Info page.
*/
flash_ctrl_info_page_t page;
/**
* Whether this page has an empty entry.
*/
hardened_bool_t has_empty_entry;
/**
* Index of the first empty entry.
*/
size_t first_empty_index;
/**
* Whether this page has a valid boot data entry.
*/
hardened_bool_t has_valid_entry;
/**
* Index of the last valid entry in the page.
*/
size_t last_valid_index;
/**
* Last valid entry in the page.
*/
boot_data_t last_valid_entry;
} boot_data_page_info_t;
/**
* Populates a page info struct for the given page.
*
* This function performs a forward search to find the first empty boot data
* entry followed by a backward search to find the last valid boot data entry.
* Reads must be enabled for the given page before this function is called, see
* `boot_data_page_info_get()`.
*
* @param page A boot data page.
* @param[out] page_info Page info struct for the given page.
* @return The result of the operation.
*/
static rom_error_t boot_data_page_info_get_impl(
flash_ctrl_info_page_t page, boot_data_page_info_t *page_info) {
uint32_t sniff_results[kBootDataEntriesPerPage];
page_info->page = page;
page_info->has_empty_entry = kHardenedBoolFalse;
page_info->has_valid_entry = kHardenedBoolFalse;
// Perform a forward search to find the first empty entry.
for (size_t i = 0; i < kBootDataEntriesPerPage; ++i) {
// Read and cache the identifier to quickly determine if an entry can be
// empty or valid.
RETURN_IF_ERROR(boot_data_sniff(page, i, &sniff_results[i]));
// Check all words of this entry only if it can be empty.
if (sniff_results[i] == kBootDataEmptyWordValue) {
RETURN_IF_ERROR(
boot_data_entry_read(page, i, &page_info->last_valid_entry));
if (boot_data_is_empty(&page_info->last_valid_entry) ==
kHardenedBoolTrue) {
page_info->first_empty_index = i;
page_info->has_empty_entry = kHardenedBoolTrue;
break;
}
}
}
// Perform a backward search to find the last valid entry.
size_t start_index = (page_info->has_empty_entry == kHardenedBoolTrue)
? page_info->first_empty_index - 1
: kBootDataEntriesPerPage - 1;
for (size_t i = start_index; i < kBootDataEntriesPerPage; --i) {
// Check the digest only if this entry can be valid.
if (sniff_results[i] == kBootDataIdentifier) {
RETURN_IF_ERROR(
boot_data_entry_read(page, i, &page_info->last_valid_entry));
if (boot_data_check(&page_info->last_valid_entry) == kErrorOk) {
page_info->last_valid_index = i;
page_info->has_valid_entry = kHardenedBoolTrue;
break;
}
}
}
return kErrorOk;
}
/**
* Handles read permissions and populates a page info struct for the given page.
*
* This function wraps the actual implementation to enable and disable reads for
* the given page, see `boot_data_page_info_get_impl()`.
*
* @param page A boot data page.
* @param[out] page_info Page info struct for the given page.
* @return The result of the operation.
*/
static rom_error_t boot_data_page_info_get(flash_ctrl_info_page_t page,
boot_data_page_info_t *page_info) {
flash_ctrl_info_perms_set(page, (flash_ctrl_perms_t){
.read = kHardenedBoolTrue,
.write = kHardenedBoolFalse,
.erase = kHardenedBoolFalse,
});
rom_error_t error = boot_data_page_info_get_impl(page, page_info);
flash_ctrl_info_perms_set(page, (flash_ctrl_perms_t){
.read = kHardenedBoolFalse,
.write = kHardenedBoolFalse,
.erase = kHardenedBoolFalse,
});
return error;
}
/**
* Finds the active info page and returns its page info struct.
*
* The active info page is the one that has the newest valid boot data entry,
* i.e. the entry with the greatest counter value.
*
* @param[out] page_info Page info struct of the active info page.
* @return The result of the operation.
*/
static rom_error_t boot_data_active_page_find(
boot_data_page_info_t *page_info) {
boot_data_page_info_t page_infos[2];
for (size_t i = 0; i < ARRAYSIZE(kPages); ++i) {
RETURN_IF_ERROR(boot_data_page_info_get(kPages[i], &page_infos[i]));
}
if (page_infos[0].has_valid_entry == kHardenedBoolTrue &&
page_infos[1].has_valid_entry == kHardenedBoolTrue) {
if (page_infos[0].last_valid_entry.counter >
page_infos[1].last_valid_entry.counter) {
*page_info = page_infos[0];
return kErrorOk;
} else if (page_infos[1].last_valid_entry.counter >
page_infos[0].last_valid_entry.counter) {
*page_info = page_infos[1];
return kErrorOk;
} else {
return kErrorBootDataNotFound;
}
} else if (page_infos[0].has_valid_entry == kHardenedBoolTrue) {
*page_info = page_infos[0];
return kErrorOk;
} else if (page_infos[1].has_valid_entry == kHardenedBoolTrue) {
*page_info = page_infos[1];
return kErrorOk;
}
return kErrorBootDataNotFound;
}
/**
* Default boot data to use if the device is in a non-prod state and there is
* no valid boot data entry in the flash info pages.
*/
boot_data_t kBootDataDefault = (boot_data_t){
.digest = {{0x0d044e5c, 0x33ceed53, 0x05aa74a4, 0x57b7017f, 0x574a685d,
0x6ec8f5f7, 0x594b0141, 0x656bae85}},
.is_valid = kBootDataValidEntry,
.identifier = kBootDataIdentifier,
// Note: This starts from 5 to have a slightly less trivial value in case we
// need to distinguish the default entry.
.counter = 5,
.min_security_version_rom_ext = 0,
};
/**
* Returns the default boot data.
*
* Default boot data can be used only in TEST_UNLOCKED, DEV, and RMA life cycle
* states.
*
* @param lc_state Life cycle state of the device.
* @param[out] boot_data Default boot data.
* @return The result of the operation.
*/
static rom_error_t boot_data_default_get(lifecycle_state_t lc_state,
boot_data_t *boot_data) {
// TODO(#8778): Default boot data.
switch (launder32(lc_state)) {
case kLcStateTest:
HARDENED_CHECK_EQ(lc_state, kLcStateTest);
*boot_data = kBootDataDefault;
return kErrorOk;
case kLcStateDev:
HARDENED_CHECK_EQ(lc_state, kLcStateDev);
*boot_data = kBootDataDefault;
return kErrorOk;
case kLcStateProd:
HARDENED_CHECK_EQ(lc_state, kLcStateProd);
return kErrorBootDataNotFound;
case kLcStateProdEnd:
HARDENED_CHECK_EQ(lc_state, kLcStateProdEnd);
return kErrorBootDataNotFound;
case kLcStateRma:
HARDENED_CHECK_EQ(lc_state, kLcStateRma);
*boot_data = kBootDataDefault;
return kErrorOk;
default:
HARDENED_UNREACHABLE();
}
}
rom_error_t boot_data_read(lifecycle_state_t lc_state, boot_data_t *boot_data) {
boot_data_page_info_t active_page;
rom_error_t error = boot_data_active_page_find(&active_page);
switch (error) {
case kErrorOk:
*boot_data = active_page.last_valid_entry;
return kErrorOk;
case kErrorBootDataNotFound:
// TODO(#8779): Recovery paths for failures in prod life cycle states?
RETURN_IF_ERROR(boot_data_default_get(lc_state, boot_data));
return kErrorOk;
default:
return error;
}
}
rom_error_t boot_data_write(const boot_data_t *boot_data) {
boot_data_t new_entry = *boot_data;
new_entry.is_valid = kBootDataValidEntry;
new_entry.identifier = kBootDataIdentifier;
boot_data_page_info_t active_page;
rom_error_t error = boot_data_active_page_find(&active_page);
if (error == kErrorOk) {
// Note: Not checking for wraparound since a successful write will
// invalidate the old entry.
new_entry.counter = active_page.last_valid_entry.counter + 1;
boot_data_digest_compute(&new_entry, &new_entry.digest);
if (active_page.has_empty_entry == kHardenedBoolTrue) {
RETURN_IF_ERROR(boot_data_entry_write(active_page.page,
active_page.first_empty_index,
&new_entry, kHardenedBoolFalse));
} else {
// Erase the other page and write the new entry there if the active page
// is full.
flash_ctrl_info_page_t new_page =
active_page.page == kPages[0] ? kPages[1] : kPages[0];
RETURN_IF_ERROR(
boot_data_entry_write(new_page, 0, &new_entry, kHardenedBoolTrue));
}
// Invalidate the previous entry so that there is only one valid entry
// across both pages.
RETURN_IF_ERROR(boot_data_entry_invalidate(active_page.page,
active_page.last_valid_index));
} else if (error == kErrorBootDataNotFound) {
// Erase the first page and write the entry there if the active page cannot
// be found, i.e. the storage is not initialized yet.
new_entry.counter = kBootDataDefault.counter + 1;
boot_data_digest_compute(&new_entry, &new_entry.digest);
RETURN_IF_ERROR(
boot_data_entry_write(kPages[0], 0, &new_entry, kHardenedBoolTrue));
} else {
return error;
}
return kErrorOk;
}
/**
* Shares for producing the `error` value in `boot_data_check()`. First 8
* shares are generated using the `sparse-fsm-encode` script while the last
* share is `kErrorOk ^ kBootDataInvalid ^ kBootDataIdentifier ^
* kCheckShares[0] ^ ... ^ kCheckShares[7]` so that xor'ing all shares with
* the initial value of `error`, i.e. `kErrorBootDataInvalid`, and
* `kBootDataIdentifier` produces `kErrorOk`.
*
* Encoding generated with
* $ ./util/design/sparse-fsm-encode.py -d 6 -m 8 -n 32 \
* -s 49105412 --language=c
*
* Minimum Hamming distance: 12
* Maximum Hamming distance: 23
* Minimum Hamming weight: 13
* Maximum Hamming weight: 20
*/
static const uint32_t kCheckShares[kHmacDigestNumWords + 1] = {
0xe021e1a9, 0xf81e8365, 0xbf8322db, 0xc7a37080, 0x271a933f,
0xdd8ce33f, 0x7585d574, 0x951777af, 0x381dee3a,
};
/**
* Checks whether the digest of a boot data entry is valid.
*
* @param boot_data A buffer that holds a boot data entry.
* @return Whether the digest of the entry is valid.
*/
rom_error_t boot_data_check(const boot_data_t *boot_data) {
static_assert(offsetof(boot_data_t, digest) == 0,
"`digest` must be the first field of `boot_data_t`.");
rom_error_t error = kErrorBootDataInvalid;
hmac_digest_t act_digest;
boot_data_digest_compute(boot_data, &act_digest);
size_t i = 0;
for (; launder32(i) < kHmacDigestNumWords; ++i) {
error ^=
boot_data->digest.digest[i] ^ act_digest.digest[i] ^ kCheckShares[i];
}
HARDENED_CHECK_EQ(i, kHmacDigestNumWords);
error ^= boot_data->identifier ^ kCheckShares[kHmacDigestNumWords];
if (launder32(error) == kErrorOk) {
HARDENED_CHECK_EQ(error, kErrorOk);
return error;
}
return kErrorBootDataInvalid;
}