diff options
Diffstat (limited to 'scripts/image_signing/sign_gsc_firmware.sh')
-rwxr-xr-x | scripts/image_signing/sign_gsc_firmware.sh | 508 |
1 files changed, 508 insertions, 0 deletions
diff --git a/scripts/image_signing/sign_gsc_firmware.sh b/scripts/image_signing/sign_gsc_firmware.sh new file mode 100755 index 00000000..768844ad --- /dev/null +++ b/scripts/image_signing/sign_gsc_firmware.sh @@ -0,0 +1,508 @@ +#!/bin/bash +# Copyright 2018 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. + +. "$(dirname "$0")/common.sh" + +load_shflags || exit 1 + +DEFINE_boolean override_keyid "${FLAGS_TRUE}" \ + "Override keyid from manifest." "" + +FLAGS_HELP="Usage: ${PROG} [options] <input_dir> <key_dir> <output_image> + +Signs <input_dir> with keys in <key_dir>. +" + +# Parse command line. +FLAGS "$@" || exit 1 +eval set -- "${FLAGS_ARGV}" + +# Abort on error and uninitialized variables. +set -e +set -u + +PRE_PVT_BID_FLAG=0x10 +MP_BID_FLAG=0x10000 +CR50_FACTORY_VERSION="0.3.22" + +# Convert unsigned 32 bit value into a signed one. +to_int32() { + local inp="$1" + python -c \ + "import struct; \ + d=struct.pack('I', $inp); \ + print (struct.unpack('i', d)[0])" +} + +# This function accepts one argument, the name of the GSC manifest file which +# needs to be verified and in certain cases altered. +# +# The function verifies that the input manifest is a proper json file, and +# that the manifest conforms to GSC version numbering and board ID flags +# conventions for various build images: +# +# - only factory version binaries can be converted to node locked images, +# board IDs for node locked images come from signing instructions, and the +# config1 manifest field value must have the 0x80000000 bit set. +# +# - when signing pre-pvt binaries (major version number is even) the 0x10 +# flags bit must be set. +# +# - when signing mp images (major version number is odd), the 0x10000 flags +# bit must be set (this can be overridden by signing instructions). +verify_and_prepare_gsc_manifest() { + if [[ $# -ne 1 ]]; then + die "Usage: verify_and_prepare_gsc_manifest <manifest .json file>" + fi + + local manifest_json="$1" + + local bid_flags + local config1 + local epoch + local major + local minor + local values + + mapfile -t values < <(jq '.config1,.epoch,.major,.minor,.board_id_flags' \ + "${manifest_json}") + + config1="${values[0]}" + epoch="${values[1]}" + major="${values[2]}" + minor="${values[3]}" + bid_flags="${values[4]}" + + if [[ ${major} == null ]]; then + die "Major version number not found in ${manifest_json}" + fi + + if [[ ${bid_flags} == null ]]; then + die "bid_flags not found in ${manifest_json}" + fi + + case "${INSN_TARGET:-}" in + + (NodeLocked) + if [[ -z ${INSN_DEVICE_ID:-} ]]; then + die "Node locked target without Device ID value" + fi + # Case of a node locked image, it must have the fixed factory version. + if [[ "${epoch}.${major}.${minor}" != "${CR50_FACTORY_VERSION}" ]];then + die "Won't create node locked images for version $epoch.$major.$minor" + fi + + local sub + local devid0 + local devid1 + + devid0="$(to_int32 "0x${INSN_DEVICE_ID/-*}")" + devid1="$(to_int32 "0x${INSN_DEVICE_ID/*-}")" + cf1="$(to_int32 $(( 0x80000000 + config1 )))" + sub="$(printf " \"DEV_ID0\": %s,\\\n \"DEV_ID1\": %s," \ + "${devid0}" "${devid1}")" + + # Manifest fields must be modified as follows: + # + # - board_id related fields removed + # - 'config1' field bit 0x80000000 set + # - least significant bit of the 'tag' field originally set to all zeros + # changed from zero to one + # - DEV_ID values spliced in into the 'fuses' section + sed -i "/board_id/d;\ + s/\"config1\":.*/\"config1\": ${cf1},/;\ + s/\(tag.*0\+\)0/\11/;\ + /\"fuses\":/ a\ + $sub" "${manifest_json}" || die "Failed to edit the manifest" + return 0 + ;; + + (PrePVT) + # All we care about for pre pvt images is that major version number is + # even and the 0x10 Board ID flag is set. + if (( !(major & 1 ) && (bid_flags & PRE_PVT_BID_FLAG) )); then + return 0 + fi + ;; + + (ReleaseCandidate|GeneralRelease) + if (( (bid_flags & MP_BID_FLAG) && (major & 1) )); then + if [[ ${INSN_TARGET} == GeneralRelease ]]; then + # Strip Board ID information for approved for release MP images. + sed -i "/board_id/d" "${manifest_json}" + fi + return 0 + fi + ;; + + (*) + die "Unsupported target '${INSN_TARGET:-}'" + esac + + die "Inconsistent manifest ${manifest_json}: major = '${major}'," \ + "board_id_flags = '${bid_flags}' target = '${INSN_TARGET}'" +} + +# This function accepts two arguments, names of two binary files. +# +# It searches the first passed-in file for the first 8 bytes of the second +# passed in file. The od utility is used to generate full hex dump of the +# first file (16 bytes per line) and the first 8 bytes of the second file. +# grep is used to check if the pattern is present in the full dump. +find_blob_in_blob() { + if [[ $# -ne 2 ]]; then + die "Usage: find_blob_in_blob <haystack> <needle>" + fi + + local main_blob="$1" + local pattern_blob="$2" + local pattern + # Show without offsets, single byte hex, no compression of zero runs. + local od_options=("-An" "-tx1" "-v") + + # Get the first 8 bytes of the pattern blob. + pattern="$(od "${od_options[@]}" -N8 "${pattern_blob}")" + + # Eliminate all newlines to be able to search the entire body as one unit. + if od "${od_options[@]}" "${main_blob}" | \ + tr -d '\n' | + grep -q -F "${pattern}"; then + return 0 + fi + + return 1 +} + +# This function accepts two arguments, names of the two ELF files. +# +# The files are searched for test RMA public key patterns - x25519 or p256, +# both files are supposed to have pattern of one of these keys and not the +# other. If this holds true the function prints the public key base name. If +# not both files include the same key, or include more than one key, the +# function reports failure and exits the script. +determine_rma_key_base() { + if [[ $# -ne 3 ]]; then + die "Usage: determine_rma_key_base <rma_key_dir> <rw_a> <rw_b>" + fi + + local rma_key_dir="$1" + local elfs=( "$2" "$3" ) + local base_name="${rma_key_dir}/rma_key_blob" + local curve + local curves=( "x25519" "p256" ) + local elf + local key_file + local mask=1 + local result=0 + local rma_key_base + + for curve in "${curves[@]}"; do + key_file="${base_name}.${curve}.test" + for elf in "${elfs[@]}"; do + if find_blob_in_blob "${elf}" "${key_file}"; then + : $(( result |= mask )) + fi + : $(( mask <<= 1 )) + done + done + + case "${result}" in + (3) curve="x25519";; + (12) curve="p256";; + (*) die "could not determine key type in the ELF files";; + esac + + echo "${base_name}.${curve}" +} + +# Sign GSC RW firmware ELF images into a combined GSC firmware image +# using the provided production keys and manifests. +sign_rw() { + if [[ $# -ne 8 ]]; then + die "Usage: sign_rw <key_file> <manifest> <fuses>" \ + "<rma_key_dir> <rw_a> <rw_b> <output> <generation>" + fi + + local key_file="$1" + local manifest_file="$2" + local fuses_file="$3" + local rma_key_dir="$4" + local elfs=( "$5" "$6" ) + local result_file="$7" + local generation="$8" + local temp_dir + local rma_key_base="" + local rw_a_offset + local rw_b_offset + + temp_dir="$(make_temp_dir)" + + if [[ ! -f "${result_file}" ]]; then + die "${result_file} not found." + fi + + local signer_command_params=(-x "${fuses_file}" --key "${key_file}") + + case "${generation}" in + (h) + # H1 image might require some tweaking. + # If signing a chip factory image (version 0.0.22) do not try figuring + # out the RMA keys. + local gsc_version + + gsc_version="$(jq '.epoch * 10000 + .major * 100 + .minor' \ + "${manifest_file}")" + + if [[ "${gsc_version}" != "22" ]]; then + rma_key_base="$(determine_rma_key_base "${rma_key_dir}" "${elfs[@]}")" + else + echo "Ignoring RMA keys for factory branch ${gsc_version}" + fi + + # Swap test public RMA server key with the prod version. + if [[ -n "${rma_key_base}" ]]; then + signer_command_params+=( + --swap "${rma_key_base}.test,${rma_key_base}.prod" + ) + fi + + # Indicate H1 signing. + signer_command_params+=( '--b' ) + # Fixed offsets into the binary blob where RW sections start. + rw_a_offset=16384 + rw_b_offset=278528 + ;; + (d) + # Indicate D1 signing. + signer_command_params+=( '--dauntless' ) + die "Need to figure out D2 RW sections offsets" + ;; + (*) + die "Unknown generation value \"${generation}\"" + ;; + esac + + signer_command_params+=(--json "${manifest_file}") + + signer_command_params+=(--format=bin) + dst_suffix='flat' + + if [[ "${FLAGS_override_keyid}" == "${FLAGS_TRUE}" ]]; then + signer_command_params+=(--override-keyid) + fi + + local count=0 + for elf in "${elfs[@]}"; do + if strings "${elf}" | grep -q "DBG/cr50"; then + die "Will not sign debug image with prod keys" + fi + signed_file="${temp_dir}/${count}.${dst_suffix}" + + # Make sure output file is not owned by root. + touch "${signed_file}" + if ! gsc-codesigner "${signer_command_params[@]}" \ + -i "${elf}" -o "${signed_file}"; then + die "gsc-codesigner ${signer_command_params[*]}" \ + "-i ${elf} -o ${signed_file} failed" + fi + + if [[ -n "${rma_key_base}" ]]; then + if find_blob_in_blob "${signed_file}" "${rma_key_base}.test"; then + die "test RMA key in the signed image!" + fi + + if ! find_blob_in_blob "${signed_file}" "${rma_key_base}.prod"; then + die "prod RMA key not in the signed image!" + fi + fi + : $(( count++ )) + done + + # Full binary image is required, paste the newly signed blobs into the + # output image. + dd if="${temp_dir}/0.${dst_suffix}" of="${result_file}" \ + seek="${rw_a_offset}" bs=1 conv=notrunc + dd if="${temp_dir}/1.${dst_suffix}" of="${result_file}" \ + seek="${rw_b_offset}" bs=1 conv=notrunc +} + +# A very crude RO verification function. The key signature found at a fixed +# offset into the RO blob must match the RO type. Prod keys have bit D2 set to +# one, dev keys have this bit set to zero. +# +# The check is bypassed if the key file directory name includes string 'test'. +verify_ro() { + if [[ $# -ne 2 ]]; then + die "Usage: verify_ro <ro_bin> <key_file>" + fi + + local ro_bin="$1" + local key_file="$2" + local key_byte + local key_path + + if [[ ! -f "${ro_bin}" ]]; then + die "${ro_bin} not a file!" + fi + + key_path="$(dirname "${key_file}")" + if [[ ${key_path##*/} == *"test"* ]]; then + info "Test run, ignoring key type verification" + return 0 + fi + + # Key signature's lowest byte is byte #5 in the line at offset 0001a0. + key_byte="$(od -Ax -t x1 -v "${ro_bin}" | awk '/0001a0/ {print $6}')" + case "${key_byte}" in + (?[4567cdef]) + return 0 + ;; + esac + + die "RO key (${key_byte}) in ${ro_bin} does not match type prod" +} + +# This function prepares a full GSC image, consisting of two ROs and two RWs +# placed at their respective offsets into the resulting blob. It invokes the +# bs (binary signer) script to actually convert ELF versions of RWs into +# binaries and sign them. +# +# The signed image is placed in the directory named as concatenation of RO and +# RW version numbers and board ID fields, if set to non-default. The ebuild +# downloading the tarball from the BCS expects the image to be in that +# directory. +sign_gsc_firmware() { + if [[ $# -ne 9 ]]; then + die "Usage: sign_gsc_firmware <key_file> <manifest> <fuses>" \ + "<rma_key_dir> <ro_a> <ro_b> <rw_a> <rw_b> <output>" + fi + + local key_file="$1" + local manifest_source="$2" + local fuses_file="$3" + local rma_key_dir="$4" + local ro_a_hex="$5" + local ro_b_hex="$6" + local rw_a="$7" + local rw_b="$8" + local output_file="$9" + local generation + local manifest_file + local temp_dir + local ro_b_base + + manifest_file="${manifest_source}.updated" + temp_dir="$(make_temp_dir)" + + # Prepare file for inline editing. + jq . < "${manifest_source}" > "${manifest_file}" || \ + die "basic validation of ${manifest_json} failed" + + # Retrieve chip type from the manifest, if preset, otherwise use h1. + generation="$(jq '.generation' "${manifest_file}")" + case "${generation}" in + (h|null) + generation="h" # Just in case this is a legacy manifest. + + # H1 flash size, image size must match. + IMAGE_SIZE="$(( 512 * 1024 ))" + ;; + (d) + # D2 flash size, image size must match. + IMAGE_SIZE="$(( 512 * 1024 ))" + ;; + (*) + die "Unknown generation value \"${generation}\" in signing manifest" + ;; + esac + + verify_and_prepare_gsc_manifest "${manifest_file}" + + dd if=/dev/zero bs="${IMAGE_SIZE}" count=1 status=none | + tr '\000' '\377' > "${output_file}" + if [[ "$(stat -c '%s' "${output_file}")" != "${IMAGE_SIZE}" ]]; then + die "Failed creating ${output_file}" + fi + + local f + local count=0 + for f in "${ro_a_hex}" "${ro_b_hex}"; do + if ! objcopy -I ihex "${f}" -O binary "${temp_dir}/${count}.bin"; then + die "Failed to convert ${f} from hex to bin" + fi + verify_ro "${temp_dir}/${count}.bin" "${key_file}" + : $(( count++ )) + done + + if ! sign_rw "${key_file}" "${manifest_file}" "${fuses_file}" \ + "${rma_key_dir}" "${rw_a}" "${rw_b}" \ + "${output_file}" "${generation}"; then + die "Failed invoking sign_rw for ELF files ${rw_a} ${rw_b}" + fi + + ro_b_base=$(( IMAGE_SIZE / 2 )) + dd if="${temp_dir}/0.bin" of="${output_file}" conv=notrunc + dd if="${temp_dir}/1.bin" of="${output_file}" seek="${ro_b_base}" bs=1 \ + conv=notrunc + + echo "Image successfully signed to ${output_file}" +} + +# Sign the directory holding GSC firmware. +sign_gsc_firmware_dir() { + if [[ $# -ne 3 ]]; then + die "Usage: sign_gsc_firmware_dir <input> <key> <output>" + fi + + local input="${1%/}" + local key_file="$2" + local output="$3" + + if [[ -d "${output}" ]]; then + output="${output}/cr50.bin.prod" + fi + + sign_gsc_firmware \ + "${key_file}" \ + "${input}/prod.json" \ + "${input}/fuses.xml" \ + "${input}" \ + "${input}/prod.ro.A" \ + "${input}/prod.ro.B" \ + "${input}/ec.RW.elf" \ + "${input}/ec.RW_B.elf" \ + "${output}" +} + +main() { + if [[ $# -ne 3 ]]; then + flags_help + exit 1 + fi + + local input="${1%/}" + local key_dir="$2" + local output="$3" + + local key_file="${key_dir}/cr50.pem" + local signing_instructions="${input}/signing_instructions.sh" + + if [[ -f ${signing_instructions} ]]; then + . "${signing_instructions}" + else + die "${signing_instructions} not found" + fi + + if [[ ! -e "${key_file}" ]]; then + die "Missing key file: ${key_file}" + fi + + if [[ ! -d "${input}" ]]; then + die "Missing input directory: ${input}" + fi + + sign_gsc_firmware_dir "${input}" "${key_file}" "${output}" +} +main "$@" |