xref: /aosp_15_r20/external/vboot_reference/scripts/image_signing/common_minimal.sh (revision 8617a60d3594060b7ecbd21bc622a7c14f3cf2bc)
1#!/bin/sh
2#
3# Copyright 2011 The ChromiumOS Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6#
7# Note: This file must be written in dash compatible way as scripts that use
8# this may run in the Chrome OS client enviornment.
9
10# shellcheck disable=SC2039,SC2059,SC2155
11
12# Determine script directory
13SCRIPT_DIR=$(dirname "$0")
14PROG=$(basename "$0")
15: "${GPT:=cgpt}"
16: "${FUTILITY:=futility}"
17
18# The tag when the rootfs is changed.
19TAG_NEEDS_TO_BE_SIGNED="/root/.need_to_be_signed"
20
21# List of Temporary files and mount points.
22TEMP_FILE_LIST=$(mktemp)
23TEMP_DIR_LIST=$(mktemp)
24
25# Finds and loads the 'shflags' library, or return as failed.
26load_shflags() {
27  # Load shflags
28  if [ -f /usr/share/misc/shflags ]; then
29    # shellcheck disable=SC1090,SC1091
30    .  /usr/share/misc/shflags
31  elif [ -f "${SCRIPT_DIR}/lib/shflags/shflags" ]; then
32    # shellcheck disable=SC1090
33    . "${SCRIPT_DIR}/lib/shflags/shflags"
34  else
35    echo "ERROR: Cannot find the required shflags library."
36    return 1
37  fi
38
39  # Add debug option for debug output below
40  DEFINE_boolean debug $FLAGS_FALSE "Provide debug messages" "d"
41}
42
43# Functions for debug output
44# ----------------------------------------------------------------------------
45
46# These helpers are for runtime systems.  For scripts using common.sh,
47# they'll get better definitions that will clobber these ones.
48info() {
49  echo "${PROG}: INFO: $*" >&2
50}
51
52warn() {
53  echo "${PROG}: WARN: $*" >&2
54}
55
56error() {
57  echo "${PROG}: ERROR: $*" >&2
58}
59
60# Reports error message and exit(1)
61# Args: error message
62die() {
63  error "$@"
64  exit 1
65}
66
67# Returns true if we're running in debug mode.
68#
69# Note that if you don't set up shflags by calling load_shflags(), you
70# must set $FLAGS_debug and $FLAGS_TRUE yourself.  The default
71# behavior is that debug will be off if you define neither $FLAGS_TRUE
72# nor $FLAGS_debug.
73is_debug_mode() {
74  [ "${FLAGS_debug:-not$FLAGS_TRUE}" = "$FLAGS_TRUE" ]
75}
76
77# Prints messages (in parameters) in debug mode
78# Args: debug message
79debug_msg() {
80  if is_debug_mode; then
81    echo "DEBUG: $*" 1>&2
82  fi
83}
84
85# Functions for temporary files and directories
86# ----------------------------------------------------------------------------
87
88# Create a new temporary file and return its name.
89# File is automatically cleaned when cleanup_temps_and_mounts() is called.
90make_temp_file() {
91  local tempfile="$(mktemp)"
92  echo "$tempfile" >> "$TEMP_FILE_LIST"
93  echo "$tempfile"
94}
95
96# Create a new temporary directory and return its name.
97# Directory is automatically deleted and any filesystem mounted on it unmounted
98# when cleanup_temps_and_mounts() is called.
99make_temp_dir() {
100  local tempdir=$(mktemp -d)
101  echo "$tempdir" >> "$TEMP_DIR_LIST"
102  echo "$tempdir"
103}
104
105cleanup_temps_and_mounts() {
106  while read -r line; do
107    rm -f "$line"
108  done < "$TEMP_FILE_LIST"
109
110  set +e  # umount may fail for unmounted directories
111  while read -r line; do
112    if [ -n "$line" ]; then
113      if has_needs_to_be_resigned_tag "$line"; then
114        echo "Warning: image may be modified. Please resign image."
115      fi
116      sudo umount "$line" 2>/dev/null
117      rm -rf "$line"
118    fi
119  done < "$TEMP_DIR_LIST"
120  set -e
121  rm -rf "$TEMP_DIR_LIST" "$TEMP_FILE_LIST"
122}
123
124trap "cleanup_temps_and_mounts" EXIT
125
126# Functions for partition management
127# ----------------------------------------------------------------------------
128
129# Construct a partition device name from a whole disk block device and a
130# partition number.
131# This works for [/dev/sda, 3] (-> /dev/sda3) as well as [/dev/mmcblk0, 2]
132# (-> /dev/mmcblk0p2).
133make_partition_dev() {
134  local block="$1"
135  local num="$2"
136  # If the disk block device ends with a number, we add a 'p' before the
137  # partition number.
138  if [ "${block%[0-9]}" = "${block}" ]; then
139    echo "${block}${num}"
140  else
141    echo "${block}p${num}"
142  fi
143}
144
145# Find the block size of a device in bytes
146# Args: DEVICE (e.g. /dev/sda)
147# Return: block size in bytes
148blocksize() {
149  local output=''
150  local path="$1"
151  if [ -b "${path}" ]; then
152    local dev="${path##*/}"
153    local sys="/sys/block/${dev}/queue/logical_block_size"
154    output="$(cat "${sys}" 2>/dev/null)"
155  fi
156  echo "${output:-512}"
157}
158
159# Read GPT table to find the starting location of a specific partition.
160# Args: DEVICE PARTNUM
161# Returns: offset (in sectors) of partition PARTNUM
162partoffset() {
163  sudo "$GPT" show -b -i "$2" "$1"
164}
165
166# Read GPT table to find the size of a specific partition.
167# Args: DEVICE PARTNUM
168# Returns: size (in sectors) of partition PARTNUM
169partsize() {
170  sudo "$GPT" show -s -i "$2" "$1"
171}
172
173# Tags a file system as "needs to be resigned".
174# Args: MOUNTDIRECTORY
175tag_as_needs_to_be_resigned() {
176  local mount_dir="$1"
177  sudo touch "$mount_dir/$TAG_NEEDS_TO_BE_SIGNED"
178}
179
180# Determines if the target file system has the tag for resign
181# Args: MOUNTDIRECTORY
182# Returns: true if the tag is there otherwise false
183has_needs_to_be_resigned_tag() {
184  local mount_dir="$1"
185  [ -f "$mount_dir/$TAG_NEEDS_TO_BE_SIGNED" ]
186}
187
188# Determines if the target file system is a Chrome OS root fs
189# Args: MOUNTDIRECTORY
190# Returns: true if MOUNTDIRECTORY looks like root fs, otherwise false
191is_rootfs_partition() {
192  local mount_dir="$1"
193  [ -f "$mount_dir/$(dirname "$TAG_NEEDS_TO_BE_SIGNED")" ]
194}
195
196# If the kernel is buggy and is unable to loop+mount quickly,
197# retry the operation a few times.
198# Args: IMAGE PARTNUM MOUNTDIRECTORY [ro]
199#
200# This function does not check whether the partition is allowed to be mounted as
201# RW.  Callers must ensure the partition can be mounted as RW before calling
202# this function without |ro| argument.
203_mount_image_partition_retry() {
204  local image=$1
205  local partnum=$2
206  local mount_dir=$3
207  local ro=$4
208  local bs="$(blocksize "${image}")"
209  local offset=$(( $(partoffset "${image}" "${partnum}") * bs ))
210  local out try
211
212  # shellcheck disable=SC2086
213  set -- sudo LC_ALL=C mount -o loop,offset=${offset},${ro} \
214    "${image}" "${mount_dir}"
215  try=1
216  while [ ${try} -le 5 ]; do
217    if ! out=$("$@" 2>&1); then
218      if [ "${out}" = "mount: you must specify the filesystem type" ]; then
219        printf 'WARNING: mounting %s at %s failed (try %i); retrying\n' \
220               "${image}" "${mount_dir}" "${try}"
221        # Try to "quiet" the disks and sleep a little to reduce contention.
222        sync
223        sleep $(( try * 5 ))
224      else
225        # Failed for a different reason; abort!
226        break
227      fi
228    else
229      # It worked!
230      return 0
231    fi
232    : $(( try += 1 ))
233  done
234  echo "ERROR: mounting ${image} at ${mount_dir} failed:"
235  echo "${out}"
236  # We don't preserve the exact exit code of `mount`, but since
237  # no one in this code base seems to check it, it's a moot point.
238  return 1
239}
240
241# If called without 'ro', make sure the partition is allowed to be mounted as
242# 'rw' before actually mounting it.
243# Args: IMAGE PARTNUM MOUNTDIRECTORY [ro]
244_mount_image_partition() {
245  local image=$1
246  local partnum=$2
247  local mount_dir=$3
248  local ro=$4
249  local bs="$(blocksize "${image}")"
250  local offset=$(( $(partoffset "${image}" "${partnum}") * bs ))
251
252  if [ "$ro" != "ro" ]; then
253    # Forcibly call enable_rw_mount.  It should fail on unsupported
254    # filesystems and be idempotent on ext*.
255    enable_rw_mount "${image}" ${offset} 2> /dev/null
256  fi
257
258  _mount_image_partition_retry "$@"
259}
260
261# If called without 'ro', make sure the partition is allowed to be mounted as
262# 'rw' before actually mounting it.
263# Args: LOOPDEV PARTNUM MOUNTDIRECTORY [ro]
264_mount_loop_image_partition() {
265  local loopdev=$1
266  local partnum=$2
267  local mount_dir=$3
268  local ro=$4
269  local loop_rootfs="${loopdev}p${partnum}"
270
271  if [ "$ro" != "ro" ]; then
272    # Forcibly call enable_rw_mount.  It should fail on unsupported
273    # filesystems and be idempotent on ext*.
274    enable_rw_mount "${loop_rootfs}" 2>/dev/null
275  fi
276
277  sudo mount -o "${ro}" "${loop_rootfs}" "${mount_dir}"
278}
279
280# Mount a partition read-only from an image into a local directory
281# Args: IMAGE PARTNUM MOUNTDIRECTORY
282mount_image_partition_ro() {
283  _mount_image_partition "$@" "ro"
284}
285
286# Mount a partition read-only from an image into a local directory
287# Args: LOOPDEV PARTNUM MOUNTDIRECTORY
288mount_loop_image_partition_ro() {
289  _mount_loop_image_partition "$@" "ro"
290}
291
292# Mount a partition from an image into a local directory
293# Args: IMAGE PARTNUM MOUNTDIRECTORY
294mount_image_partition() {
295  local mount_dir=$3
296  _mount_image_partition "$@"
297  if is_rootfs_partition "${mount_dir}"; then
298    tag_as_needs_to_be_resigned "${mount_dir}"
299  fi
300}
301
302# Mount a partition from an image into a local directory
303# Args: LOOPDEV PARTNUM MOUNTDIRECTORY
304mount_loop_image_partition() {
305  local mount_dir=$3
306  _mount_loop_image_partition "$@"
307  if is_rootfs_partition "${mount_dir}"; then
308    tag_as_needs_to_be_resigned "${mount_dir}"
309  fi
310}
311
312# Mount the image's ESP (EFI System Partition) on a newly created temporary
313# directory.
314# Prints out the newly created temporary directory path if succeeded.
315# If the image doens't have an ESP partition, returns 0 without print anything.
316# Args: LOOPDEV
317# Returns: 0 if succeeded, 1 otherwise.
318mount_image_esp() {
319  local loopdev="$1"
320  local ESP_PARTNUM=12
321  local loop_esp="${loopdev}p${ESP_PARTNUM}"
322
323  local esp_offset=$(( $(partoffset "${loopdev}" "${ESP_PARTNUM}") ))
324  # Check if the image has an ESP partition.
325  if [[ "${esp_offset}" == "0" ]]; then
326    return 0
327  fi
328
329  local esp_dir="$(make_temp_dir)"
330  if ! sudo mount -o "${ro}" "${loop_esp}" "${esp_dir}"; then
331    return 1
332  fi
333
334  echo "${esp_dir}"
335  return 0
336}
337
338# Extract a partition to a file
339# Args: IMAGE PARTNUM OUTPUTFILE
340extract_image_partition() {
341  local image=$1
342  local partnum=$2
343  local output_file=$3
344  local offset=$(partoffset "$image" "$partnum")
345  local size=$(partsize "$image" "$partnum")
346
347  # shellcheck disable=SC2086
348  dd if="$image" of="$output_file" bs=512 skip=$offset count=$size \
349    conv=notrunc 2>/dev/null
350}
351
352# Replace a partition in an image from file
353# Args: IMAGE PARTNUM INPUTFILE
354replace_image_partition() {
355  local image=$1
356  local partnum=$2
357  local input_file=$3
358  local offset=$(partoffset "$image" "$partnum")
359  local size=$(partsize "$image" "$partnum")
360
361  # shellcheck disable=SC2086
362  dd if="$input_file" of="$image" bs=512 seek=$offset count=$size \
363    conv=notrunc 2>/dev/null
364}
365
366# For details, see crosutils.git/common.sh
367enable_rw_mount() {
368  local rootfs="$1"
369  local offset="${2-0}"
370
371  # Make sure we're checking an ext2 image
372  # shellcheck disable=SC2086
373  if ! is_ext2 "$rootfs" $offset; then
374    echo "enable_rw_mount called on non-ext2 filesystem: $rootfs $offset" 1>&2
375    return 1
376  fi
377
378  local ro_compat_offset=$((0x464 + 3))  # Set 'highest' byte
379  # Dash can't do echo -ne, but it can do printf "\NNN"
380  # We could use /dev/zero here, but this matches what would be
381  # needed for disable_rw_mount (printf '\377').
382  printf '\000' |
383    sudo dd of="$rootfs" seek=$((offset + ro_compat_offset)) \
384            conv=notrunc count=1 bs=1 2>/dev/null
385}
386
387# For details, see crosutils.git/common.sh
388is_ext2() {
389  local rootfs="$1"
390  local offset="${2-0}"
391
392  # Make sure we're checking an ext2 image
393  local sb_magic_offset=$((0x438))
394  local sb_value=$(sudo dd if="$rootfs" skip=$((offset + sb_magic_offset)) \
395                   count=2 bs=1 2>/dev/null)
396  local expected_sb_value=$(printf '\123\357')
397  if [ "$sb_value" = "$expected_sb_value" ]; then
398    return 0
399  fi
400  return 1
401}
402
403disable_rw_mount() {
404  local rootfs="$1"
405  local offset="${2-0}"
406
407  # Make sure we're checking an ext2 image
408  # shellcheck disable=SC2086
409  if ! is_ext2 "$rootfs" $offset; then
410    echo "disable_rw_mount called on non-ext2 filesystem: $rootfs $offset" 1>&2
411    return 1
412  fi
413
414  local ro_compat_offset=$((0x464 + 3))  # Set 'highest' byte
415  # Dash can't do echo -ne, but it can do printf "\NNN"
416  # We could use /dev/zero here, but this matches what would be
417  # needed for disable_rw_mount (printf '\377').
418  printf '\377' |
419    sudo dd of="$rootfs" seek=$((offset + ro_compat_offset)) \
420            conv=notrunc count=1 bs=1 2>/dev/null
421}
422
423rw_mount_disabled() {
424  local rootfs="$1"
425  local offset="${2-0}"
426
427  # Make sure we're checking an ext2 image
428  # shellcheck disable=SC2086
429  if ! is_ext2 "$rootfs" $offset; then
430    return 2
431  fi
432
433  local ro_compat_offset=$((0x464 + 3))  # Set 'highest' byte
434  local ro_value=$(sudo dd if="$rootfs" skip=$((offset + ro_compat_offset)) \
435                   count=1 bs=1 2>/dev/null)
436  local expected_ro_value=$(printf '\377')
437  if [ "$ro_value" = "$expected_ro_value" ]; then
438    return 0
439  fi
440  return 1
441}
442
443# Functions for CBFS management
444# ----------------------------------------------------------------------------
445
446# Get the compression algorithm used for the given CBFS file.
447# Args: INPUT_CBFS_IMAGE CBFS_FILE_NAME
448get_cbfs_compression() {
449  cbfstool "$1" print -r "FW_MAIN_A" | awk -vname="$2" '$1 == name {print $5}'
450}
451
452# Store a file in CBFS.
453# Args: INPUT_CBFS_IMAGE INPUT_FILE CBFS_FILE_NAME
454store_file_in_cbfs() {
455  local image="$1"
456  local file="$2"
457  local name="$3"
458  local compression=$(get_cbfs_compression "$1" "${name}")
459
460  # Don't re-add a file to a section if it's unchanged.  Otherwise this seems
461  # to break signature of existing contents.  https://crbug.com/889716
462  if cbfstool "${image}" extract -r "FW_MAIN_A,FW_MAIN_B" \
463       -f "${file}.orig" -n "${name}"; then
464    if cmp -s "${file}" "${file}.orig"; then
465      rm -f "${file}.orig"
466      return
467    fi
468    rm -f "${file}.orig"
469  fi
470
471  cbfstool "${image}" remove -r "FW_MAIN_A,FW_MAIN_B" -n "${name}" || return
472  # This add can fail if
473  # 1. Size of a signature after compression is larger
474  # 2. CBFS is full
475  # These conditions extremely unlikely become true at the same time.
476  cbfstool "${image}" add -r "FW_MAIN_A,FW_MAIN_B" -t "raw" \
477    -c "${compression}" -f "${file}" -n "${name}" || return
478}
479
480# Misc functions
481# ----------------------------------------------------------------------------
482
483# Parses the version file containing key=value lines
484# Args: key file
485# Returns: value
486get_version() {
487  local key="$1"
488  local file="$2"
489  awk -F= -vkey="${key}" '$1 == key { print $NF }' "${file}"
490}
491
492# Returns true if all files in parameters exist.
493# Args: List of files
494ensure_files_exist() {
495  local filename return_value=0
496  for filename in "$@"; do
497    if [ ! -f "$filename"  ] && [ ! -b "$filename" ]; then
498      echo "ERROR: Cannot find required file: $filename"
499      return_value=1
500    fi
501  done
502
503  return $return_value
504}
505
506# Check if the 'chronos' user already has a password
507# Args: rootfs
508no_chronos_password() {
509  local rootfs=$1
510  # Make sure the chronos user actually exists.
511  if grep -qs '^chronos:' "${rootfs}/etc/passwd"; then
512    sudo grep -q '^chronos:\*:' "${rootfs}/etc/shadow"
513  fi
514}
515
516# Returns true if given ec.bin is signed or false if not.
517is_ec_rw_signed() {
518  ${FUTILITY} dump_fmap "$1" | grep -q KEY_RO
519}
520