Home | History | Annotate | Download | only in scripts
      1 #!/bin/bash
      2 
      3 # Copyright 2015 The Chromium OS Authors. All rights reserved.
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 # Script to generate a Brillo update for use by the update engine.
      8 #
      9 # usage: brillo_update_payload COMMAND [ARGS]
     10 # The following commands are supported:
     11 #  generate    generate an unsigned payload
     12 #  hash        generate a payload or metadata hash
     13 #  sign        generate a signed payload
     14 #  properties  generate a properties file from a payload
     15 #
     16 #  Generate command arguments:
     17 #  --payload             generated unsigned payload output file
     18 #  --source_image        if defined, generate a delta payload from the specified
     19 #                        image to the target_image
     20 #  --target_image        the target image that should be sent to clients
     21 #  --metadata_size_file  if defined, generate a file containing the size of the payload
     22 #                        metadata in bytes to the specified file
     23 #
     24 #  Hash command arguments:
     25 #  --unsigned_payload    the input unsigned payload to generate the hash from
     26 #  --signature_size      signature sizes in bytes in the following format:
     27 #                        "size1:size2[:...]"
     28 #  --payload_hash_file   if defined, generate a payload hash and output to the
     29 #                        specified file
     30 #  --metadata_hash_file  if defined, generate a metadata hash and output to the
     31 #                        specified file
     32 #
     33 #  Sign command arguments:
     34 #  --unsigned_payload        the input unsigned payload to insert the signatures
     35 #  --payload                 the output signed payload
     36 #  --signature_size          signature sizes in bytes in the following format:
     37 #                            "size1:size2[:...]"
     38 #  --payload_signature_file  the payload signature files in the following
     39 #                            format:
     40 #                            "payload_signature1:payload_signature2[:...]"
     41 #  --metadata_signature_file the metadata signature files in the following
     42 #                            format:
     43 #                            "metadata_signature1:metadata_signature2[:...]"
     44 #  --metadata_size_file      if defined, generate a file containing the size of
     45 #                            the signed payload metadata in bytes to the
     46 #                            specified file
     47 #  Note that the number of signature sizes and payload signatures have to match.
     48 #
     49 #  Properties command arguments:
     50 #  --payload                 the input signed or unsigned payload
     51 #  --properties_file         the output path where to write the properties, or
     52 #                            '-' for stdout.
     53 
     54 
     55 # Exit codes:
     56 EX_UNSUPPORTED_DELTA=100
     57 
     58 warn() {
     59   echo "brillo_update_payload: warning: $*" >&2
     60 }
     61 
     62 die() {
     63   echo "brillo_update_payload: error: $*" >&2
     64   exit 1
     65 }
     66 
     67 # Loads shflags. We first look at the default install location; then look for
     68 # crosutils (chroot); finally check our own directory (au-generator zipfile).
     69 load_shflags() {
     70   local my_dir="$(dirname "$(readlink -f "$0")")"
     71   local path
     72   for path in /usr/share/misc {/usr/lib/crosutils,"${my_dir}"}/lib/shflags; do
     73     if [[ -r "${path}/shflags" ]]; then
     74       . "${path}/shflags" || die "Could not load ${path}/shflags."
     75       return
     76     fi
     77   done
     78   die "Could not find shflags."
     79 }
     80 
     81 load_shflags
     82 
     83 HELP_GENERATE="generate: Generate an unsigned update payload."
     84 HELP_HASH="hash: Generate the hashes of the unsigned payload and metadata used \
     85 for signing."
     86 HELP_SIGN="sign: Insert the signatures into the unsigned payload."
     87 HELP_PROPERTIES="properties: Extract payload properties to a file."
     88 
     89 usage() {
     90   echo "Supported commands:"
     91   echo
     92   echo "${HELP_GENERATE}"
     93   echo "${HELP_HASH}"
     94   echo "${HELP_SIGN}"
     95   echo "${HELP_PROPERTIES}"
     96   echo
     97   echo "Use: \"$0 <command> --help\" for more options."
     98 }
     99 
    100 # Check that a command is specified.
    101 if [[ $# -lt 1 ]]; then
    102   echo "Please specify a command [generate|hash|sign|properties]"
    103   exit 1
    104 fi
    105 
    106 # Parse command.
    107 COMMAND="${1:-}"
    108 shift
    109 
    110 case "${COMMAND}" in
    111   generate)
    112     FLAGS_HELP="${HELP_GENERATE}"
    113     ;;
    114 
    115   hash)
    116     FLAGS_HELP="${HELP_HASH}"
    117     ;;
    118 
    119   sign)
    120     FLAGS_HELP="${HELP_SIGN}"
    121     ;;
    122 
    123   properties)
    124     FLAGS_HELP="${HELP_PROPERTIES}"
    125     ;;
    126   *)
    127     echo "Unrecognized command: \"${COMMAND}\"" >&2
    128     usage >&2
    129     exit 1
    130     ;;
    131 esac
    132 
    133 # Flags
    134 FLAGS_HELP="Usage: $0 ${COMMAND} [flags]
    135 ${FLAGS_HELP}"
    136 
    137 if [[ "${COMMAND}" == "generate" ]]; then
    138   DEFINE_string payload "" \
    139     "Path to output the generated unsigned payload file."
    140   DEFINE_string target_image "" \
    141     "Path to the target image that should be sent to clients."
    142   DEFINE_string source_image "" \
    143     "Optional: Path to a source image. If specified, this makes a delta update."
    144   DEFINE_string metadata_size_file "" \
    145     "Optional: Path to output metadata size."
    146 fi
    147 if [[ "${COMMAND}" == "hash" || "${COMMAND}" == "sign" ]]; then
    148   DEFINE_string unsigned_payload "" "Path to the input unsigned payload."
    149   DEFINE_string signature_size "" \
    150     "Signature sizes in bytes in the following format: size1:size2[:...]"
    151 fi
    152 if [[ "${COMMAND}" == "hash" ]]; then
    153   DEFINE_string metadata_hash_file "" \
    154     "Optional: Path to output metadata hash file."
    155   DEFINE_string payload_hash_file "" \
    156     "Optional: Path to output payload hash file."
    157 fi
    158 if [[ "${COMMAND}" == "sign" ]]; then
    159   DEFINE_string payload "" \
    160     "Path to output the generated unsigned payload file."
    161   DEFINE_string metadata_signature_file "" \
    162     "The metatada signatures in the following format: \
    163 metadata_signature1:metadata_signature2[:...]"
    164   DEFINE_string payload_signature_file "" \
    165     "The payload signatures in the following format: \
    166 payload_signature1:payload_signature2[:...]"
    167   DEFINE_string metadata_size_file "" \
    168     "Optional: Path to output metadata size."
    169 fi
    170 if [[ "${COMMAND}" == "properties" ]]; then
    171   DEFINE_string payload "" \
    172     "Path to the input signed or unsigned payload file."
    173   DEFINE_string properties_file "-" \
    174     "Path to output the extracted property files. If '-' is passed stdout will \
    175 be used."
    176 fi
    177 
    178 DEFINE_string work_dir "/tmp" "Where to dump temporary files."
    179 
    180 # Parse command line flag arguments
    181 FLAGS "$@" || exit 1
    182 eval set -- "${FLAGS_ARGV}"
    183 set -e
    184 
    185 # Associative arrays from partition name to file in the source and target
    186 # images. The size of the updated area must be the size of the file.
    187 declare -A SRC_PARTITIONS
    188 declare -A DST_PARTITIONS
    189 
    190 # A list of temporary files to remove during cleanup.
    191 CLEANUP_FILES=()
    192 
    193 # Global options to force the version of the payload.
    194 FORCE_MAJOR_VERSION=""
    195 FORCE_MINOR_VERSION=""
    196 
    197 # Path to the postinstall config file in target image if exists.
    198 POSTINSTALL_CONFIG_FILE=""
    199 
    200 # The fingerprint of zlib in the source image.
    201 ZLIB_FINGERPRINT=""
    202 
    203 # read_option_int <file.txt> <option_key> [default_value]
    204 #
    205 # Reads the unsigned integer value associated with |option_key| in a key=value
    206 # file |file.txt|. Prints the read value if found and valid, otherwise prints
    207 # the |default_value|.
    208 read_option_uint() {
    209   local file_txt="$1"
    210   local option_key="$2"
    211   local default_value="${3:-}"
    212   local value
    213   if value=$(look "${option_key}=" "${file_txt}" | tail -n 1); then
    214     if value=$(echo "${value}" | cut -f 2- -d "=" | grep -E "^[0-9]+$"); then
    215       echo "${value}"
    216       return
    217     fi
    218   fi
    219   echo "${default_value}"
    220 }
    221 
    222 # Create a temporary file in the work_dir with an optional pattern name.
    223 # Prints the name of the newly created file.
    224 create_tempfile() {
    225   local pattern="${1:-tempfile.XXXXXX}"
    226   mktemp --tmpdir="${FLAGS_work_dir}" "${pattern}"
    227 }
    228 
    229 cleanup() {
    230   local err=""
    231   rm -f "${CLEANUP_FILES[@]}" || err=1
    232 
    233   # If we are cleaning up after an error, or if we got an error during
    234   # cleanup (even if we eventually succeeded) return a non-zero exit
    235   # code. This triggers additional logging in most environments that call
    236   # this script.
    237   if [[ -n "${err}" ]]; then
    238     die "Cleanup encountered an error."
    239   fi
    240 }
    241 
    242 cleanup_on_error() {
    243   trap - INT TERM ERR EXIT
    244   cleanup
    245   die "Cleanup success after an error."
    246 }
    247 
    248 cleanup_on_exit() {
    249   trap - INT TERM ERR EXIT
    250   cleanup
    251 }
    252 
    253 trap cleanup_on_error INT TERM ERR
    254 trap cleanup_on_exit EXIT
    255 
    256 
    257 # extract_image <image> <partitions_array>
    258 #
    259 # Detect the format of the |image| file and extract its updatable partitions
    260 # into new temporary files. Add the list of partition names and its files to the
    261 # associative array passed in |partitions_array|.
    262 extract_image() {
    263   local image="$1"
    264 
    265   # Brillo images are zip files. We detect the 4-byte magic header of the zip
    266   # file.
    267   local magic=$(head --bytes=4 "${image}" | hexdump -e '1/1 "%.2x"')
    268   if [[ "${magic}" == "504b0304" ]]; then
    269     echo "Detected .zip file, extracting Brillo image."
    270     extract_image_brillo "$@"
    271     return
    272   fi
    273 
    274   # Chrome OS images are GPT partitioned disks. We should have the cgpt binary
    275   # bundled here and we will use it to extract the partitions, so the GPT
    276   # headers must be valid.
    277   if cgpt show -q -n "${image}" >/dev/null; then
    278     echo "Detected GPT image, extracting Chrome OS image."
    279     extract_image_cros "$@"
    280     return
    281   fi
    282 
    283   die "Couldn't detect the image format of ${image}"
    284 }
    285 
    286 # extract_image_cros <image.bin> <partitions_array>
    287 #
    288 # Extract Chromium OS recovery images into new temporary files.
    289 extract_image_cros() {
    290   local image="$1"
    291   local partitions_array="$2"
    292 
    293   local kernel root
    294   kernel=$(create_tempfile "kernel.bin.XXXXXX")
    295   CLEANUP_FILES+=("${kernel}")
    296   root=$(create_tempfile "root.bin.XXXXXX")
    297   CLEANUP_FILES+=("${root}")
    298 
    299   cros_generate_update_payload --extract \
    300     --image "${image}" \
    301     --kern_path "${kernel}" --root_path "${root}" \
    302     --work_dir "${FLAGS_work_dir}" --outside_chroot
    303 
    304   # Chrome OS uses major_version 1 payloads for all versions, even if the
    305   # updater supports a newer major version.
    306   FORCE_MAJOR_VERSION="1"
    307 
    308   if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then
    309     # Copy from zlib_fingerprint in source image to stdout.
    310     ZLIB_FINGERPRINT=$(e2cp "${root}":/etc/zlib_fingerprint -)
    311   fi
    312 
    313   # When generating legacy Chrome OS images, we need to use "boot" and "system"
    314   # for the partition names to be compatible with updating Brillo devices with
    315   # Chrome OS images.
    316   eval ${partitions_array}[boot]=\""${kernel}"\"
    317   eval ${partitions_array}[system]=\""${root}"\"
    318 
    319   local part varname
    320   for part in boot system; do
    321     varname="${partitions_array}[${part}]"
    322     printf "md5sum of %s: " "${varname}"
    323     md5sum "${!varname}"
    324   done
    325 }
    326 
    327 # extract_image_brillo <target_files.zip> <partitions_array>
    328 #
    329 # Extract the A/B updated partitions from a Brillo target_files zip file into
    330 # new temporary files.
    331 extract_image_brillo() {
    332   local image="$1"
    333   local partitions_array="$2"
    334 
    335   local partitions=( "boot" "system" )
    336   local ab_partitions_list
    337   ab_partitions_list=$(create_tempfile "ab_partitions_list.XXXXXX")
    338   CLEANUP_FILES+=("${ab_partitions_list}")
    339   if unzip -p "${image}" "META/ab_partitions.txt" >"${ab_partitions_list}"; then
    340     if grep -v -E '^[a-zA-Z0-9_-]*$' "${ab_partitions_list}" >&2; then
    341       die "Invalid partition names found in the partition list."
    342     fi
    343     partitions=($(cat "${ab_partitions_list}"))
    344     if [[ ${#partitions[@]} -eq 0 ]]; then
    345       die "The list of partitions is empty. Can't generate a payload."
    346     fi
    347   else
    348     warn "No ab_partitions.txt found. Using default."
    349   fi
    350   echo "List of A/B partitions: ${partitions[@]}"
    351 
    352   # All Brillo updaters support major version 2.
    353   FORCE_MAJOR_VERSION="2"
    354 
    355   if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then
    356     # Source image
    357     local ue_config=$(create_tempfile "ue_config.XXXXXX")
    358     CLEANUP_FILES+=("${ue_config}")
    359     if ! unzip -p "${image}" "META/update_engine_config.txt" \
    360         >"${ue_config}"; then
    361       warn "No update_engine_config.txt found. Assuming pre-release image, \
    362 using payload minor version 2"
    363     fi
    364     # For delta payloads, we use the major and minor version supported by the
    365     # old updater.
    366     FORCE_MINOR_VERSION=$(read_option_uint "${ue_config}" \
    367       "PAYLOAD_MINOR_VERSION" 2)
    368     FORCE_MAJOR_VERSION=$(read_option_uint "${ue_config}" \
    369       "PAYLOAD_MAJOR_VERSION" 2)
    370 
    371     # Brillo support for deltas started with minor version 3.
    372     if [[ "${FORCE_MINOR_VERSION}" -le 2 ]]; then
    373       warn "No delta support from minor version ${FORCE_MINOR_VERSION}. \
    374 Disabling deltas for this source version."
    375       exit ${EX_UNSUPPORTED_DELTA}
    376     fi
    377 
    378     if [[ "${FORCE_MINOR_VERSION}" -ge 4 ]]; then
    379       ZLIB_FINGERPRINT=$(unzip -p "${image}" "META/zlib_fingerprint.txt")
    380     fi
    381   else
    382     # Target image
    383     local postinstall_config=$(create_tempfile "postinstall_config.XXXXXX")
    384     CLEANUP_FILES+=("${postinstall_config}")
    385     if unzip -p "${image}" "META/postinstall_config.txt" \
    386         >"${postinstall_config}"; then
    387       POSTINSTALL_CONFIG_FILE="${postinstall_config}"
    388     fi
    389   fi
    390 
    391   local part part_file temp_raw filesize
    392   for part in "${partitions[@]}"; do
    393     part_file=$(create_tempfile "${part}.img.XXXXXX")
    394     CLEANUP_FILES+=("${part_file}")
    395     unzip -p "${image}" "IMAGES/${part}.img" >"${part_file}"
    396 
    397     # If the partition is stored as an Android sparse image file, we need to
    398     # convert them to a raw image for the update.
    399     local magic=$(head --bytes=4 "${part_file}" | hexdump -e '1/1 "%.2x"')
    400     if [[ "${magic}" == "3aff26ed" ]]; then
    401       temp_raw=$(create_tempfile "${part}.raw.XXXXXX")
    402       CLEANUP_FILES+=("${temp_raw}")
    403       echo "Converting Android sparse image ${part}.img to RAW."
    404       simg2img "${part_file}" "${temp_raw}"
    405       # At this point, we can drop the contents of the old part_file file, but
    406       # we can't delete the file because it will be deleted in cleanup.
    407       true >"${part_file}"
    408       part_file="${temp_raw}"
    409     fi
    410 
    411     # delta_generator only supports images multiple of 4 KiB, so we pad with
    412     # zeros if needed.
    413     filesize=$(stat -c%s "${part_file}")
    414     if [[ $(( filesize % 4096 )) -ne 0 ]]; then
    415       echo "Rounding up partition ${part}.img to multiple of 4 KiB."
    416       : $(( filesize = (filesize + 4095) & -4096 ))
    417       truncate --size="${filesize}" "${part_file}"
    418     fi
    419 
    420     eval "${partitions_array}[\"${part}\"]=\"${part_file}\""
    421     echo "Extracted ${partitions_array}[${part}]: ${filesize} bytes"
    422   done
    423 }
    424 
    425 validate_generate() {
    426   [[ -n "${FLAGS_payload}" ]] ||
    427     die "Error: you must specify an output filename with --payload FILENAME"
    428 
    429   [[ -n "${FLAGS_target_image}" ]] ||
    430     die "Error: you must specify a target image with --target_image FILENAME"
    431 }
    432 
    433 cmd_generate() {
    434   local payload_type="delta"
    435   if [[ -z "${FLAGS_source_image}" ]]; then
    436     payload_type="full"
    437   fi
    438 
    439   echo "Extracting images for ${payload_type} update."
    440 
    441   extract_image "${FLAGS_target_image}" DST_PARTITIONS
    442   if [[ "${payload_type}" == "delta" ]]; then
    443     extract_image "${FLAGS_source_image}" SRC_PARTITIONS
    444   fi
    445 
    446   echo "Generating ${payload_type} update."
    447   # Common payload args:
    448   GENERATOR_ARGS=( -out_file="${FLAGS_payload}" )
    449 
    450   local part old_partitions="" new_partitions="" partition_names=""
    451   for part in "${!DST_PARTITIONS[@]}"; do
    452     if [[ -n "${partition_names}" ]]; then
    453       partition_names+=":"
    454       new_partitions+=":"
    455       old_partitions+=":"
    456     fi
    457     partition_names+="${part}"
    458     new_partitions+="${DST_PARTITIONS[${part}]}"
    459     old_partitions+="${SRC_PARTITIONS[${part}]:-}"
    460   done
    461 
    462   # Target image args:
    463   GENERATOR_ARGS+=(
    464     -partition_names="${partition_names}"
    465     -new_partitions="${new_partitions}"
    466   )
    467 
    468   if [[ "${payload_type}" == "delta" ]]; then
    469     # Source image args:
    470     GENERATOR_ARGS+=(
    471       -old_partitions="${old_partitions}"
    472     )
    473     if [[ -n "${FORCE_MINOR_VERSION}" ]]; then
    474       GENERATOR_ARGS+=( --minor_version="${FORCE_MINOR_VERSION}" )
    475     fi
    476     if [[ -n "${ZLIB_FINGERPRINT}" ]]; then
    477       GENERATOR_ARGS+=( --zlib_fingerprint="${ZLIB_FINGERPRINT}" )
    478     fi
    479   fi
    480 
    481   if [[ -n "${FORCE_MAJOR_VERSION}" ]]; then
    482     GENERATOR_ARGS+=( --major_version="${FORCE_MAJOR_VERSION}" )
    483   fi
    484 
    485   if [[ -n "${FLAGS_metadata_size_file}" ]]; then
    486     GENERATOR_ARGS+=( --out_metadata_size_file="${FLAGS_metadata_size_file}" )
    487   fi
    488 
    489   if [[ -n "${POSTINSTALL_CONFIG_FILE}" ]]; then
    490     GENERATOR_ARGS+=(
    491       --new_postinstall_config_file="${POSTINSTALL_CONFIG_FILE}"
    492     )
    493   fi
    494 
    495   echo "Running delta_generator with args: ${GENERATOR_ARGS[@]}"
    496   "${GENERATOR}" "${GENERATOR_ARGS[@]}"
    497 
    498   echo "Done generating ${payload_type} update."
    499 }
    500 
    501 validate_hash() {
    502   [[ -n "${FLAGS_signature_size}" ]] ||
    503     die "Error: you must specify signature size with --signature_size SIZES"
    504 
    505   [[ -n "${FLAGS_unsigned_payload}" ]] ||
    506     die "Error: you must specify the input unsigned payload with \
    507 --unsigned_payload FILENAME"
    508 
    509   [[ -n "${FLAGS_payload_hash_file}" ]] ||
    510     die "Error: you must specify --payload_hash_file FILENAME"
    511 
    512   [[ -n "${FLAGS_metadata_hash_file}" ]] ||
    513     die "Error: you must specify --metadata_hash_file FILENAME"
    514 }
    515 
    516 cmd_hash() {
    517   "${GENERATOR}" \
    518       -in_file="${FLAGS_unsigned_payload}" \
    519       -signature_size="${FLAGS_signature_size}" \
    520       -out_hash_file="${FLAGS_payload_hash_file}" \
    521       -out_metadata_hash_file="${FLAGS_metadata_hash_file}"
    522 
    523   echo "Done generating hash."
    524 }
    525 
    526 validate_sign() {
    527   [[ -n "${FLAGS_signature_size}" ]] ||
    528     die "Error: you must specify signature size with --signature_size SIZES"
    529 
    530   [[ -n "${FLAGS_unsigned_payload}" ]] ||
    531     die "Error: you must specify the input unsigned payload with \
    532 --unsigned_payload FILENAME"
    533 
    534   [[ -n "${FLAGS_payload}" ]] ||
    535     die "Error: you must specify the output signed payload with \
    536 --payload FILENAME"
    537 
    538   [[ -n "${FLAGS_payload_signature_file}" ]] ||
    539     die "Error: you must specify the payload signature file with \
    540 --payload_signature_file SIGNATURES"
    541 
    542   [[ -n "${FLAGS_metadata_signature_file}" ]] ||
    543     die "Error: you must specify the metadata signature file with \
    544 --metadata_signature_file SIGNATURES"
    545 }
    546 
    547 cmd_sign() {
    548   GENERATOR_ARGS=(
    549     -in_file="${FLAGS_unsigned_payload}"
    550     -signature_size="${FLAGS_signature_size}"
    551     -signature_file="${FLAGS_payload_signature_file}"
    552     -metadata_signature_file="${FLAGS_metadata_signature_file}"
    553     -out_file="${FLAGS_payload}"
    554   )
    555 
    556   if [[ -n "${FLAGS_metadata_size_file}" ]]; then
    557     GENERATOR_ARGS+=( --out_metadata_size_file="${FLAGS_metadata_size_file}" )
    558   fi
    559 
    560   "${GENERATOR}" "${GENERATOR_ARGS[@]}"
    561   echo "Done signing payload."
    562 }
    563 
    564 validate_properties() {
    565   [[ -n "${FLAGS_payload}" ]] ||
    566     die "Error: you must specify the payload file with --payload FILENAME"
    567 
    568   [[ -n "${FLAGS_properties_file}" ]] ||
    569     die "Error: you must specify a non empty --properties_file FILENAME"
    570 }
    571 
    572 cmd_properties() {
    573   "${GENERATOR}" \
    574       -in_file="${FLAGS_payload}" \
    575       -properties_file="${FLAGS_properties_file}"
    576 }
    577 
    578 # Sanity check that the real generator exists:
    579 GENERATOR="$(which delta_generator)"
    580 [[ -x "${GENERATOR}" ]] || die "can't find delta_generator"
    581 
    582 case "$COMMAND" in
    583   generate) validate_generate
    584             cmd_generate
    585             ;;
    586   hash) validate_hash
    587         cmd_hash
    588         ;;
    589   sign) validate_sign
    590         cmd_sign
    591         ;;
    592   properties) validate_properties
    593               cmd_properties
    594               ;;
    595 esac
    596