From 7862194df7cced1752923ab98a56614bf4a9db4e Mon Sep 17 00:00:00 2001 From: Jonathon Hall Date: Sat, 28 Oct 2023 11:38:25 -0400 Subject: [PATCH] POC: Add tooling for built-in ROM digest verification Add --replace command to cbfs in flashtools, ensures data is replaced in-place without altering any other part of ROM. Add calc_digest.sh to calculate or zero digest in ROM file. Add check_digest.sh to verify integrity of ROM using built-in digest. Signed-off-by: Jonathon Hall --- initrd/bin/calc_digest.sh | 46 ++++++ initrd/bin/check_digest.sh | 33 +++++ ...f12568cb23387144a4b7a6535fe1bc1e79b1.patch | 139 ++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100755 initrd/bin/calc_digest.sh create mode 100755 initrd/bin/check_digest.sh create mode 100644 patches/flashtools-d1e6f12568cb23387144a4b7a6535fe1bc1e79b1.patch diff --git a/initrd/bin/calc_digest.sh b/initrd/bin/calc_digest.sh new file mode 100755 index 000000000..9aef9d726 --- /dev/null +++ b/initrd/bin/calc_digest.sh @@ -0,0 +1,46 @@ +#! /bin/bash + +set -eo pipefail + +ARG_ZERO= + +if [ "$1" = "-z" ]; then + ARG_ZERO=y + shift +fi + +if [ "$#" -lt 2 ]; then + echo "usage: $0 [-z] " >&2 + echo + echo "Updates (or adds) rom_digest in the ROM." + echo + echo "By default, rom_digest is set to the ROM's SHA-256 digest (with" + echo "rom_digest set to all-0)." + echo + echo "With -z, just zero rom_digest, so integrity can be checked." + exit 1 +fi + +ORIGINAL_ROM="$1" +UPDATED_ROM="$2" + +cp "$ORIGINAL_ROM" "$UPDATED_ROM" +dd if=/dev/zero bs=32 count=1 of=/tmp/digest.bin status=none + +# Ensure there is a zeroed rom_digest file, but don't delete any existing file +if ! cbfs -l -o "$UPDATED_ROM" | grep -q '^rom_digest$'; then + # Add the file + cbfs -a rom_digest -o "$UPDATED_ROM" -f /tmp/digest.bin +else + # Replace the file content + cbfs -p rom_digest -o "$UPDATED_ROM" -f /tmp/digest.bin +fi + +# If we are just zeroing the digest, we're done, otherwise calculate a digest +# and set it +if [ -z "$ARG_ZERO" ]; then + DIGEST_HEX="$(sha256sum "$UPDATED_ROM" | cut -d\ -f1)" + + echo -n "$DIGEST_HEX" | xxd -p -r >/tmp/digest.bin + cbfs -p rom_digest -o "$UPDATED_ROM" -f /tmp/digest.bin +fi diff --git a/initrd/bin/check_digest.sh b/initrd/bin/check_digest.sh new file mode 100755 index 000000000..22e5d37f3 --- /dev/null +++ b/initrd/bin/check_digest.sh @@ -0,0 +1,33 @@ +#! /bin/bash + +set -eo pipefail + +if [ "$#" -lt 1 ]; then + echo "usage: $0 " >&2 + echo + echo "Checks the integrity of the specified ROM using the embedded" + echo "SHA-256 digest in rom_digest." + echo + echo "If the digest is found, prints one of the following:" + echo " OK - The digest matches the ROM." + echo " Corrupt - The digest does not match the ROM." + echo + echo "If the integrity check can't be performed (no digest, or it" + echo "can't be read, etc.), the script fails." + exit 1 +fi + +ROM="$(realpath "$1")" + +cd "$(dirname "${BASH_SOURCE[0]}")" + +# If there is no digest, this causes the script to fail via set -e. +DIGEST_HEX="$(cbfs -o "$ROM" -r rom_digest | xxd -p | tr -d ' \n')" + +calc_digest.sh -z "$ROM" "/tmp/verify-digest.tmp" + +if echo "$DIGEST_HEX /tmp/verify-digest.tmp" | sha256sum -c; then + echo "OK" +else + echo "Corrupt" +fi diff --git a/patches/flashtools-d1e6f12568cb23387144a4b7a6535fe1bc1e79b1.patch b/patches/flashtools-d1e6f12568cb23387144a4b7a6535fe1bc1e79b1.patch new file mode 100644 index 000000000..883a54375 --- /dev/null +++ b/patches/flashtools-d1e6f12568cb23387144a4b7a6535fe1bc1e79b1.patch @@ -0,0 +1,139 @@ +diff --git a/cbfs.c b/cbfs.c +index 6252488..b53d89b 100644 +--- a/cbfs.c ++++ b/cbfs.c +@@ -39,6 +39,7 @@ static const struct option long_options[] = { + { "read", 1, NULL, 'r' }, + { "add", 1, NULL, 'a' }, + { "file", 1, NULL, 'f' }, ++ { "replace", 1, NULL, 'p' }, + { "rom", 1, NULL, 'o' }, + { "list", 0, NULL, 'l' }, + { "type", 1, NULL, 't' }, +@@ -57,6 +58,7 @@ static const char usage[] = + " -r | --read name Export a CBFS file to stdout\n" + " -a | --add name -f | --file path Add a CBFS file\n" + " -d | --delete name Delete a CBFS file\n" ++" -p | --replace name -f path Replace a CBFS file with same-size content\n" + " -t | --type 50 Filter/set to CBFS file type (hex)\n" + "\n"; + +@@ -424,6 +426,7 @@ int main(int argc, char** argv) { + int use_file = 0; + int do_delete = 0; + int do_add = 0; ++ int do_replace = 0; + int do_read = 0; + int do_list = 0; + int do_type = 0; +@@ -431,7 +434,7 @@ int main(int argc, char** argv) { + const char * romname = NULL; + const char * cbfsname = NULL; + const char * filename = NULL; +- while ((opt = getopt_long(argc, argv, "h?vld:a:f:o:r:t:", ++ while ((opt = getopt_long(argc, argv, "h?vld:a:f:o:r:t:p:", + long_options, NULL)) != -1) + { + switch(opt) +@@ -465,6 +468,10 @@ int main(int argc, char** argv) { + do_type = 1; + cbfs_file_type = strtoul(optarg, NULL, 16); + break; ++ case 'p': ++ do_replace = 1; ++ cbfsname = optarg; ++ break; + case '?': case 'h': + fprintf(stderr, "%s", usage); + return EXIT_SUCCESS; +@@ -474,13 +481,13 @@ int main(int argc, char** argv) { + } + } + +- if (!do_list && !do_read && !do_add && !do_delete) { ++ if (!do_list && !do_read && !do_add && !do_delete && !do_replace) { + fprintf(stderr, "%s", usage); + return EXIT_FAILURE; + } + +- if (do_add && do_delete) { +- fprintf(stderr, "Unsupported option: add and delete at the same time"); ++ if (do_add + do_delete + do_replace > 1) { ++ fprintf(stderr, "Unsupported option: add/delete/replace at the same time"); + return EXIT_FAILURE; + } + +@@ -500,7 +507,7 @@ int main(int argc, char** argv) { + void *cb_map; + + if (use_file) { +- int readonly = do_add || do_delete ? 0 : 1; ++ int readonly = do_add || do_delete || do_replace ? 0 : 1; + rom = map_file(romname, &size, readonly); + if (rom == NULL) { + fprintf(stderr, "Failed to map ROM file: %s '%s'\n", romname, +@@ -617,6 +624,31 @@ int main(int argc, char** argv) { + } + } + ++ // The purpose of the 'replace' action is to replace identically-sized data ++ // in a CBFS file without altering any other part of the ROM. This is used ++ // for digest verification - we must be able to set and clear the digest ++ // without altering any other data, which would affect the digest. ++ // ++ // Currently replace only works with identically-sized content. This could ++ // be extended to also allow resizing the file (potentially moving it if a ++ // large allocation is needed) but the guarantee for identically-sized data ++ // must be preserved. ++ // ++ // cbfs does not support CBFS extended attributes, a hash extended attribute ++ // is not checked or updated. ++ ++ // Replacement file content position, length ++ void *replace_data = NULL; ++ uint64_t replace_len = 0; ++ if (do_replace) { ++ replace_data = map_file(filename, &replace_len, 1); ++ if (replace_data == NULL && errno > 0) { ++ fprintf(stderr, "Failed to map replacement file: %s '%s'\n", filename, ++ strerror(errno)); ++ return EXIT_FAILURE; ++ } ++ } ++ + // loop through files + off = rom + ((uint64_t) header.offset); + while (off < rom + size) { +@@ -731,6 +763,23 @@ int main(int argc, char** argv) { + } + } + ++ if (do_replace) { ++ if (strncmp(name, cbfsname, name_size) == 0) { ++ if (file.len != replace_len) { ++ fprintf(stderr, "Cannot replace content of size %" PRIu64 ++ " with file of size %" PRIu32 "\n", replace_len, ++ file.len); ++ return EXIT_FAILURE; ++ } ++ ++ void *replace_write = off + file.offset; ++ memcpy(replace_write, replace_data, replace_len); ++ ++ ++do_replace; ++ break; ++ } ++ } ++ + off += inc; + } + +@@ -791,7 +840,7 @@ int main(int argc, char** argv) { + (delete_empty_end - delete_empty_start - empty_offset)); + } + +- if (do_read == 1) { ++ if (do_read == 1 || do_replace == 1) { + fprintf(stderr, "Failed to find CBFS file named '%s'\n", cbfsname); + return EXIT_FAILURE; + }