diff --git a/examples/htool.c b/examples/htool.c index 3e84dee..da861a0 100644 --- a/examples/htool.c +++ b/examples/htool.c @@ -1319,6 +1319,15 @@ static const struct htool_cmd CMDS[] = { {HTOOL_POSITIONAL, .name = "source-file"}, {}}, .func = htool_payload_info_nonstatic, }, + { + .verbs = (const char*[]){"payload", "info", "target_watchdog", NULL}, + .desc = "Print the target watchdog config of a Titan image or image " + "descriptor as key=value lines.", + .params = + (const struct htool_param[]){ + {HTOOL_POSITIONAL, .name = "source-file"}, {}}, + .func = htool_payload_info_target_watchdog, + }, { .verbs = (const char*[]){"payload", "info", NULL}, .desc = "Display payload info for a Titan image.", diff --git a/examples/htool_payload.c b/examples/htool_payload.c index d502dde..9a1d9af 100644 --- a/examples/htool_payload.c +++ b/examples/htool_payload.c @@ -272,6 +272,34 @@ int htool_payload_info_all(const struct htool_invocation* inv) { return htool_payload_image_close(&img); } +int htool_payload_info_target_watchdog(const struct htool_invocation* inv) { + struct htool_payload_image img; + if (htool_payload_image_open(inv, &img) != 0) { + return -1; + } + + struct target_watchdog_config config; + enum image_blob_status status = + libhoth_payload_target_watchdog_config(img.image, img.size, &config); + int rv = htool_payload_image_close(&img); + if (status == IMAGE_BLOB_NOT_FOUND) { + fprintf(stderr, "Image has no target watchdog (TWCH) blob\n"); + return -1; + } + if (status != IMAGE_BLOB_OK) { + fprintf(stderr, "Invalid target watchdog config in image: %s\n", + libhoth_image_blob_status_string(status)); + return -1; + } + + printf("initial_delay_seconds=%" PRIu32 "\n", config.initial_delay_seconds); + printf("watchdog_timeout_seconds=%" PRIu32 "\n", + config.watchdog_timeout_seconds); + printf("hold_in_reset_microseconds=%" PRIu32 "\n", + config.hold_in_reset_microseconds); + return rv; +} + int htool_payload_info_nonstatic(const struct htool_invocation* inv) { struct htool_payload_image img; if (htool_payload_image_open(inv, &img) != 0) { diff --git a/examples/htool_payload.h b/examples/htool_payload.h index a4cf32a..d720cbf 100644 --- a/examples/htool_payload.h +++ b/examples/htool_payload.h @@ -28,6 +28,7 @@ int htool_payload_status(const struct htool_invocation* inv); int htool_payload_info(const struct htool_invocation* inv); int htool_payload_info_all(const struct htool_invocation* inv); int htool_payload_info_nonstatic(const struct htool_invocation* inv); +int htool_payload_info_target_watchdog(const struct htool_invocation* inv); int htool_get_compiled_payload_mauv(const struct htool_invocation* inv); int htool_get_effective_payload_mauv(const struct htool_invocation* inv); diff --git a/protocol/payload_info.c b/protocol/payload_info.c index 905b403..a6dad53 100644 --- a/protocol/payload_info.c +++ b/protocol/payload_info.c @@ -26,7 +26,7 @@ const struct image_descriptor* libhoth_find_image_descriptor( struct image_descriptor* img_dsc = (struct image_descriptor*)(image + off); - if (img_dsc->descriptor_area_size + off > len) { + if (img_dsc->descriptor_area_size > len - off) { // Image descriptor is clipped return NULL; } @@ -130,3 +130,108 @@ bool libhoth_payload_info_all(const uint8_t* image, size_t len, return true; } + +#define TITAN_IMAGE_DESCRIPTOR_MAX_MAJOR_VERSION 1 +#define TITAN_IMAGE_DESCRIPTOR_BLOB_MAGIC 0x424f4c42 // "BLOB" +#define IMAGE_BLOB_ALIGNMENT 4 +#define IMAGE_BLOB_TYPE_TARGET_WATCHDOG 0x48435754 // "TWCH" + +struct image_blob_header { + uint32_t blob_type; + // Size of the blob in bytes. Does NOT include `sizeof(image_blob_header)`. + uint32_t payload_size; +} __attribute__((__packed__)); + +static uint32_t blob_list_magic_offset(const struct image_descriptor* descr) { + uint32_t offset = sizeof(struct image_descriptor) + + descr->region_count * sizeof(struct image_region) + + sizeof(struct hash_sha256); + if (descr->denylist_size != 0) { + offset += sizeof(uint32_t) + + descr->denylist_size * sizeof(struct payload_version); + } + return offset; +} + +const char* libhoth_image_blob_status_string(enum image_blob_status status) { + switch (status) { + case IMAGE_BLOB_OK: + return "ok"; + case IMAGE_BLOB_NOT_FOUND: + return "blob not found"; + case IMAGE_BLOB_NO_DESCRIPTOR: + return "no image descriptor found"; + case IMAGE_BLOB_UNSUPPORTED_DESCRIPTOR: + return "unsupported image descriptor version or hash type"; + case IMAGE_BLOB_LIST_INVALID_MAGIC: + return "invalid blob list magic"; + case IMAGE_BLOB_LIST_MALFORMED: + return "malformed blob list"; + case IMAGE_BLOB_DUPLICATE: + return "duplicate blob"; + case IMAGE_BLOB_INVALID_SIZE: + return "invalid blob payload size"; + } + return "unknown error"; +} + +enum image_blob_status libhoth_payload_target_watchdog_config( + const uint8_t* image, size_t len, struct target_watchdog_config* config) { + const struct image_descriptor* descr = + libhoth_find_image_descriptor(image, len); + if (descr == NULL) { + return IMAGE_BLOB_NO_DESCRIPTOR; + } + // The blob list's location depends on the descriptor's layout. + if (descr->descriptor_major > TITAN_IMAGE_DESCRIPTOR_MAX_MAJOR_VERSION || + descr->hash_type != HASH_SHA2_256) { + return IMAGE_BLOB_UNSUPPORTED_DESCRIPTOR; + } + if (descr->blob_size == 0) { + return IMAGE_BLOB_NOT_FOUND; + } + + // libhoth_find_image_descriptor() guarantees that the whole descriptor area + // is inside `image`, so bounding the list by it keeps every read in bounds. + const uint8_t* descr_bytes = (const uint8_t*)descr; + uint64_t magic_offset = blob_list_magic_offset(descr); + uint64_t list_end = magic_offset + sizeof(uint32_t) + descr->blob_size; + if (list_end > descr->descriptor_area_size) { + return IMAGE_BLOB_LIST_MALFORMED; + } + uint32_t magic; + memcpy(&magic, descr_bytes + magic_offset, sizeof(magic)); + if (magic != TITAN_IMAGE_DESCRIPTOR_BLOB_MAGIC) { + return IMAGE_BLOB_LIST_INVALID_MAGIC; + } + + bool found = false; + uint64_t offset = magic_offset + sizeof(magic); + while (offset < list_end) { + struct image_blob_header header; + if (list_end - offset < sizeof(header)) { + return IMAGE_BLOB_LIST_MALFORMED; + } + memcpy(&header, descr_bytes + offset, sizeof(header)); + offset += sizeof(header); + if (header.payload_size > list_end - offset) { + return IMAGE_BLOB_LIST_MALFORMED; + } + + if (header.blob_type == IMAGE_BLOB_TYPE_TARGET_WATCHDOG) { + if (found) { + return IMAGE_BLOB_DUPLICATE; + } + if (header.payload_size != sizeof(*config)) { + return IMAGE_BLOB_INVALID_SIZE; + } + memcpy(config, descr_bytes + offset, sizeof(*config)); + found = true; + } + + offset += header.payload_size; + offset = (offset + IMAGE_BLOB_ALIGNMENT - 1) / IMAGE_BLOB_ALIGNMENT * + IMAGE_BLOB_ALIGNMENT; + } + return found ? IMAGE_BLOB_OK : IMAGE_BLOB_NOT_FOUND; +} diff --git a/protocol/payload_info.h b/protocol/payload_info.h index a5774fb..7816c3b 100644 --- a/protocol/payload_info.h +++ b/protocol/payload_info.h @@ -116,7 +116,7 @@ struct image_descriptor { /* image_type enum { DEV, PROD, BREAKOUT, UNSIGNED_INTEGRITY} */ uint8_t image_type; - uint8_t reserved_3; + uint8_t denylist_size; /* hash_type enum { NONE, SHA2_224, SHA2_256, ...} */ uint8_t hash_type; @@ -186,6 +186,29 @@ bool libhoth_payload_info(const uint8_t* image, size_t len, bool libhoth_payload_info_all(const uint8_t* image, size_t len, struct payload_info_all* info_all); +enum image_blob_status { + IMAGE_BLOB_OK = 0, + IMAGE_BLOB_NOT_FOUND, + IMAGE_BLOB_NO_DESCRIPTOR, + IMAGE_BLOB_UNSUPPORTED_DESCRIPTOR, + IMAGE_BLOB_LIST_INVALID_MAGIC, + // The blob list overruns the descriptor area, or an entry overruns the list. + IMAGE_BLOB_LIST_MALFORMED, + IMAGE_BLOB_DUPLICATE, + IMAGE_BLOB_INVALID_SIZE, +}; + +const char* libhoth_image_blob_status_string(enum image_blob_status status); + +struct target_watchdog_config { + uint32_t initial_delay_seconds; + uint32_t watchdog_timeout_seconds; + uint32_t hold_in_reset_microseconds; +} __attribute__((__packed__)); + +enum image_blob_status libhoth_payload_target_watchdog_config( + const uint8_t* image, size_t len, struct target_watchdog_config* config); + #ifdef __cplusplus } #endif diff --git a/protocol/payload_info_test.cc b/protocol/payload_info_test.cc index 07cdcbb..db380cf 100644 --- a/protocol/payload_info_test.cc +++ b/protocol/payload_info_test.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include "payload_info.h" @@ -231,3 +232,320 @@ TEST(PayloadInfotest, payload_info_all) { (void)munmap(image, statbuf.st_size); close(fd); } + +namespace { + +constexpr uint32_t kBlobListMagic = 0x424f4c42; // "BLOB" +constexpr uint32_t kTargetWatchdogBlobType = 0x48435754; // "TWCH" +constexpr uint32_t kOtherBlobType = 0x5248544f; // "OTHR" +constexpr size_t kBlobHeaderSize = 8; +constexpr uint32_t kDescriptorOffset = TITAN_IMAGE_DESCRIPTOR_ALIGNMENT; +constexpr uint32_t kDescriptorAreaSize = 4096; +constexpr uint8_t kRegionCount = 1; + +using Bytes = std::vector; + +void AppendLe32(Bytes& bytes, uint32_t value) { + for (int shift = 0; shift < 32; shift += 8) { + bytes.push_back(static_cast(value >> shift)); + } +} + +Bytes BlobEntry(uint32_t blob_type, const Bytes& payload) { + Bytes entry; + AppendLe32(entry, blob_type); + AppendLe32(entry, payload.size()); + entry.insert(entry.end(), payload.begin(), payload.end()); + while (entry.size() % 4 != 0) { + entry.push_back(0xff); + } + return entry; +} + +Bytes TargetWatchdogEntry(const struct target_watchdog_config& config) { + Bytes payload; + AppendLe32(payload, config.initial_delay_seconds); + AppendLe32(payload, config.watchdog_timeout_seconds); + AppendLe32(payload, config.hold_in_reset_microseconds); + return BlobEntry(kTargetWatchdogBlobType, payload); +} + +Bytes Concat(std::initializer_list entries) { + Bytes list; + for (const auto& entry : entries) { + list.insert(list.end(), entry.begin(), entry.end()); + } + return list; +} + +// Lays out a payload image as: +// the descriptor, its regions, the hash, the optional denylist, then the blob +// list. +struct TestImage { + Bytes blob_list; + uint8_t denylist_size = 0; + uint32_t blob_list_magic = kBlobListMagic; + uint8_t descriptor_major = 1; + uint8_t hash_type = HASH_SHA2_256; + + size_t BlobListMagicOffset() const { + size_t offset = sizeof(struct image_descriptor) + + kRegionCount * sizeof(struct image_region) + + sizeof(struct hash_sha256); + if (denylist_size != 0) { + offset += sizeof(uint32_t) + denylist_size * sizeof(payload_version); + } + return offset; + } + + Bytes DescriptorArea() const { + Bytes area(kDescriptorAreaSize, 0xff); + + struct image_descriptor descr = {}; + descr.descriptor_magic = TITAN_IMAGE_DESCRIPTOR_MAGIC; + descr.descriptor_major = descriptor_major; + descr.descriptor_area_size = kDescriptorAreaSize; + descr.hash_type = hash_type; + descr.denylist_size = denylist_size; + descr.region_count = kRegionCount; + descr.image_size = kDescriptorOffset + kDescriptorAreaSize; + descr.blob_size = blob_list.size(); + std::memcpy(area.data(), &descr, sizeof(descr)); + + struct image_region region = {}; + region.region_offset = kDescriptorOffset; + region.region_size = kDescriptorAreaSize; + region.region_attributes = IMAGE_REGION_STATIC; + std::memcpy(area.data() + sizeof(descr), ®ion, sizeof(region)); + + struct hash_sha256 hash = {}; + hash.hash_magic = TITAN_IMAGE_DESCRIPTOR_HASH_MAGIC; + std::memcpy(area.data() + sizeof(descr) + sizeof(region), &hash, + sizeof(hash)); + + if (!blob_list.empty()) { + Bytes blob_section; + AppendLe32(blob_section, blob_list_magic); + blob_section.insert(blob_section.end(), blob_list.begin(), + blob_list.end()); + std::copy(blob_section.begin(), blob_section.end(), + area.begin() + BlobListMagicOffset()); + } + return area; + } + + Bytes Image() const { + Bytes image(kDescriptorOffset, 0xff); + Bytes area = DescriptorArea(); + image.insert(image.end(), area.begin(), area.end()); + return image; + } +}; + +enum image_blob_status ReadTargetWatchdog( + const Bytes& image, struct target_watchdog_config* config) { + return libhoth_payload_target_watchdog_config(image.data(), image.size(), + config); +} + +constexpr struct target_watchdog_config kConfig = {600, 60, 100000}; + +} // namespace + +bool operator==(const struct target_watchdog_config& a, + const struct target_watchdog_config& b) { + return a.initial_delay_seconds == b.initial_delay_seconds && + a.watchdog_timeout_seconds == b.watchdog_timeout_seconds && + a.hold_in_reset_microseconds == b.hold_in_reset_microseconds; +} + +TEST(PayloadTargetWatchdogTest, FullImageWithTargetWatchdog) { + TestImage image{.blob_list = TargetWatchdogEntry(kConfig)}; + + struct target_watchdog_config config; + ASSERT_EQ(ReadTargetWatchdog(image.Image(), &config), IMAGE_BLOB_OK); + EXPECT_EQ(config, kConfig); +} + +TEST(PayloadTargetWatchdogTest, RawDescriptorWithTargetWatchdog) { + TestImage image{.blob_list = TargetWatchdogEntry(kConfig)}; + + struct target_watchdog_config config; + ASSERT_EQ(ReadTargetWatchdog(image.DescriptorArea(), &config), IMAGE_BLOB_OK); + EXPECT_EQ(config, kConfig); +} + +TEST(PayloadTargetWatchdogTest, TruncatedRawDescriptorIsRejected) { + TestImage image{.blob_list = TargetWatchdogEntry(kConfig)}; + Bytes raw_descriptor = image.DescriptorArea(); + raw_descriptor.pop_back(); + + struct target_watchdog_config config; + EXPECT_EQ(ReadTargetWatchdog(raw_descriptor, &config), + IMAGE_BLOB_NO_DESCRIPTOR); +} + +TEST(PayloadTargetWatchdogTest, NoBlobs) { + TestImage image; + + struct target_watchdog_config config; + EXPECT_EQ(ReadTargetWatchdog(image.Image(), &config), IMAGE_BLOB_NOT_FOUND); +} + +TEST(PayloadTargetWatchdogTest, TestPayloadHasNoTargetWatchdog) { + int fd = open(kTestData, O_RDONLY, 0); + ASSERT_NE(fd, -1); + struct stat statbuf; + ASSERT_EQ(fstat(fd, &statbuf), 0); + uint8_t* image = reinterpret_cast( + mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0)); + ASSERT_NE(image, MAP_FAILED); + + struct target_watchdog_config config; + EXPECT_EQ( + libhoth_payload_target_watchdog_config(image, statbuf.st_size, &config), + IMAGE_BLOB_NOT_FOUND); + + (void)munmap(image, statbuf.st_size); + close(fd); +} + +TEST(PayloadTargetWatchdogTest, UnknownBlobsAndTargetWatchdog) { + TestImage image{.blob_list = Concat({ + BlobEntry(kOtherBlobType, {0xaa, 0xaa, 0xaa}), + TargetWatchdogEntry(kConfig), + BlobEntry(kOtherBlobType, {}), + })}; + + struct target_watchdog_config config; + ASSERT_EQ(ReadTargetWatchdog(image.Image(), &config), IMAGE_BLOB_OK); + EXPECT_EQ(config, kConfig); +} + +TEST(PayloadTargetWatchdogTest, OnlyUnknownBlobs) { + TestImage image{.blob_list = BlobEntry(kOtherBlobType, {1, 2, 3, 4})}; + + struct target_watchdog_config config; + EXPECT_EQ(ReadTargetWatchdog(image.Image(), &config), IMAGE_BLOB_NOT_FOUND); +} + +TEST(PayloadTargetWatchdogTest, DenylistPrecedesBlobList) { + TestImage image{.blob_list = TargetWatchdogEntry(kConfig), + .denylist_size = 2}; + + struct target_watchdog_config config; + ASSERT_EQ(ReadTargetWatchdog(image.Image(), &config), IMAGE_BLOB_OK); + EXPECT_EQ(config, kConfig); +} + +TEST(PayloadTargetWatchdogTest, ListMayEndInPadding) { + Bytes unpadded_entry = BlobEntry(kOtherBlobType, {0xaa}); + unpadded_entry.resize(kBlobHeaderSize + 1); + TestImage image{.blob_list = + Concat({TargetWatchdogEntry(kConfig), unpadded_entry})}; + + struct target_watchdog_config config; + ASSERT_EQ(ReadTargetWatchdog(image.Image(), &config), IMAGE_BLOB_OK); + EXPECT_EQ(config, kConfig); +} + +TEST(PayloadTargetWatchdogTest, InvalidBlobListMagic) { + TestImage image{.blob_list = TargetWatchdogEntry(kConfig), + .blob_list_magic = 0x12345678}; + + struct target_watchdog_config config; + EXPECT_EQ(ReadTargetWatchdog(image.Image(), &config), + IMAGE_BLOB_LIST_INVALID_MAGIC); +} + +TEST(PayloadTargetWatchdogTest, MalformedEntries) { + Bytes entry = TargetWatchdogEntry(kConfig); + Bytes truncated_header(kBlobHeaderSize - 1, 0); + Bytes truncated_payload(entry.begin(), entry.end() - 1); + Bytes oversized_payload = BlobEntry(kOtherBlobType, Bytes(4)); + std::fill(oversized_payload.begin() + 4, oversized_payload.begin() + 8, 0xff); + + for (const Bytes& list : { + truncated_header, + truncated_payload, + oversized_payload, + // The firmware validates the whole list, not just up to the + // target watchdog blob. + Concat({entry, truncated_header}), + }) { + TestImage image{.blob_list = list}; + + struct target_watchdog_config config; + EXPECT_EQ(ReadTargetWatchdog(image.Image(), &config), + IMAGE_BLOB_LIST_MALFORMED); + } +} + +TEST(PayloadTargetWatchdogTest, DuplicateTargetWatchdog) { + TestImage image{.blob_list = Concat({TargetWatchdogEntry(kConfig), + TargetWatchdogEntry(kConfig)})}; + + struct target_watchdog_config config; + EXPECT_EQ(ReadTargetWatchdog(image.Image(), &config), IMAGE_BLOB_DUPLICATE); +} + +TEST(PayloadTargetWatchdogTest, WrongTargetWatchdogSize) { + TestImage image{.blob_list = BlobEntry(kTargetWatchdogBlobType, Bytes(8))}; + + struct target_watchdog_config config; + EXPECT_EQ(ReadTargetWatchdog(image.Image(), &config), + IMAGE_BLOB_INVALID_SIZE); +} + +TEST(PayloadTargetWatchdogTest, ValuesTheRotWouldRejectAreReturnedAsStored) { + constexpr struct target_watchdog_config kRejected = {UINT32_MAX, 0, + UINT32_MAX}; + TestImage image{.blob_list = TargetWatchdogEntry(kRejected)}; + + struct target_watchdog_config config; + ASSERT_EQ(ReadTargetWatchdog(image.Image(), &config), IMAGE_BLOB_OK); + EXPECT_EQ(config, kRejected); +} + +TEST(PayloadTargetWatchdogTest, BlobListFillingDescriptorArea) { + TestImage image; + size_t list_size = kDescriptorAreaSize - image.BlobListMagicOffset() - 4; + Bytes entry = TargetWatchdogEntry(kConfig); + image.blob_list = Concat( + {entry, + BlobEntry(kOtherBlobType, + Bytes(list_size - entry.size() - kBlobHeaderSize, 0xaa))}); + ASSERT_EQ(image.blob_list.size(), list_size); + Bytes bytes = image.DescriptorArea(); + auto* descr = reinterpret_cast(bytes.data()); + + struct target_watchdog_config config; + ASSERT_EQ(ReadTargetWatchdog(bytes, &config), IMAGE_BLOB_OK); + EXPECT_EQ(config, kConfig); + + for (uint32_t blob_size : + {static_cast(list_size + 1), UINT32_MAX}) { + descr->blob_size = blob_size; + EXPECT_EQ(ReadTargetWatchdog(bytes, &config), IMAGE_BLOB_LIST_MALFORMED); + } +} + +TEST(PayloadTargetWatchdogTest, UnsupportedDescriptor) { + TestImage newer_major{.blob_list = TargetWatchdogEntry(kConfig), + .descriptor_major = 2}; + TestImage sha512{.blob_list = TargetWatchdogEntry(kConfig), + .hash_type = HASH_SHA2_512}; + + struct target_watchdog_config config; + EXPECT_EQ(ReadTargetWatchdog(newer_major.Image(), &config), + IMAGE_BLOB_UNSUPPORTED_DESCRIPTOR); + EXPECT_EQ(ReadTargetWatchdog(sha512.Image(), &config), + IMAGE_BLOB_UNSUPPORTED_DESCRIPTOR); +} + +TEST(PayloadTargetWatchdogTest, NoDescriptor) { + Bytes bytes(kDescriptorAreaSize, 0xff); + + struct target_watchdog_config config; + EXPECT_EQ(ReadTargetWatchdog(bytes, &config), IMAGE_BLOB_NO_DESCRIPTOR); +}