#!/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] Signs with keys in . " # 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])" } # Functions allowing to determine the base address of a binary blob in ihex # format. Invoked in a subprocess through () to be able to use stdout as the # return values. # In ihex format binary data is represented as a set of records. Each record # is a text string of hex values in ASCII. All records start with a header # which determines the record contents. # # The most common record type is the data record, its header includes the 16 # bit address of where the record data will have to be placed in the physical # address space. Naturally 16 bits is not enough as of last thirty years, some # special types of record are used to specify the segment base of there the # 16 bit address is used as the offset. # # The segment base is still represented as a 16 bit value, depending on the # record type the base is shifted right ether 4 (record type 02) or 16 (record # type 04) bits. # # The first two records of the ihex binary blob are a segment record and a # data record. Combining the segment value from the first record and the # address value from the second record one can determine the base address # where the blob is supposed to be placed. # # See https://en.wikipedia.org/wiki/Intel_HEX for further details. parse_segment() { local string="$1" if [[ "${string}" =~ ^:020000 && "${#string}" -eq 15 ]]; then local type="${string:7:2}" local value="0x${string:9:4}" local segment case "${type}" in (02) segment=$(( value << 4 )) ;; (04) segment=$(( value << 16 )) ;; (*) error "unknown segment record type ${type}" ;; esac printf "0x%x" "${segment}" else error "unexpected segment record: ${string}" fi } # The second record in the ihex binary blob is mapped to the lowest 16 bit # address in the segment. parse_data() { local string="$1" if [[ "${string}" =~ ^:10 && "${#string}" -eq 43 ]]; then echo "0x${string:3:4}" else error "unexpected data record: ${string}" fi } # Given an ihex binary blob determine its base address as a sum of the segment # address and the offset of the first record into the segment. get_hex_base() { local hexf="$1" local strings local segment local base_offset # Some ihex blobs include , drop to allow for fixed size check. mapfile -t strings < <(head -2 "${hexf}" | sed 's/\x0d//') if [[ ${#strings[@]} != 2 ]]; then error "input file ${hexf} too short" return fi segment="$(parse_segment "${strings[0]}")" base_offset="$(parse_data "${strings[1]}")" if [[ -n "${segment}" && -n "${base_offset}" ]]; then printf "%d\n" $(( segment + base_offset )) else error "${hexf} does not seem to be a valid ihex module." fi } # Paste a binary blob into a larger binary file at a given offset. paste_bin() { local file="${1}" local blob="${2}" local image_base="${3}" local hex_base="${4}" local file_size local blob_size local offset file_size="$(stat -c '%s' "${file}")" blob_size="$(stat -c '%s' "${blob}")" offset="$(( hex_base - image_base ))" if [[ $(( blob_size + offset )) -ge ${file_size} ]];then die \ "Can't fit ${blob_size} bytes at offset ${offset} into ${file_size} bytes" fi dd if="${blob}" of="${file}" seek="${offset}" bs=1 conv=notrunc } # 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 " 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 " 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 " 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 9 ]]; then die "Usage: sign_rw " \ " " fi local key_file="$1" local manifest_file="$2" local fuses_file="$3" local rma_key_dir="$4" local rws=( "$5" "$6" ) local result_file="$7" local generation="$8" local image_base="$9" local base_name local rma_key_base="" local signer_command_params local temp_dir temp_dir="$(make_temp_dir)" 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}" "${rws[@]}")" else warn "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" ) base_name="cr50" ;; (d) # Indicate D1 signing. signer_command_params+=( "--dauntless" "--ihex" ) base_name="ti50" ;; (*) die "Unknown generation value \"${generation}\"" ;; esac signer_command_params+=(--json "${manifest_file}") if [[ "${FLAGS_override_keyid}" == "${FLAGS_TRUE}" ]]; then signer_command_params+=(--override-keyid) fi for rw in "${rws[@]}"; do local hex_signed="${temp_dir}/hex_signed" local bin_signed="${temp_dir}/bin_signed" local hex_base # Make sure output files are not owned by root. touch "${bin_signed}" "${hex_signed}" if ! gsc-codesigner "${signer_command_params[@]}" \ -i "${rw}" -o "${hex_signed}"; then die "gsc-codesigner ${signer_command_params[*]}" \ "-i ${rw} -o ${hex_signed} failed" fi if ! objcopy -I ihex "${hex_signed}" -O binary "${bin_signed}"; then die "Failed to convert ${rw} from hex to bin" fi if [[ -n "${rma_key_base}" ]]; then if find_blob_in_blob "${bin_signed}" "${rma_key_base}.test"; then die "test RMA key in the signed image!" fi if ! find_blob_in_blob "${bin_signed}" "${rma_key_base}.prod"; then die "prod RMA key not in the signed image!" fi fi hex_base="$(get_hex_base "${hex_signed}")" paste_bin "${result_file}" "${bin_signed}" "${image_base}" "${hex_base}" done if strings "${rw}" | grep -q "DBG/${base_name}"; then die "Will not sign debug image with prod keys" fi } # 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 " 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 10 ]]; then die "Usage: sign_gsc_firmware " \ " " fi local key_file="$1" local manifest_file="$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="${10}" local temp_dir temp_dir="$(make_temp_dir)" case "${generation}" in (h) # H1 flash size, image size must match. IMAGE_SIZE="$(( 512 * 1024 ))" IMAGE_BASE="0x40000" ;; (d) # D2 flash size, image size must match. IMAGE_SIZE="$(( 1024 * 1024 ))" IMAGE_BASE="0x80000" ;; 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 if ! sign_rw "${key_file}" "${manifest_file}" "${fuses_file}" \ "${rma_key_dir}" "${rw_a}" "${rw_b}" \ "${output_file}" "${generation}" "${IMAGE_BASE}"; then die "Failed invoking sign_rw for ${rw_a} and ${rw_b}" fi local f for f in "${ro_a_hex}" "${ro_b_hex}"; do local hex_base local bin hex_base="$(get_hex_base "${f}")" bin="${temp_dir}/bin" if [[ -z ${hex_base} ]]; then die "Failed retrieving base address from ${f}" fi if ! objcopy -I ihex "${f}" -O binary "${bin}"; then die "Failed to convert ${f} from hex to bin" fi if [[ "${generation}" == "h" ]]; then verify_ro "${bin}" "${key_file}" fi paste_bin "${output_file}" "${bin}" "${IMAGE_BASE}" "${hex_base}" done info "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 " fi local input="${1%/}" local key_dir="$2" local output="$3" local generation local rw_a local rw_b local manifest_source local manifest_file local key_file local base_name manifest_source="${input}/prod.json" manifest_file="${manifest_source}.updated" # Verify signing manifest. jq . < "${manifest_source}" > "${manifest_file}" || \ die "basic validation of ${manifest_source} failed" # Retrieve chip type from the manifest, if present, otherwise use h1. generation="$(jq ".generation" "${input}/prod.json" | sed 's/"//g')" case "${generation}" in (h|null) generation="h" base_name="cr50" rw_a="${input}/ec.RW.elf" rw_b="${input}/ec.RW_B.elf" ;; (d) base_name="ti50" rw_a="${input}/rw_A.hex" rw_b="${input}/rw_B.hex" ;; (*) die "Unknown generation value \"${generation}\" in signing manifest" ;; esac key_file="${key_dir}/${base_name}.pem" if [[ ! -e "${key_file}" ]]; then die "Missing key file: ${key_file}" fi if [[ -d "${output}" ]]; then output="${output}/${base_name}.bin.prod" fi sign_gsc_firmware \ "${key_file}" \ "${manifest_file}" \ "${input}/fuses.xml" \ "${input}" \ "${input}/prod.ro.A" \ "${input}/prod.ro.B" \ "${rw_a}" \ "${rw_b}" \ "${output}" \ "${generation}" } main() { if [[ $# -ne 3 ]]; then flags_help exit 1 fi local input="${1%/}" local key_dir="$2" local output="$3" local signing_instructions="${input}/signing_instructions.sh" if [[ -f ${signing_instructions} ]]; then . "${signing_instructions}" else die "${signing_instructions} not found" fi if [[ ! -d "${input}" ]]; then die "Missing input directory: ${input}" fi sign_gsc_firmware_dir "${input}" "${key_dir}" "${output}" } main "$@"