Add reseal command for external signing Pass `--external-sign` to `picotool seal` to use a blank signature block and output the hash Then sign this hash externally, and pass signature and public key to `picotool reseal`
diff --git a/README.md b/README.md index b22ba87..c4cb7f1 100644 --- a/README.md +++ b/README.md
@@ -29,12 +29,15 @@ picotool erase -r <from> <to> [device-selection] picotool reboot [-a] [-u] [-g <partition>] [-c <cpu>] [device-selection] picotool seal [--quiet] [--verbose] [--hash] [--sign] [--clear] [--pin-xip-sram] - [--no-squash] <infile> [-t <type>] [-o <offset>] <outfile> [-t <type>] [<key>] - [<otp>] [--major <major>] [--minor <minor>] [--rollback <rollback> [<rows>..]] + [--no-squash] [--external-sign] <infile> [-t <type>] [-o <offset>] <outfile> + [-t <type>] [<key>] [<otp>] [--major <major>] [--minor <minor>] [--rollback + <rollback> [<rows>..]] picotool encrypt [--quiet] [--verbose] [--embed] [--fast-rosc] [--use-mbedtls] [--otp-key-page <page>] [--hash] [--sign] [--no-clear] [--pin-xip-sram] <infile> [-t <type>] [-o <offset>] <outfile> [-t <type>] <aes_key> <iv_salt> [<signing_key>] [<otp>] + picotool reseal [--quiet] [--verbose] <infile> [-t <type>] [-o <offset>] <sigfile> <pubkey> + [<otp>] picotool partition info|create picotool uf2 convert|combine|info picotool otp get|set|load|white-label|permissions|dump|list @@ -58,6 +61,7 @@ reboot Reboot the device seal Add final metadata to a binary, optionally including a hash and/or signature. encrypt Encrypt the program. + reseal Replace the signature in the final metadata of a binary. partition Commands related to RP2350 Partition Tables uf2 Commands related to UF2 creation and status otp Commands related to the RP2350 OTP (One-Time-Programmable) Memory @@ -71,7 +75,7 @@ Note commands that aren't acting on files require a device in BOOTSEL mode to be connected. ## Links to documentation for `picotool` commands -[`info`](#info) [`config`](#config) [`load`](#load) [`save`](#save) [`verify`](#verify) [`erase`](#erase) [`reboot`](#reboot) [`seal`](#seal) [`encrypt`](#encrypt) [`partition`](#partition) [`uf2`](#uf2) [`otp`](#otp) [`coprodis`](#coprodis) [`link`](#link) [`bdev`](#bdev) +[`info`](#info) [`config`](#config) [`load`](#load) [`save`](#save) [`verify`](#verify) [`erase`](#erase) [`reboot`](#reboot) [`seal`](#seal) [`encrypt`](#encrypt) [`reseal`](#reseal) [`partition`](#partition) [`uf2`](#uf2) [`otp`](#otp) [`coprodis`](#coprodis) [`link`](#link) [`bdev`](#bdev) ## Building & Installing @@ -715,8 +719,9 @@ SYNOPSIS: picotool seal [--quiet] [--verbose] [--hash] [--sign] [--clear] [--pin-xip-sram] - [--no-squash] <infile> [-t <type>] [-o <offset>] <outfile> [-t <type>] [<key>] - [<otp>] [--major <major>] [--minor <minor>] [--rollback <rollback> [<rows>..]] + [--no-squash] [--external-sign] <infile> [-t <type>] [-o <offset>] <outfile> + [-t <type>] [<key>] [<otp>] [--major <major>] [--minor <minor>] [--rollback + <rollback> [<rows>..]] OPTIONS: --quiet @@ -744,6 +749,8 @@ Pin XIP SRAM on load --no-squash Don't squash segments in the ELF file + --external-sign + For use with external signing and `picotool reseal` File to load from <infile> The file name @@ -843,6 +850,88 @@ Specify file type (uf2 | elf | bin) explicitly, ignoring file extension ``` +## reseal + +`reseal` allows you to modify the signature of a binary that has already been sealed with `seal`. + +This can be used for signing binaries without passing `picotool` the private key, for example with hardware security modules, or password-protected private keys. As with `seal`, your signing key must be for the _secp256k1_ curve, the public key must be in the PEM format, and the signature must be in the DER format. + +For an example, to generate a password-protected private key and corresponding public key, you could use the following commands: +```text +$ openssl ecparam -name secp256k1 -genkey -out private.pem +$ openssl ec -aes256 -in private.pem -out private.enc.pem +read EC key +writing EC key +Enter pass phrase for PEM: +Verifying - Enter pass phrase for PEM: +$ mv private.enc.pem private.pem +$ openssl ec -in private.pem -out public.pem -pubout +read EC key +Enter pass phrase for private.pem: +writing EC key +``` + +Then seal the binary and sign the hash (and verify the signature): +```text +$ picotool seal --external-sign hello_usb.uf2 hello_usb.signed.uf2 --quiet > hash.txt +$ xxd -r -p hash.txt > hash.bin +$ openssl pkeyutl -in hash.bin -inkey private.pem -out signature.der -pkeyopt digest:sha256 +Enter pass phrase for private.pem: +$ openssl pkeyutl -in hash.bin -inkey public.pem -pubin -verify -sigfile signature.der -pkeyopt digest:sha256 +Signature Verified Successfully +``` + +Finally, reseal the binary with the new signature: +```text +$ picotool reseal hello_usb.signed.uf2 signature.der public.pem otp.json +Resealed File hello_usb.signed.uf2: + +Program Information + name: hello_usb + web site: https://github.com/raspberrypi/pico-examples/tree/HEAD/hello_world/usb + features: USB stdin / stdout + binary start: 0x10000000 + binary end: 0x10005038 + target chip: RP2350 + image type: ARM Secure + hash: verified + signature: verified +``` + +This will produce the same output binary as `picotool seal --sign --hash ...` would for a non-password-protected key. + +```text +$ picotool help reseal +RESEAL: + Replace the signature in the final metadata of a binary. + +SYNOPSIS: + picotool reseal [--quiet] [--verbose] <infile> [-t <type>] [-o <offset>] <sigfile> <pubkey> + [<otp>] + +OPTIONS: + --quiet + Don't print any output + --verbose + Print verbose output + <sigfile> + Signature file (.der) + <pubkey> + Public key file (.pem) + <otp> + JSON file to save OTP to (will edit existing file if it exists) + File to re-seal + <infile> + The file name + -t <type> + Specify file type (uf2 | elf | bin) explicitly, ignoring file extension + BIN file options + -o, --offset + Specify the load address for a BIN file + <offset> + Load offset (memory address; default 0x10000000) +``` + ## partition The `partition` commands allow you to interact with the partition tables on RP2350 devices, and also create them.
diff --git a/bintool/bintool.cpp b/bintool/bintool.cpp index 43a23a0..e0a2986 100644 --- a/bintool/bintool.cpp +++ b/bintool/bintool.cpp
@@ -40,15 +40,20 @@ int rc; mbedtls_pk_init(&pk_ctx); -#if MBEDTLS_VERSION_MAJOR >= 3 - // This rng is only used for blinding when reading the key file - // As this should only be done on a secure computer, blinding is not required, so it's fine to not actually seed it with any entropy - mbedtls_ctr_drbg_context ctr_drbg; - mbedtls_ctr_drbg_init(&ctr_drbg); - rc = mbedtls_pk_parse_keyfile(&pk_ctx, filename.c_str(), NULL, mbedtls_ctr_drbg_random, &ctr_drbg); -#else - rc = mbedtls_pk_parse_keyfile(&pk_ctx, filename.c_str(), NULL); -#endif + if (private_key == nullptr) { + // Only read public key + rc = mbedtls_pk_parse_public_keyfile(&pk_ctx, filename.c_str()); + } else { + #if MBEDTLS_VERSION_MAJOR >= 3 + // This rng is only used for blinding when reading the key file + // As this should only be done on a secure computer, blinding is not required, so it's fine to not actually seed it with any entropy + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_ctr_drbg_init(&ctr_drbg); + rc = mbedtls_pk_parse_keyfile(&pk_ctx, filename.c_str(), NULL, mbedtls_ctr_drbg_random, &ctr_drbg); + #else + rc = mbedtls_pk_parse_keyfile(&pk_ctx, filename.c_str(), NULL); + #endif + } if (rc != 0) { char error_string[128]; mbedtls_strerror(rc, error_string, sizeof(error_string)); @@ -60,7 +65,9 @@ if (!keypair) { fail(ERROR_FORMAT, "Failed to parse key file %s", filename.c_str()); } - mbedtls_mpi_write_binary(&keypair->d, reinterpret_cast<unsigned char *>(private_key), 32); + if (private_key != nullptr) { + mbedtls_mpi_write_binary(&keypair->d, reinterpret_cast<unsigned char *>(private_key), 32); + } mbedtls_mpi_write_binary(&keypair->Q.X, reinterpret_cast<unsigned char *>(public_key), 32); mbedtls_mpi_write_binary(&keypair->Q.Y, reinterpret_cast<unsigned char *>(public_key) + 32, 32); return 0; @@ -690,13 +697,21 @@ e = rand(); } - signature_t sig; - sign_sha256(entropy, sizeof(entropy), &sha256, &public_key, &private_key, &sig); - dumper("SIG", sig); + signature_t sig = {0}; - uint32_t err = verify_signature_secp256k1(&sig, &public_key, &sha256); - if (err) { - fail(ERROR_VERIFICATION_FAILED, "Signature verification failed"); + // Skip signing if keys are zero, as that is a binary for external signing + int public_zero = memcmp(&(public_key.bytes[0]), &(public_key.bytes[1]), sizeof(public_key.bytes) - 1); + int private_zero = memcmp(&(private_key.bytes[0]), &(private_key.bytes[1]), sizeof(private_key.bytes) - 1); + bool empty_keys = (public_zero == 0 && public_key.bytes[0] == 0 && private_zero == 0 && private_key.bytes[0] == 0); + + if (!empty_keys) { + sign_sha256(entropy, sizeof(entropy), &sha256, &public_key, &private_key, &sig); + dumper("SIG", sig); + + uint32_t err = verify_signature_secp256k1(&sig, &public_key, &sha256); + if (err) { + fail(ERROR_VERIFICATION_FAILED, "Signature verification failed"); + } } std::shared_ptr<signature_item> signature = std::make_shared<signature_item>(PICOBIN_SIGNATURE_SECP256K1);
diff --git a/bintool/mbedtls_wrapper.c b/bintool/mbedtls_wrapper.c index c623ebd..89568ac 100644 --- a/bintool/mbedtls_wrapper.c +++ b/bintool/mbedtls_wrapper.c
@@ -36,6 +36,8 @@ #define dump_pubkey(...) ((void)0) #endif +#define assert_or_return(assertion, retval) assert(assertion); if (!(assertion)) return retval; + void mb_sha256_buffer(const uint8_t *data, size_t len, message_digest_t *digest_out) { mbedtls_sha256(data, len, digest_out->bytes, 0); } @@ -150,14 +152,14 @@ } -void der_to_raw(signature_t *sig) { - assert(sig->der[0] == 0x30); - assert(sig->der[2] == 0x02); +bool der_to_raw(signature_t *sig) { + assert_or_return(sig->der[0] == 0x30, false); + assert_or_return(sig->der[2] == 0x02, false); uint8_t b2 = sig->der[3]; - assert(sig->der[4 + b2] == 0x02); + assert_or_return(sig->der[4 + b2] == 0x02, false); uint8_t b3 = sig->der[5 + b2]; - assert(sig->der_len == 6u + b2 + b3); + assert_or_return(sig->der_len == 6u + b2 + b3, false); unsigned char r[32]; if (b2 == 33) { @@ -182,6 +184,8 @@ memset(sig->bytes, 0, sizeof(sig->bytes)); memcpy(sig->bytes, r, sizeof(r)); memcpy(sig->bytes + 32, s, sizeof(s)); + + return true; }
diff --git a/bintool/mbedtls_wrapper.h b/bintool/mbedtls_wrapper.h index 48c06f7..9e2526d 100644 --- a/bintool/mbedtls_wrapper.h +++ b/bintool/mbedtls_wrapper.h
@@ -9,6 +9,7 @@ #include <stdint.h> #include <stdlib.h> #include <assert.h> +#include <stdbool.h> #include <mbedtls/sha256.h> #include <mbedtls/ecdsa.h> @@ -78,6 +79,8 @@ const public_t public_key[1], const message_digest_t digest[1]); +bool der_to_raw(signature_t *sig); + #define sha256_buffer mb_sha256_buffer #define aes256_buffer mb_aes256_buffer #define sign_sha256 mb_sign_sha256
diff --git a/main.cpp b/main.cpp index 38c467b..9dc7fcb 100644 --- a/main.cpp +++ b/main.cpp
@@ -236,7 +236,7 @@ }; #endif -enum class filetype {bin, elf, uf2, pem, json}; +enum class filetype {bin, elf, uf2, pem, json, der}; const string getFiletypeName(enum filetype type) { switch (type) @@ -246,6 +246,7 @@ case filetype::uf2: return "UF2"; case filetype::pem: return "PEM"; case filetype::json: return "JSON"; + case filetype::der: return "DER"; default: assert(false); return "ERROR_TYPE"; } } @@ -633,6 +634,7 @@ bool pin_xip_sram = false; bool set_tbyb = false; bool no_squash = false; + bool external_sign = false; uint16_t major_version = 0; uint16_t minor_version = 0; uint16_t rollback_version = 0; @@ -1203,7 +1205,8 @@ option("--sign").set(settings.seal.sign) % "Sign the file" + option("--clear").set(settings.seal.clear_sram) % "Clear all of main SRAM on load" + option("--pin-xip-sram").set(settings.seal.pin_xip_sram) % "Pin XIP SRAM on load" + - option("--no-squash").set(settings.seal.no_squash) % "Don't squash segments in the ELF file" + option("--no-squash").set(settings.seal.no_squash) % "Don't squash segments in the ELF file" + + option("--external-sign").set(settings.seal.external_sign) % "For use with external signing and `picotool reseal`" ).min(0).doc_non_optional(true) % "Configuration" + named_file_selection_x("infile", 0) % "File to load from" + ( @@ -1233,6 +1236,31 @@ return "Add final metadata to a binary, optionally including a hash and/or signature."; } }; + +struct reseal_command : public cmd { + reseal_command() : cmd("reseal") {} + bool execute(device_map &devices) override; + virtual device_support get_device_support() override { return none; } + + group get_cli() override { + return ( + option("--quiet").set(settings.quiet) % "Don't print any output" + + option("--verbose").set(settings.verbose) % "Print verbose output" + + named_file_selection_x("infile", 0) % "File to re-seal" + + ( + option('o', "--offset").set(settings.offset_set) % "Specify the load address for a BIN file" & + hex("offset").set(settings.offset) % "Load offset (memory address; default 0x10000000)" + ).force_expand_help(true) % "BIN file options" + + named_untyped_file_selection_x("sigfile", 1) % "Signature file (.der)" + + named_untyped_file_selection_x("pubkey", 2) % "Public key file (.pem)" + + optional_untyped_file_selection_x("otp", 3) % "JSON file to save OTP to (will edit existing file if it exists)" + ); + } + + string get_doc() const override { + return "Replace the signature in the final metadata of a binary."; + } +}; #endif struct link_command : public cmd { @@ -1806,6 +1834,7 @@ #if HAS_MBEDTLS std::shared_ptr<cmd>(new seal_command()), std::shared_ptr<cmd>(new encrypt_command()), + std::shared_ptr<cmd>(new reseal_command()), #endif std::shared_ptr<cmd>(new partition_command()), std::shared_ptr<cmd>(new uf2_command()), @@ -3242,6 +3271,8 @@ return filetype::pem; } else if (low.rfind(".json") == low.size() - 5) { return filetype::json; + } else if (low.rfind(".der") == low.size() - 4) { + return filetype::der; } } else if (!file_type.empty()) { low = lowercase(file_type); @@ -3260,6 +3291,9 @@ if (low == "json") { return filetype::json; } + if (low == "der") { + return filetype::der; + } throw cli::parse_error("unsupported file type '" + low + "'"); } throw cli::parse_error("filename '" + filename+ "' does not have a recognized file type (extension)"); @@ -6119,6 +6153,115 @@ return false; } +void output_otp_secure_boot(uint8_t idx, public_t public_key) { + message_digest_t pub_sha256; + sha256_buffer(public_key.bytes, sizeof(public_key.bytes), &pub_sha256); + DEBUG_LOG("PUBLIC KEY SHA256 "); + for(uint8_t i : pub_sha256.bytes) { + DEBUG_LOG("%02x", i); + } + DEBUG_LOG("\n"); + + if (get_file_type_idx(idx) != filetype::json) { + fail(ERROR_ARGS, "Can only output OTP json"); + } + auto check_json_file = std::ifstream(settings.filenames[idx]); + json otp_json; + if (check_json_file.good()) { + otp_json = json::parse(check_json_file); + DEBUG_LOG("Appending to existing otp json\n"); + check_json_file.close(); + } + auto json_out = get_file_idx(ios::out, idx); + + // Add otp bootkey rows + for (int i = 0; i < 32; ++i) { + otp_json["bootkey0"][i] = pub_sha256.bytes[i]; + } + + // Add otp fields to enable secure boot + otp_json["crit1"]["secure_boot_enable"] = 1; + otp_json["boot_flags1"]["key_valid"] = 1; + + *json_out << std::setw(4) << otp_json << std::endl; + json_out->close(); +} + +bool reseal_command::execute(device_map &devices) { + + if (get_file_type_idx(1) != filetype::der) { + fail(ERROR_ARGS, "Can only read der signatures"); + } + + if (get_file_type_idx(2) != filetype::pem) { + fail(ERROR_ARGS, "Can only read pem keys"); + } + + public_t public_key = {0}; + + read_keys(settings.filenames[2], &public_key, nullptr); + + signature_t signature = {0}; + + auto sigfile = get_file_idx(ios::in|ios::binary, 1); + sigfile->exceptions(std::iostream::failbit | std::iostream::badbit); + sigfile->seekg(0, std::ios::end); + signature.der_len = sigfile->tellg(); + sigfile->seekg(0, std::ios::beg); + sigfile->read((char*)signature.der, signature.der_len); + sigfile->close(); + + if (!der_to_raw(&signature)) { + fail(ERROR_ARGS, "Signature was not in DER format"); + } + + auto file_access = get_file_memory_access(0, true); + set_model_from_metadata(file_access); + vector<uint8_t> bin; + std::unique_ptr<block> last_block = find_last_block(file_access, bin); + if (last_block == nullptr) { + fail(ERROR_NOT_POSSIBLE, "Last block not found"); + } + + // Replace signature in block + std::shared_ptr<signature_item> sig_item = last_block->get_item<signature_item>(); + if (sig_item == nullptr) { + fail(ERROR_NOT_POSSIBLE, "Last block does not contain a signature item"); + } + sig_item->public_key_bytes = std::vector<uint8_t>(public_key.bytes, public_key.bytes + sizeof(public_key.bytes)); + sig_item->signature_bytes = std::vector<uint8_t>(signature.bytes, signature.bytes + sizeof(signature.bytes)); + + // Verify the signature + std::shared_ptr<hash_value_item> hash_item = last_block->get_item<hash_value_item>(); + if (hash_item == nullptr) { + fail(ERROR_NOT_POSSIBLE, "Last block does not contain a hash value item"); + } + message_digest_t sha256; + memcpy(sha256.bytes, hash_item->hash_bytes.data(), sizeof(sha256.bytes)); + uint32_t err = verify_signature_secp256k1(&signature, &public_key, &sha256); + if (err) { + fail(ERROR_VERIFICATION_FAILED, "Signature verification failed"); + } + + // Write out modified block + std::vector<uint32_t> words = last_block->to_words(); + file_access.write_vector(last_block->physical_addr, words); + + // Write OTP JSON if requested + if (!settings.filenames[3].empty()) { + output_otp_secure_boot(3, public_key); + } + + if (!settings.quiet) { + set_model_from_metadata(file_access); + fos << "Resealed File " << settings.filenames[0] << ":\n\n"; + settings.info.show_basic = true; + info_guts(file_access, nullptr); + } + + return false; +} + bool seal_command::execute(device_map &devices) { bool isElf = false; bool isBin = false; @@ -6133,11 +6276,16 @@ fail(ERROR_ARGS, "Can only sign ELFs, BINs or UF2s"); } + if (settings.seal.external_sign) { + settings.seal.sign = true; + settings.seal.hash = true; + } + if (get_file_type_idx(1) != get_file_type()) { fail(ERROR_ARGS, "Can only sign to same file type"); } - if (settings.seal.sign && settings.filenames[2].empty()) { + if (settings.seal.sign && settings.filenames[2].empty() && !settings.seal.external_sign) { fail(ERROR_ARGS, "missing key file for signing"); } @@ -6172,10 +6320,10 @@ } - private_t private_key = {}; - public_t public_key = {}; + private_t private_key = {0}; + public_t public_key = {0}; - if (settings.seal.sign) read_keys(settings.filenames[2], &public_key, &private_key); + if (settings.seal.sign && !settings.seal.external_sign) read_keys(settings.filenames[2], &public_key, &private_key); model_t model = get_model(0); @@ -6236,40 +6384,8 @@ fail(ERROR_ARGS, "Must be ELF or BIN"); } - if (settings.seal.sign) { - message_digest_t pub_sha256; - sha256_buffer(public_key.bytes, sizeof(public_key.bytes), &pub_sha256); - DEBUG_LOG("PUBLIC KEY SHA256 "); - for(uint8_t i : pub_sha256.bytes) { - DEBUG_LOG("%02x", i); - } - DEBUG_LOG("\n"); - - if (!settings.filenames[3].empty()) { - if (get_file_type_idx(3) != filetype::json) { - fail(ERROR_ARGS, "Can only output OTP json"); - } - auto check_json_file = std::ifstream(settings.filenames[3]); - json otp_json; - if (check_json_file.good()) { - otp_json = json::parse(check_json_file); - DEBUG_LOG("Appending to existing otp json\n"); - check_json_file.close(); - } - auto json_out = get_file_idx(ios::out, 3); - - // Add otp bootkey rows - for (int i = 0; i < 32; ++i) { - otp_json["bootkey0"][i] = pub_sha256.bytes[i]; - } - - // Add otp fields to enable secure boot - otp_json["crit1"]["secure_boot_enable"] = 1; - otp_json["boot_flags1"]["key_valid"] = 1; - - *json_out << std::setw(4) << otp_json << std::endl; - json_out->close(); - } + if (settings.seal.sign && !settings.filenames[3].empty()) { + output_otp_secure_boot(3, public_key); } if (!settings.quiet) { @@ -6280,6 +6396,28 @@ info_guts(access, nullptr); } + if (settings.seal.external_sign) { + auto access = get_file_memory_access(1); + set_model_from_metadata(access); + vector<uint8_t> bin; + std::unique_ptr<block> last_block = find_last_block(access, bin); + std::shared_ptr<hash_value_item> hash_value = last_block->get_item<hash_value_item>(); + if(hash_value != nullptr) { + std::stringstream val; + for(uint8_t i : hash_value->hash_bytes) { + val << hex_string(i, 2, false, true); + } + if (settings.quiet) { + // Just print hash value + printf("%s\n", val.str().c_str()); + } else { + fos.first_column(0); + fos.hanging_indent(0); + fos << "\nHash value for external signing: " << val.str() << "\n"; + } + } + } + return false; } #endif