blob: 7f089828c8c784c76c848ccfe95059c68494b04e [file] [log] [blame]
Jason Kusumabe998f42015-09-03 15:53:13 -07001#!/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#
15# Generate command arguments:
Jason Kusuma9a4cae22015-10-08 18:17:57 -070016# --payload generated unsigned payload output file
17# --source_image if defined, generate a delta payload from the specified
18# image to the target_image
19# --target_image the target image that should be sent to clients
20# --metadata_size_file if defined, generate a file containing the size of the payload
21# metadata in bytes to the specified file
Jason Kusumabe998f42015-09-03 15:53:13 -070022#
23# Hash command arguments:
24# --unsigned_payload the input unsigned payload to generate the hash from
25# --signature_size signature sizes in bytes in the following format:
Alex Deymo89ff9e32015-09-15 19:29:01 -070026# "size1:size2[:...]"
Jason Kusumabe998f42015-09-03 15:53:13 -070027# --payload_hash_file if defined, generate a payload hash and output to the
28# specified file
29# --metadata_hash_file if defined, generate a metadata hash and output to the
30# specified file
31#
32# Sign command arguments:
Alex Deymo89ff9e32015-09-15 19:29:01 -070033# --unsigned_payload the input unsigned payload to insert the signatures
34# --payload the output signed payload
35# --signature_size signature sizes in bytes in the following format:
36# "size1:size2[:...]"
37# --payload_signature_file the payload signature files in the following
38# format:
39# "payload_signature1:payload_signature2[:...]"
40# --metadata_signature_file the metadata signature files in the following
41# format:
42# "metadata_signature1:metadata_signature2[:...]"
Jason Kusuma9a4cae22015-10-08 18:17:57 -070043# --metadata_size_file if defined, generate a file containing the size of
44# the signed payload metadata in bytes to the
45# specified file
Jason Kusumabe998f42015-09-03 15:53:13 -070046# Note that the number of signature sizes and payload signatures have to match.
47
48# Load common CrOS utilities. Inside the chroot this file is installed in
49# /usr/lib/crosutils. This script may also be called from a zipfile, in which
50# case common.sh will be in the current directory.
51find_common_sh() {
52 local thisdir="$(dirname "$(readlink -f "$0")")"
53 local common_paths=(/usr/lib/crosutils "${thisdir}")
54 local path
55
56 SCRIPT_ROOT="${common_paths[0]}"
57 for path in "${common_paths[@]}"; do
58 if [[ -r "${path}/common.sh" ]]; then
59 SCRIPT_ROOT="${path}"
60 break
61 fi
62 done
63
64 # We have to fake GCLIENT_ROOT in case we're running inside
65 # au_zip enviroment. GCLIENT_ROOT detection became fatal.
66 [[ "${SCRIPT_ROOT}" == "${thisdir}" ]] && export GCLIENT_ROOT="."
67}
68
69find_common_sh
70. "${SCRIPT_ROOT}/common.sh" || exit 1
71
Alex Deymoc64ffd52015-09-25 18:10:07 -070072HELP_GENERATE="generate: Generate an unsigned update payload."
73HELP_HASH="hash: Generate the hashes of the unsigned payload and metadata used \
74for signing."
75HELP_SIGN="sign: Insert the signatures into the unsigned payload."
76
77usage() {
78 echo "Supported commands:"
79 echo
80 echo "${HELP_GENERATE}"
81 echo "${HELP_HASH}"
82 echo "${HELP_SIGN}"
83 echo
84 echo "Use: \"$0 <command> --help\" for more options."
85}
86
87# Check that a command is specified.
Jason Kusumabe998f42015-09-03 15:53:13 -070088if [[ $# -lt 1 ]]; then
89 echo "Please specify a command [generate|hash|sign]"
90 exit 1
91fi
92
Alex Deymoc64ffd52015-09-25 18:10:07 -070093# Parse command.
94COMMAND="${1:-}"
95shift
96
97case "${COMMAND}" in
98 generate)
99 FLAGS_HELP="${HELP_GENERATE}"
100 ;;
101
102 hash)
103 FLAGS_HELP="${HELP_HASH}"
104 ;;
105
106 sign)
107 FLAGS_HELP="${HELP_SIGN}"
Jason Kusumabe998f42015-09-03 15:53:13 -0700108 ;;
109 *)
Alex Deymoc64ffd52015-09-25 18:10:07 -0700110 echo "Unrecognized command: \"${COMMAND}\"" >&2
111 usage >&2
Jason Kusumabe998f42015-09-03 15:53:13 -0700112 exit 1
113 ;;
114esac
115
Jason Kusumabe998f42015-09-03 15:53:13 -0700116# Flags
Alex Deymoc64ffd52015-09-25 18:10:07 -0700117FLAGS_HELP="Usage: $0 ${COMMAND} [flags]
118${FLAGS_HELP}"
119
120if [[ "${COMMAND}" == "generate" ]]; then
121 DEFINE_string payload "" \
122 "Path to output the generated unsigned payload file."
123 DEFINE_string target_image "" \
124 "Path to the target image that should be sent to clients."
125 DEFINE_string source_image "" \
126 "Optional: Path to a source image. If specified, this makes a delta update."
Jason Kusuma9a4cae22015-10-08 18:17:57 -0700127 DEFINE_string metadata_size_file "" \
128 "Optional: Path to output metadata size."
Alex Deymoc64ffd52015-09-25 18:10:07 -0700129fi
130if [[ "${COMMAND}" == "hash" || "${COMMAND}" == "sign" ]]; then
131 DEFINE_string unsigned_payload "" "Path to the input unsigned payload."
132 DEFINE_string signature_size "" \
133 "Signature sizes in bytes in the following format: size1:size2[:...]"
134fi
135if [[ "${COMMAND}" == "hash" ]]; then
136 DEFINE_string metadata_hash_file "" \
137 "Optional: Path to output metadata hash file."
138 DEFINE_string payload_hash_file "" \
139 "Optional: Path to output payload hash file."
140fi
141if [[ "${COMMAND}" == "sign" ]]; then
142 DEFINE_string payload "" \
143 "Path to output the generated unsigned payload file."
144 DEFINE_string metadata_signature_file "" \
145 "The metatada signatures in the following format: \
146metadata_signature1:metadata_signature2[:...]"
147 DEFINE_string payload_signature_file "" \
148 "The payload signatures in the following format: \
149payload_signature1:payload_signature2[:...]"
Jason Kusuma9a4cae22015-10-08 18:17:57 -0700150 DEFINE_string metadata_size_file "" \
151 "Optional: Path to output metadata size."
Alex Deymoc64ffd52015-09-25 18:10:07 -0700152fi
Jason Kusumabe998f42015-09-03 15:53:13 -0700153DEFINE_string work_dir "/tmp" "Where to dump temporary files."
154
155# Parse command line flag arguments
156FLAGS "$@" || exit 1
157eval set -- "${FLAGS_ARGV}"
Alex Deymo89ff9e32015-09-15 19:29:01 -0700158set -e
Jason Kusumabe998f42015-09-03 15:53:13 -0700159
Alex Deymo89ff9e32015-09-15 19:29:01 -0700160# Associative arrays from partition name to file in the source and target
161# images. The size of the updated area must be the size of the file.
162declare -A SRC_PARTITIONS
163declare -A DST_PARTITIONS
164
165# A list of temporary files to remove during cleanup.
166CLEANUP_FILES=()
167
Alex Deymo48b502a2015-09-17 19:00:18 -0700168# Global options to force the version of the payload.
169FORCE_MAJOR_VERSION=""
170FORCE_MINOR_VERSION=""
171
Alex Deymoc97df432015-09-25 17:23:52 -0700172# read_option_int <file.txt> <option_key> [default_value]
173#
174# Reads the unsigned integer value associated with |option_key| in a key=value
175# file |file.txt|. Prints the read value if found and valid, otherwise prints
176# the |default_value|.
177read_option_uint() {
178 local file_txt="$1"
179 local option_key="$2"
180 local default_value="${3:-}"
181 local value
182 if value=$(look "${option_key}=" "${file_txt}" | tail -n 1); then
183 if value=$(echo "${value}" | cut -f 2- -d "=" | grep -E "^[0-9]+$"); then
184 echo "${value}"
185 return
186 fi
187 fi
188 echo "${default_value}"
189}
190
Alex Deymo89ff9e32015-09-15 19:29:01 -0700191# Create a temporary file in the work_dir with an optional pattern name.
192# Prints the name of the newly created file.
193create_tempfile() {
194 local pattern="${1:-tempfile.XXXXXX}"
195 mktemp --tmpdir="${FLAGS_work_dir}" "${pattern}"
196}
Jason Kusumabe998f42015-09-03 15:53:13 -0700197
198cleanup() {
199 local err=""
Alex Deymo89ff9e32015-09-15 19:29:01 -0700200 rm -f "${CLEANUP_FILES[@]}" || err=1
Jason Kusumabe998f42015-09-03 15:53:13 -0700201
202 # If we are cleaning up after an error, or if we got an error during
203 # cleanup (even if we eventually succeeded) return a non-zero exit
204 # code. This triggers additional logging in most environments that call
205 # this script.
206 if [[ -n "${err}" ]]; then
207 die "Cleanup encountered an error."
208 fi
209}
210
211cleanup_on_error() {
212 trap - INT TERM ERR EXIT
213 cleanup
214 die "Cleanup success after an error."
215}
216
217cleanup_on_exit() {
218 trap - INT TERM ERR EXIT
219 cleanup
220}
221
222trap cleanup_on_error INT TERM ERR
223trap cleanup_on_exit EXIT
224
Alex Deymo48b502a2015-09-17 19:00:18 -0700225
226# extract_image <image> <partitions_array>
227#
228# Detect the format of the |image| file and extract its updatable partitions
229# into new temporary files. Add the list of partition names and its files to the
230# associative array passed in |partitions_array|.
231extract_image() {
232 local image="$1"
233
234 # Brillo images are zip files. We detect the 4-byte magic header of the zip
235 # file.
236 local magic=$(head --bytes=4 "${image}" | hexdump -e '1/1 "%.2x"')
237 if [[ "${magic}" == "504b0304" ]]; then
238 echo "Detected .zip file, extracting Brillo image."
239 extract_image_brillo "$@"
240 return
241 fi
242
243 # Chrome OS images are GPT partitioned disks. We should have the cgpt binary
244 # bundled here and we will use it to extract the partitions, so the GPT
245 # headers must be valid.
246 if cgpt show -q -n "${image}" >/dev/null; then
247 echo "Detected GPT image, extracting Chrome OS image."
248 extract_image_cros "$@"
249 return
250 fi
251
252 die "Couldn't detect the image format of ${image}"
253}
254
Alex Deymo89ff9e32015-09-15 19:29:01 -0700255# extract_image_cros <image.bin> <partitions_array>
256#
Alex Deymo48b502a2015-09-17 19:00:18 -0700257# Extract Chromium OS recovery images into new temporary files.
Alex Deymo89ff9e32015-09-15 19:29:01 -0700258extract_image_cros() {
259 local image="$1"
260 local partitions_array="$2"
261
262 local kernel root
263 kernel=$(create_tempfile "kernel.bin.XXXXXX")
264 CLEANUP_FILES+=("${kernel}")
265 root=$(create_tempfile "root.bin.XXXXXX")
266 CLEANUP_FILES+=("${root}")
267
268 cros_generate_update_payload --extract \
269 --image "${image}" \
270 --kern_path "${kernel}" --root_path "${root}" \
271 --work_dir "${FLAGS_work_dir}" --outside_chroot
272
Alex Deymo48b502a2015-09-17 19:00:18 -0700273 # When generating legacy Chrome OS images, we need to use "boot" and "system"
274 # for the partition names to be compatible with updating Brillo devices with
275 # Chrome OS images.
276 eval ${partitions_array}[boot]=\""${kernel}"\"
277 eval ${partitions_array}[system]=\""${root}"\"
Alex Deymo89ff9e32015-09-15 19:29:01 -0700278
279 local part varname
Alex Deymo48b502a2015-09-17 19:00:18 -0700280 for part in boot system; do
Alex Deymo89ff9e32015-09-15 19:29:01 -0700281 varname="${partitions_array}[${part}]"
282 printf "md5sum of %s: " "${varname}"
283 md5sum "${!varname}"
284 done
285}
286
Alex Deymo48b502a2015-09-17 19:00:18 -0700287# extract_image_brillo <target_files.zip> <partitions_array>
288#
289# Extract the A/B updated partitions from a Brillo target_files zip file into
290# new temporary files.
291extract_image_brillo() {
292 local image="$1"
293 local partitions_array="$2"
294
295 # TODO(deymo): Read the list of partitions from the metadata. We should
296 # sanitize the list of partition names to be in [a-zA-Z0-9-]+.
297 local partitions=( "boot" "system" )
298
299 if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then
Alex Deymoc97df432015-09-25 17:23:52 -0700300 ue_config=$(create_tempfile "ue_config.XXXXXX")
301 CLEANUP_FILES+=("${ue_config}")
302 if ! unzip -p "${image}" "META/update_engine_config.txt" \
303 >"${ue_config}"; then
304 warn "No update_engine_config.txt found. Assuming pre-release image, \
305using payload minor version 2"
306 fi
307 FORCE_MINOR_VERSION=$(read_option_uint "${ue_config}" \
308 "PAYLOAD_MINOR_VERSION" 2)
Alex Deymo48b502a2015-09-17 19:00:18 -0700309 fi
310
311 local part part_file temp_raw filesize
312 for part in "${partitions[@]}"; do
313 part_file=$(create_tempfile "${part}.img.XXXXXX")
314 CLEANUP_FILES+=("${part_file}")
315 unzip -p "${image}" "IMAGES/${part}.img" >"${part_file}"
316
317 # If the partition is stored as an Android sparse image file, we need to
318 # convert them to a raw image for the update.
319 local magic=$(head --bytes=4 "${part_file}" | hexdump -e '1/1 "%.2x"')
320 if [[ "${magic}" == "3aff26ed" ]]; then
321 temp_raw=$(create_tempfile "${part}.raw.XXXXXX")
322 CLEANUP_FILES+=("${temp_raw}")
323 echo "Converting Android sparse image ${part}.img to RAW."
324 simg2img "${part_file}" "${temp_raw}"
325 # At this point, we can drop the contents of the old part_file file, but
326 # we can't delete the file because it will be deleted in cleanup.
327 true >"${part_file}"
328 part_file="${temp_raw}"
329 fi
330
331 # delta_generator only supports images multiple of 4 KiB, so we pad with
332 # zeros if needed.
333 filesize=$(stat -c%s "${part_file}")
334 if [[ $(( filesize % 4096 )) -ne 0 ]]; then
335 echo "Rounding up partition ${part}.img to multiple of 4 KiB."
336 : $(( filesize = (filesize + 4095) & -4096 ))
337 truncate --size="${filesize}" "${part_file}"
338 fi
339
340 eval "${partitions_array}[\"${part}\"]=\"${part_file}\""
341 echo "Extracted ${partitions_array}[${part}]: ${filesize} bytes"
342 done
343}
344
Jason Kusumabe998f42015-09-03 15:53:13 -0700345validate_generate() {
346 [[ -n "${FLAGS_payload}" ]] ||
347 die "Error: you must specify an output filename with --payload FILENAME"
348
349 [[ -n "${FLAGS_target_image}" ]] ||
350 die "Error: you must specify a target image with --target_image FILENAME"
351}
352
353cmd_generate() {
Alex Deymo89ff9e32015-09-15 19:29:01 -0700354 local payload_type="delta"
Jason Kusumabe998f42015-09-03 15:53:13 -0700355 if [[ -z "${FLAGS_source_image}" ]]; then
Alex Deymo89ff9e32015-09-15 19:29:01 -0700356 payload_type="full"
Jason Kusumabe998f42015-09-03 15:53:13 -0700357 fi
358
Alex Deymo48b502a2015-09-17 19:00:18 -0700359 echo "Extracting images for ${payload_type} update."
Jason Kusumabe998f42015-09-03 15:53:13 -0700360
Alex Deymo48b502a2015-09-17 19:00:18 -0700361 extract_image "${FLAGS_target_image}" DST_PARTITIONS
Alex Deymo89ff9e32015-09-15 19:29:01 -0700362 if [[ "${payload_type}" == "delta" ]]; then
Alex Deymo48b502a2015-09-17 19:00:18 -0700363 extract_image "${FLAGS_source_image}" SRC_PARTITIONS
Jason Kusumabe998f42015-09-03 15:53:13 -0700364 fi
365
Alex Deymo48b502a2015-09-17 19:00:18 -0700366 echo "Generating ${payload_type} update."
Jason Kusumabe998f42015-09-03 15:53:13 -0700367 GENERATOR_ARGS=(
368 # Common payload args:
369 -out_file="${FLAGS_payload}"
370 # Target image args:
Alex Deymo89ff9e32015-09-15 19:29:01 -0700371 # TODO(deymo): Pass the list of partitions to the generator.
Alex Deymo48b502a2015-09-17 19:00:18 -0700372 -new_image="${DST_PARTITIONS[system]}"
373 -new_kernel="${DST_PARTITIONS[boot]}"
Jason Kusumabe998f42015-09-03 15:53:13 -0700374 )
375
Alex Deymo89ff9e32015-09-15 19:29:01 -0700376 if [[ "${payload_type}" == "delta" ]]; then
Jason Kusumabe998f42015-09-03 15:53:13 -0700377 GENERATOR_ARGS+=(
378 # Source image args:
Alex Deymo48b502a2015-09-17 19:00:18 -0700379 -old_image="${SRC_PARTITIONS[system]}"
380 -old_kernel="${SRC_PARTITIONS[boot]}"
Jason Kusumabe998f42015-09-03 15:53:13 -0700381 )
Alex Deymo48b502a2015-09-17 19:00:18 -0700382 if [[ -n "${FORCE_MINOR_VERSION}" ]]; then
383 GENERATOR_ARGS+=( --minor_version="${FORCE_MINOR_VERSION}" )
384 fi
385 fi
386
387 if [[ -n "${FORCE_MAJOR_VERSION}" ]]; then
388 GENERATOR_ARGS+=( --major_version="${FORCE_MAJOR_VERSION}" )
Jason Kusumabe998f42015-09-03 15:53:13 -0700389 fi
390
Jason Kusuma9a4cae22015-10-08 18:17:57 -0700391 if [[ -n "${FLAGS_metadata_size_file}" ]]; then
392 GENERATOR_ARGS+=( --out_metadata_size_file="${FLAGS_metadata_size_file}" )
393 fi
394
Jason Kusumabe998f42015-09-03 15:53:13 -0700395 echo "Running delta_generator with args: ${GENERATOR_ARGS[@]}"
Jason Kusuma9a4cae22015-10-08 18:17:57 -0700396 "${GENERATOR}" "${GENERATOR_ARGS[@]}"
Jason Kusumabe998f42015-09-03 15:53:13 -0700397
Alex Deymo89ff9e32015-09-15 19:29:01 -0700398 echo "Done generating ${payload_type} update."
Jason Kusumabe998f42015-09-03 15:53:13 -0700399}
400
401validate_hash() {
402 [[ -n "${FLAGS_signature_size}" ]] ||
403 die "Error: you must specify signature size with --signature_size SIZES"
404
405 [[ -n "${FLAGS_unsigned_payload}" ]] ||
406 die "Error: you must specify the input unsigned payload with \
407--unsigned_payload FILENAME"
408
409 [[ -n "${FLAGS_metadata_hash_file}" ]] ||
410 [[ -n "${FLAGS_payload_hash_file}" ]] ||
411 die "Error: you must specify --metadata_hash_file FILENAME \
412or --payload_hash_file FILENAME"
413}
414
415cmd_hash() {
416 if [[ -n "${FLAGS_metadata_hash_file}" ]]; then
417 "${GENERATOR}" \
418 -in_file="${FLAGS_unsigned_payload}" \
419 -signature_size="${FLAGS_signature_size}" \
420 -out_metadata_hash_file="${FLAGS_metadata_hash_file}"
421 fi
422
423 if [[ -n "${FLAGS_payload_hash_file}" ]]; then
424 "${GENERATOR}" \
425 -in_file="${FLAGS_unsigned_payload}" \
426 -signature_size="${FLAGS_signature_size}" \
427 -out_hash_file="${FLAGS_payload_hash_file}"
428 fi
429 echo "Done generating hash."
430}
431
432validate_sign() {
433 [[ -n "${FLAGS_signature_size}" ]] ||
434 die "Error: you must specify signature size with --signature_size SIZES"
435
436 [[ -n "${FLAGS_unsigned_payload}" ]] ||
437 die "Error: you must specify the input unsigned payload with \
438--unsigned_payload FILENAME"
439
440 [[ -n "${FLAGS_payload}" ]] ||
441 die "Error: you must specify the output signed payload with \
442--payload FILENAME"
443
444 [[ -n "${FLAGS_payload_signature_file}" ]] ||
445 die "Error: you must specify the payload signature file with \
446--payload_signature_file SIGNATURES"
Alex Deymo89ff9e32015-09-15 19:29:01 -0700447
448 [[ -n "${FLAGS_metadata_signature_file}" ]] ||
449 die "Error: you must specify the metadata signature file with \
450--metadata_signature_file SIGNATURES"
Jason Kusumabe998f42015-09-03 15:53:13 -0700451}
452
453cmd_sign() {
Jason Kusuma9a4cae22015-10-08 18:17:57 -0700454 GENERATOR_ARGS=(
455 -in_file="${FLAGS_unsigned_payload}"
456 -signature_size="${FLAGS_signature_size}"
457 -signature_file="${FLAGS_payload_signature_file}"
458 -metadata_signature_file="${FLAGS_metadata_signature_file}"
459 -out_file="${FLAGS_payload}"
460 )
461
462 if [[ -n "${FLAGS_metadata_size_file}" ]]; then
463 GENERATOR_ARGS+=( --out_metadata_size_file="${FLAGS_metadata_size_file}" )
464 fi
465
466 "${GENERATOR}" "${GENERATOR_ARGS[@]}"
Jason Kusumabe998f42015-09-03 15:53:13 -0700467 echo "Done signing payload."
468}
469
470# TODO: Extract the input zip files once the format is finalized
471
472# Sanity check that the real generator exists:
473GENERATOR="$(which delta_generator)"
474[[ -x "${GENERATOR}" ]] || die "can't find delta_generator"
475
476case "$COMMAND" in
477 generate) validate_generate
478 cmd_generate
479 ;;
480 hash) validate_hash
481 cmd_hash
482 ;;
483 sign) validate_sign
484 cmd_sign
485 ;;
486esac