| #!/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])" |
| } |
| |
| # 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 <cr><lf>, drop <cr> 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 <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 9 ]]; then |
| die "Usage: sign_rw <key_file> <manifest> <fuses>" \ |
| "<rma_key_dir> <rw_a> <rw_b> <output> <generation> <image_base>" |
| 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 <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 10 ]]; then |
| die "Usage: sign_gsc_firmware <key_file> <manifest> <fuses>" \ |
| "<rma_key_dir> <ro_a> <ro_b> <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 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 <input> <key dir> <output>" |
| 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 "$@" |