xref: /aosp_15_r20/external/vboot_reference/scripts/image_signing/sign_gsc_firmware.sh (revision 8617a60d3594060b7ecbd21bc622a7c14f3cf2bc)
1#!/bin/bash
2# Copyright 2018 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6. "$(dirname "$0")/common.sh"
7
8load_shflags || exit 1
9
10DEFINE_boolean override_keyid "${FLAGS_TRUE}" \
11  "Override keyid from manifest." ""
12
13FLAGS_HELP="Usage: ${PROG} [options] <input_dir> <key_dir> <output_image>
14
15Signs <input_dir> with keys in <key_dir>.
16"
17
18# Parse command line.
19FLAGS "$@" || exit 1
20eval set -- "${FLAGS_ARGV}"
21
22# Abort on error and uninitialized variables.
23set -e
24set -u
25
26PRE_PVT_BID_FLAG=0x10
27MP_BID_FLAG=0x10000
28NIGHTLY_BID_FLAG=0x20000
29
30# Convert unsigned 32 bit value into a signed one.
31to_int32() {
32  local inp="$1"
33  python -c \
34         "import struct; \
35          d=struct.pack('I', $inp); \
36           print (struct.unpack('i', d)[0])"
37}
38
39# Functions allowing to determine the base address of a binary blob in ihex
40# format. Invoked in a subprocess through () to be able to use stdout as the
41# return values.
42
43# In ihex format binary data is represented as a set of records. Each record
44# is a text string of hex values in ASCII. All records start with a header
45# which determines the record contents.
46#
47# The most common record type is the data record, its header includes the 16
48# bit address of where the record data will have to be placed in the physical
49# address space. Naturally 16 bits is not enough as of last thirty years, some
50# special types of record are used to specify the segment base of there the
51# 16 bit address is used as the offset.
52#
53# The segment base is still represented as a 16 bit value, depending on the
54# record type the base is shifted right ether 4 (record type 02) or 16 (record
55# type 04) bits.
56#
57# The first two records of the ihex binary blob are a segment record and a
58# data record. Combining the segment value from the first record and the
59# address value from the second record one can determine the base address
60# where the blob is supposed to be placed.
61#
62# See https://en.wikipedia.org/wiki/Intel_HEX for further details.
63parse_segment() {
64  local string="$1"
65
66  if [[ "${string}" =~ ^:020000 && "${#string}" -eq 15 ]]; then
67    local type="${string:7:2}"
68    local value="0x${string:9:4}"
69    local segment
70
71    case "${type}" in
72      (02)
73        segment=$(( value << 4 ))
74        ;;
75      (04)
76        segment=$(( value << 16 ))
77        ;;
78      (*)
79        error "unknown segment record type ${type}"
80        ;;
81    esac
82    printf "0x%x" "${segment}"
83  else
84    error "unexpected segment record: ${string}"
85  fi
86}
87
88# The second record in the ihex binary blob is mapped to the lowest 16 bit
89# address in the segment.
90parse_data() {
91  local string="$1"
92
93  if [[ "${string}" =~ ^:10 && "${#string}" -eq 43 ]]; then
94    echo "0x${string:3:4}"
95  else
96    error "unexpected data record: ${string}"
97  fi
98}
99
100# Given an ihex binary blob determine its base address as a sum of the segment
101# address and the offset of the first record into the segment.
102get_hex_base() {
103  local hexf="$1"
104  local strings
105  local segment
106  local base_offset
107
108  # Some ihex blobs include <cr><lf>, drop <cr> to allow for fixed size check.
109  mapfile -t strings < <(head -2 "${hexf}" | sed 's/\x0d//')
110
111  if [[ ${#strings[@]} != 2 ]]; then
112    error "input file ${hexf} too short"
113    return
114  fi
115  segment="$(parse_segment "${strings[0]}")"
116  base_offset="$(parse_data "${strings[1]}")"
117
118  if [[ -n "${segment}" && -n "${base_offset}" ]]; then
119    printf "%d\n" $(( segment + base_offset ))
120  else
121    error "${hexf} does not seem to be a valid ihex module."
122  fi
123}
124
125# Paste a binary blob into a larger binary file at a given offset.
126paste_bin() {
127  local file="${1}"
128  local blob="${2}"
129  local image_base="${3}"
130  local hex_base="${4}"
131  local file_size
132  local blob_size
133  local offset
134
135
136  file_size="$(stat -c '%s' "${file}")"
137  blob_size="$(stat -c '%s' "${blob}")"
138  offset="$(( hex_base - image_base ))"
139
140  if [[ $(( blob_size + offset )) -ge ${file_size} ]];then
141    die \
142      "Can't fit ${blob_size} bytes at offset ${offset} into ${file_size} bytes"
143  fi
144  dd if="${blob}" of="${file}" seek="${offset}" bs=1 conv=notrunc
145}
146
147# This function accepts two arguments, the name of the GSC manifest file which
148# needs to be verified and in certain cases altered and the generation of the
149# chip (h or g).
150#
151# The function verifies that the input manifest is a proper json file, and
152# that the manifest conforms to GSC board ID flags conventions for various
153# build images:
154
155# - board IDs for node locked images come from signing instructions, and the
156#   config1 manifest field value must have the 0x80000000 bit set.
157#
158# - when signing pre-pvt binaries (major version number is even) the 0x10
159#   flags bit must be set.
160#
161# - when signing mp images (major version number is odd), the 0x10000 flags
162#   bit must be set (this can be overridden by signing instructions).
163#
164# - when signing nightly images (major version number is 26), the flags are
165#   0x20000
166verify_and_prepare_gsc_manifest() {
167  if [[ $# -ne 2 ]]; then
168    die "Usage: verify_and_prepare_gsc_manifest <manifest .json file> <generation>"
169  fi
170
171  local manifest_json="$1"
172  local generation="$2"
173
174  local bid_flags
175  local config1
176  local epoch
177  local major
178  local minor
179  local values
180
181  mapfile -t values < <(jq '.config1,.epoch,.major,.minor,.board_id_flags' \
182             "${manifest_json}")
183
184  config1="${values[0]}"
185  epoch="${values[1]}"
186  major="${values[2]}"
187  minor="${values[3]}"
188  bid_flags="${values[4]}"
189
190  if [[ ${major} == null ]]; then
191    die "Major version number not found in ${manifest_json}"
192  fi
193
194  if [[ ${bid_flags} == null ]]; then
195    die "bid_flags not found in ${manifest_json}"
196  fi
197
198  case "${INSN_TARGET:-}" in
199
200    (Nightly)
201      # At this point Nightly builds must have the major version 26 and
202      # 0x20000 board id flags, so it can't run on released devices.
203      if (( (major == 26 ) && (bid_flags == NIGHTLY_BID_FLAG) )); then
204        # The Nightly target is only valid for ti50 devices.
205        if [[ "${generation}" == "d" ]] ; then
206          return 0
207        fi
208      fi
209      ;;
210
211    (NodeLocked)
212      if [[ -z ${INSN_DEVICE_ID:-} ]]; then
213        die "Node locked target without Device ID value"
214      fi
215
216      local sub
217      local devid0
218      local devid1
219
220      devid0="$(to_int32 "0x${INSN_DEVICE_ID/-*}")"
221      devid1="$(to_int32 "0x${INSN_DEVICE_ID/*-}")"
222      cf1="$(to_int32 $(( 0x80000000 + config1 )))"
223      sub="$(printf "   \"DEV_ID0\": %s,\\\n  \"DEV_ID1\": %s," \
224                              "${devid0}" "${devid1}")"
225
226      # Manifest fields must be modified as follows:
227      #
228      # - board_id related fields removed
229      # - 'config1' field bit 0x80000000 set
230      # - least significant bit of the 'tag' field originally set to all zeros
231      #   changed from zero to one
232      # - DEV_ID values spliced in into the 'fuses' section
233      sed -i  "/board_id/d;\
234        s/\"config1\":.*/\"config1\": ${cf1},/;\
235        s/\(tag.*0\+\)0/\11/;\
236        /\"fuses\":/ a\
237            $sub"  "${manifest_json}" || die "Failed to edit the manifest"
238      return 0
239      ;;
240
241    (PrePVT)
242      # All we care about for pre pvt images is that major version number is
243      # even and the 0x10 Board ID flag is set.
244      if (( !(major & 1 ) && (bid_flags & PRE_PVT_BID_FLAG) )); then
245        return 0
246      fi
247      ;;
248
249    (ReleaseCandidate|GeneralRelease)
250      if (( (bid_flags & MP_BID_FLAG) && (major & 1) )); then
251        if [[ ${INSN_TARGET} == GeneralRelease ]]; then
252          # Strip Board ID information for approved for release MP images.
253          sed -i  "/board_id/d" "${manifest_json}"
254        fi
255        return 0
256      fi
257      ;;
258
259    (*)
260      die "Unsupported target '${INSN_TARGET:-}': " \
261	  "generation = '${generation}' major = '${major}'"
262  esac
263
264  die "Inconsistent manifest ${manifest_json}: generation = '${generation}'," \
265      "major = '${major}',board_id_flags = '${bid_flags}' " \
266      "target = '${INSN_TARGET}'"
267}
268
269# This function accepts two arguments, names of two binary files.
270#
271# It searches the first passed-in file for the first 8 bytes of the second
272# passed in file. The od utility is used to generate full hex dump of the
273# first file (16 bytes per line) and the first 8 bytes of the second file.
274# grep is used to check if the pattern is present in the full dump.
275find_blob_in_blob() {
276  if [[ $# -ne 2 ]]; then
277    die "Usage: find_blob_in_blob <haystack> <needle>"
278  fi
279
280  local main_blob="$1"
281  local pattern_blob="$2"
282  local pattern
283  # Show without offsets, single byte hex, no compression of zero runs.
284  local od_options=("-An" "-tx1" "-v")
285
286  # Get the first 8 bytes of the pattern blob.
287  pattern="$(od "${od_options[@]}" -N8 "${pattern_blob}")"
288
289  # Eliminate all newlines to be able to search the entire body as one unit.
290  if od "${od_options[@]}" "${main_blob}" | \
291     tr -d '\n' |
292     grep -q -F "${pattern}"; then
293    return 0
294  fi
295
296  return 1
297}
298
299# This function accepts two arguments, names of the two ELF files.
300#
301# The files are searched for test RMA public key patterns - x25519 or p256,
302# both files are supposed to have pattern of one of these keys and not the
303# other. If this holds true the function prints the public key base name. If
304# not both files include the same key, or include more than one key, the
305# function reports failure and exits the script.
306determine_rma_key_base() {
307  if [[ $# -ne 3 ]]; then
308    die "Usage: determine_rma_key_base <rma_key_dir> <rw_a> <rw_b>"
309  fi
310
311  local rma_key_dir="$1"
312  local elfs=( "$2" "$3" )
313  local base_name="${rma_key_dir}/rma_key_blob"
314  local curve
315  local curves=( "x25519" "p256" )
316  local elf
317  local key_file
318  local mask=1
319  local result=0
320  local rma_key_base
321
322  for curve in "${curves[@]}"; do
323    key_file="${base_name}.${curve}.test"
324    for elf in "${elfs[@]}"; do
325      if find_blob_in_blob "${elf}" "${key_file}"; then
326        : $(( result |= mask ))
327      fi
328      : $(( mask <<= 1 ))
329    done
330  done
331
332  case "${result}" in
333    (3)  curve="x25519";;
334    (12) curve="p256";;
335    (*)  die "could not determine key type in the ELF files";;
336  esac
337
338  echo "${base_name}.${curve}"
339}
340
341# Sign GSC RW firmware ELF images into a combined GSC firmware image
342# using the provided production keys and manifests.
343sign_rw() {
344  if [[ $# -ne 9 ]]; then
345    die "Usage: sign_rw <key_file> <manifest> <fuses>" \
346        "<rma_key_dir> <rw_a> <rw_b> <output> <generation> <image_base>"
347  fi
348
349  local key_file="$1"
350  local manifest_file="$2"
351  local fuses_file="$3"
352  local rma_key_dir="$4"
353  local rws=( "$5" "$6" )
354  local result_file="$7"
355  local generation="$8"
356  local image_base="$9"
357  local base_name
358  local rma_key_base=""
359  local signer_command_params
360  local temp_dir
361  local prohibited_blobs=()
362
363  temp_dir="$(make_temp_dir)"
364  signer_command_params=(-x "${fuses_file}" --key "${key_file}")
365
366  case "${generation}"  in
367    (h)
368      # H1 image might require some tweaking.
369      # If signing a chip factory image (version 0.0.22) do not try figuring
370      # out the RMA keys.
371      local gsc_version
372
373      gsc_version="$(jq '.epoch * 10000 + .major * 100 + .minor' \
374        "${manifest_file}")"
375
376      if [[ "${gsc_version}" != "22" ]]; then
377        rma_key_base="$(determine_rma_key_base "${rma_key_dir}" "${rws[@]}")"
378      else
379        warn "Ignoring RMA keys for factory branch ${gsc_version}"
380      fi
381
382      # Swap test public RMA server key with the prod version.
383      if [[ -n "${rma_key_base}" ]]; then
384        signer_command_params+=(
385          --swap "${rma_key_base}.test,${rma_key_base}.prod"
386        )
387      fi
388
389      # Indicate H1 signing.
390      signer_command_params+=( "--b" )
391      base_name="cr50"
392      ;;
393    (d)
394      # Indicate D1 signing.
395      signer_command_params+=( "--dauntless" "--ihex" )
396      base_name="ti50"
397      # Key and hashes used in dev, must not leak into prod signed images.
398      prohibited_blobs=(
399        "${rma_key_dir}/rma_test_pub_key.bin"
400        "${rma_key_dir}/arv_2k_test_key_hash.bin"
401        "${rma_key_dir}/arv_4k_test_key_hash.bin"
402      )
403      ;;
404    (*)
405      die "Unknown generation value \"${generation}\""
406      ;;
407  esac
408
409  signer_command_params+=(--json "${manifest_file}")
410
411
412  if [[ "${FLAGS_override_keyid}" == "${FLAGS_TRUE}" ]]; then
413    signer_command_params+=(--override-keyid)
414  fi
415
416  for rw in "${rws[@]}"; do
417    local hex_signed="${temp_dir}/hex_signed"
418    local bin_signed="${temp_dir}/bin_signed"
419    local hex_base
420    local blob
421
422    # Make sure output files are not owned by root.
423    touch "${bin_signed}" "${hex_signed}"
424    if ! gsc-codesigner "${signer_command_params[@]}" \
425        -i "${rw}" -o "${hex_signed}"; then
426      die "gsc-codesigner ${signer_command_params[*]}" \
427        "-i ${rw} -o ${hex_signed} failed"
428    fi
429
430    if ! objcopy -I ihex "${hex_signed}" -O binary "${bin_signed}"; then
431      die "Failed to convert ${rw} from hex to bin"
432    fi
433
434    if [[ -n "${rma_key_base}" ]]; then
435      if find_blob_in_blob  "${bin_signed}" "${rma_key_base}.test"; then
436        die "test RMA key in the signed image!"
437      fi
438
439      if ! find_blob_in_blob "${bin_signed}" "${rma_key_base}.prod"; then
440        die "prod RMA key not in the signed image!"
441      fi
442    fi
443
444    for blob in "${prohibited_blobs[@]}"; do
445      if [[ ! -f ${blob} ]]; then
446        die "${blob} not found in the GSC tarball"
447      fi
448      if find_blob_in_blob "${bin_signed}" "${blob}"; then
449        die "${blob} found in signed image"
450      fi
451    done
452
453    hex_base="$(get_hex_base "${hex_signed}")"
454    paste_bin "${result_file}" "${bin_signed}" "${image_base}" "${hex_base}"
455  done
456
457  if strings "${rw}" | grep -q "DBG/${base_name}"; then
458    die "Will not sign debug image with prod keys"
459  fi
460
461}
462
463# A very crude RO verification function. The key signature found at a fixed
464# offset into the RO blob must match the RO type. Prod keys have bit D2 set to
465# one, dev keys have this bit set to zero.
466#
467# The check is bypassed if the key file directory name includes string 'test'.
468verify_ro() {
469  if [[ $# -ne 2 ]]; then
470    die "Usage: verify_ro <ro_bin> <key_file>"
471  fi
472
473  local ro_bin="$1"
474  local key_file="$2"
475  local key_byte
476  local key_path
477
478  if [[ ! -f "${ro_bin}" ]]; then
479    die "${ro_bin} not a file!"
480  fi
481
482  key_path="$(dirname "${key_file}")"
483  if [[ ${key_path##*/} == *"test"* ]]; then
484    info "Test run, ignoring key type verification"
485    return 0
486  fi
487
488  # Key signature's lowest byte is byte #5 in the line at offset 0001a0.
489  key_byte="$(od -Ax -t x1 -v "${ro_bin}" | awk '/0001a0/ {print $6}')"
490  case "${key_byte}" in
491    (?[4567cdef])
492      return 0
493      ;;
494  esac
495
496  die "RO key (${key_byte}) in ${ro_bin} does not match type prod"
497}
498
499# This function prepares a full GSC image, consisting of two ROs and two RWs
500# placed at their respective offsets into the resulting blob. It invokes the
501# bs (binary signer) script to actually convert ELF versions of RWs into
502# binaries and sign them.
503#
504# The signed image is placed in the directory named as concatenation of RO and
505# RW version numbers and board ID fields, if set to non-default. The ebuild
506# downloading the tarball from the BCS expects the image to be in that
507# directory.
508sign_gsc_firmware() {
509  if [[ $# -ne 10 ]]; then
510    die "Usage: sign_gsc_firmware <key_file> <manifest> <fuses>" \
511        "<rma_key_dir> <ro_a> <ro_b> <rw_a> <rw_b> <output> <generation>"
512  fi
513
514  local key_file="$1"
515  local manifest_file="$2"
516  local fuses_file="$3"
517  local rma_key_dir="$4"
518  local ro_a_hex="$5"
519  local ro_b_hex="$6"
520  local rw_a="$7"
521  local rw_b="$8"
522  local output_file="$9"
523  local generation="${10}"
524  local temp_dir
525  local chip_name
526
527  temp_dir="$(make_temp_dir)"
528
529  case "${generation}"  in
530    (h)
531      # H1 flash size, image size must match.
532      IMAGE_SIZE="$(( 512 * 1024 ))"
533      IMAGE_BASE="0x40000"
534      chip_name="cr50"
535      ;;
536    (d)
537      # D2 flash size, image size must match.
538      IMAGE_SIZE="$(( 1024 * 1024 ))"
539      IMAGE_BASE="0x80000"
540      chip_name="ti50"
541      ;;
542  esac
543
544  verify_and_prepare_gsc_manifest "${manifest_file}" "${generation}"
545
546  dd if=/dev/zero bs="${IMAGE_SIZE}" count=1 status=none |
547    tr '\000' '\377' > "${output_file}"
548  if [[ "$(stat -c '%s' "${output_file}")" != "${IMAGE_SIZE}" ]]; then
549    die "Failed creating ${output_file}"
550  fi
551
552  if ! sign_rw "${key_file}" "${manifest_file}" "${fuses_file}" \
553       "${rma_key_dir}" "${rw_a}" "${rw_b}" \
554       "${output_file}" "${generation}" "${IMAGE_BASE}"; then
555    die "Failed invoking sign_rw for ${rw_a} and ${rw_b}"
556  fi
557
558  local f
559  for f in "${ro_a_hex}" "${ro_b_hex}"; do
560    local hex_base
561    local bin
562
563    hex_base="$(get_hex_base "${f}")"
564    bin="${temp_dir}/bin"
565
566    if [[ -z ${hex_base} ]]; then
567      die "Failed retrieving base address from ${f}"
568    fi
569
570    if ! objcopy -I ihex "${f}" -O binary "${bin}"; then
571      die "Failed to convert ${f} from hex to bin"
572    fi
573    if [[ "${generation}" == "h" ]]; then
574      verify_ro "${bin}" "${key_file}"
575    fi
576
577    paste_bin "${output_file}" "${bin}" "${IMAGE_BASE}" "${hex_base}"
578  done
579
580  # Tell the signer how to rename the @CHIP@ portion of the output.
581  echo "${chip_name}" > "${output_file}.rename"
582
583  info "Image successfully signed to ${output_file}"
584}
585
586# Sign the directory holding GSC firmware.
587sign_gsc_firmware_dir() {
588  if [[ $# -ne 3 ]]; then
589    die "Usage: sign_gsc_firmware_dir <input> <key dir> <output>"
590  fi
591
592  local input="${1%/}"
593  local key_dir="$2"
594  local output="$3"
595  local generation
596  local rw_a
597  local rw_b
598  local manifest_source
599  local manifest_file
600  local key_file
601  local base_name
602
603  manifest_source="${input}/prod.json"
604  manifest_file="${manifest_source}.updated"
605
606  # Verify signing manifest.
607  jq . < "${manifest_source}" > "${manifest_file}" || \
608    die "basic validation of ${manifest_source} failed"
609
610
611  # Retrieve chip type from the manifest, if present, otherwise use h1.
612  generation="$(jq ".generation" "${input}/prod.json" | sed 's/"//g')"
613  case "${generation}"  in
614    (h|null)
615      generation="h"
616      base_name="cr50"
617      rw_a="${input}/ec.RW.elf"
618      rw_b="${input}/ec.RW_B.elf"
619      ;;
620    (d)
621      base_name="ti50"
622      rw_a="${input}/rw_A.hex"
623      rw_b="${input}/rw_B.hex"
624      ;;
625    (*)
626      die "Unknown generation value \"${generation}\" in signing manifest"
627      ;;
628  esac
629
630  key_file="${key_dir}/${base_name}.pem"
631  if [[ ! -e "${key_file}" ]]; then
632    die "Missing key file: ${key_file}"
633  fi
634
635  if [[ -d "${output}" ]]; then
636    output="${output}/${base_name}.bin.prod"
637  fi
638
639  sign_gsc_firmware \
640          "${key_file}" \
641          "${manifest_file}" \
642          "${input}/fuses.xml" \
643          "${input}" \
644          "${input}/prod.ro.A" \
645          "${input}/prod.ro.B" \
646          "${rw_a}" \
647          "${rw_b}" \
648          "${output}" \
649          "${generation}"
650}
651
652main() {
653  if [[ $# -ne 3 ]]; then
654    flags_help
655    exit 1
656  fi
657
658  local input="${1%/}"
659  local key_dir="$2"
660  local output="$3"
661
662  local signing_instructions="${input}/signing_instructions.sh"
663
664  if [[ -f ${signing_instructions} ]]; then
665    . "${signing_instructions}"
666  else
667    die "${signing_instructions} not found"
668  fi
669
670  if [[ ! -d "${input}" ]]; then
671    die "Missing input directory: ${input}"
672  fi
673
674  sign_gsc_firmware_dir "${input}" "${key_dir}" "${output}"
675}
676main "$@"
677