blob: 68fb13ac9c3342692e346b9b424be4d9a984ff99 [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:
16# --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#
21# Hash command arguments:
22# --unsigned_payload the input unsigned payload to generate the hash from
23# --signature_size signature sizes in bytes in the following format:
Alex Deymo89ff9e32015-09-15 19:29:01 -070024# "size1:size2[:...]"
Jason Kusumabe998f42015-09-03 15:53:13 -070025# --payload_hash_file if defined, generate a payload hash and output to the
26# specified file
27# --metadata_hash_file if defined, generate a metadata hash and output to the
28# specified file
29#
30# Sign command arguments:
Alex Deymo89ff9e32015-09-15 19:29:01 -070031# --unsigned_payload the input unsigned payload to insert the signatures
32# --payload the output signed payload
33# --signature_size signature sizes in bytes in the following format:
34# "size1:size2[:...]"
35# --payload_signature_file the payload signature files in the following
36# format:
37# "payload_signature1:payload_signature2[:...]"
38# --metadata_signature_file the metadata signature files in the following
39# format:
40# "metadata_signature1:metadata_signature2[:...]"
Jason Kusumabe998f42015-09-03 15:53:13 -070041# Note that the number of signature sizes and payload signatures have to match.
42
43# Load common CrOS utilities. Inside the chroot this file is installed in
44# /usr/lib/crosutils. This script may also be called from a zipfile, in which
45# case common.sh will be in the current directory.
46find_common_sh() {
47 local thisdir="$(dirname "$(readlink -f "$0")")"
48 local common_paths=(/usr/lib/crosutils "${thisdir}")
49 local path
50
51 SCRIPT_ROOT="${common_paths[0]}"
52 for path in "${common_paths[@]}"; do
53 if [[ -r "${path}/common.sh" ]]; then
54 SCRIPT_ROOT="${path}"
55 break
56 fi
57 done
58
59 # We have to fake GCLIENT_ROOT in case we're running inside
60 # au_zip enviroment. GCLIENT_ROOT detection became fatal.
61 [[ "${SCRIPT_ROOT}" == "${thisdir}" ]] && export GCLIENT_ROOT="."
62}
63
64find_common_sh
65. "${SCRIPT_ROOT}/common.sh" || exit 1
66
67# Check that a command is specified
68if [[ $# -lt 1 ]]; then
69 echo "Please specify a command [generate|hash|sign]"
70 exit 1
71fi
72
73# Parse command
74case "$1" in
75 generate|hash|sign)
76 COMMAND=$1
77 ;;
78 *)
79 echo "Unrecognized command:" $1
80 exit 1
81 ;;
82esac
83
84shift
85
86# Flags
87DEFINE_string payload "" "Path to output the generated payload file."
Alex Deymo89ff9e32015-09-15 19:29:01 -070088DEFINE_string target_image "" \
89 "Path to the target image that should be sent to clients."
90DEFINE_string source_image "" \
91 "Optional: Path to a source image. If specified, this makes\
Jason Kusumabe998f42015-09-03 15:53:13 -070092 a delta update."
93DEFINE_string unsigned_payload "" "Path to the generated unsigned payload."
Alex Deymo89ff9e32015-09-15 19:29:01 -070094DEFINE_string signature_size "" \
95 "Signature sizes in bytes in the following format: size1:size2[:...]"
Jason Kusumabe998f42015-09-03 15:53:13 -070096DEFINE_string payload_hash_file "" "Optional: Path to output payload hash file."
Alex Deymo89ff9e32015-09-15 19:29:01 -070097DEFINE_string metadata_hash_file "" \
98 "Optional: Path to output metadata hash file."
99DEFINE_string payload_signature_file "" \
100 "The payload signatures in the following format:\
101 payload_signature1:payload_signature2[:...]"
102DEFINE_string metadata_signature_file "" \
103 "The metatada signatures in the following format:\
104 metadata_signature1:metadata_signature2[:...]"
Jason Kusumabe998f42015-09-03 15:53:13 -0700105DEFINE_string work_dir "/tmp" "Where to dump temporary files."
106
107# Parse command line flag arguments
108FLAGS "$@" || exit 1
109eval set -- "${FLAGS_ARGV}"
Alex Deymo89ff9e32015-09-15 19:29:01 -0700110set -e
Jason Kusumabe998f42015-09-03 15:53:13 -0700111
Alex Deymo89ff9e32015-09-15 19:29:01 -0700112# Associative arrays from partition name to file in the source and target
113# images. The size of the updated area must be the size of the file.
114declare -A SRC_PARTITIONS
115declare -A DST_PARTITIONS
116
117# A list of temporary files to remove during cleanup.
118CLEANUP_FILES=()
119
Alex Deymo48b502a2015-09-17 19:00:18 -0700120# Global options to force the version of the payload.
121FORCE_MAJOR_VERSION=""
122FORCE_MINOR_VERSION=""
123
Alex Deymoc97df432015-09-25 17:23:52 -0700124# read_option_int <file.txt> <option_key> [default_value]
125#
126# Reads the unsigned integer value associated with |option_key| in a key=value
127# file |file.txt|. Prints the read value if found and valid, otherwise prints
128# the |default_value|.
129read_option_uint() {
130 local file_txt="$1"
131 local option_key="$2"
132 local default_value="${3:-}"
133 local value
134 if value=$(look "${option_key}=" "${file_txt}" | tail -n 1); then
135 if value=$(echo "${value}" | cut -f 2- -d "=" | grep -E "^[0-9]+$"); then
136 echo "${value}"
137 return
138 fi
139 fi
140 echo "${default_value}"
141}
142
Alex Deymo89ff9e32015-09-15 19:29:01 -0700143# Create a temporary file in the work_dir with an optional pattern name.
144# Prints the name of the newly created file.
145create_tempfile() {
146 local pattern="${1:-tempfile.XXXXXX}"
147 mktemp --tmpdir="${FLAGS_work_dir}" "${pattern}"
148}
Jason Kusumabe998f42015-09-03 15:53:13 -0700149
150cleanup() {
151 local err=""
Alex Deymo89ff9e32015-09-15 19:29:01 -0700152 rm -f "${CLEANUP_FILES[@]}" || err=1
Jason Kusumabe998f42015-09-03 15:53:13 -0700153
154 # If we are cleaning up after an error, or if we got an error during
155 # cleanup (even if we eventually succeeded) return a non-zero exit
156 # code. This triggers additional logging in most environments that call
157 # this script.
158 if [[ -n "${err}" ]]; then
159 die "Cleanup encountered an error."
160 fi
161}
162
163cleanup_on_error() {
164 trap - INT TERM ERR EXIT
165 cleanup
166 die "Cleanup success after an error."
167}
168
169cleanup_on_exit() {
170 trap - INT TERM ERR EXIT
171 cleanup
172}
173
174trap cleanup_on_error INT TERM ERR
175trap cleanup_on_exit EXIT
176
Alex Deymo48b502a2015-09-17 19:00:18 -0700177
178# extract_image <image> <partitions_array>
179#
180# Detect the format of the |image| file and extract its updatable partitions
181# into new temporary files. Add the list of partition names and its files to the
182# associative array passed in |partitions_array|.
183extract_image() {
184 local image="$1"
185
186 # Brillo images are zip files. We detect the 4-byte magic header of the zip
187 # file.
188 local magic=$(head --bytes=4 "${image}" | hexdump -e '1/1 "%.2x"')
189 if [[ "${magic}" == "504b0304" ]]; then
190 echo "Detected .zip file, extracting Brillo image."
191 extract_image_brillo "$@"
192 return
193 fi
194
195 # Chrome OS images are GPT partitioned disks. We should have the cgpt binary
196 # bundled here and we will use it to extract the partitions, so the GPT
197 # headers must be valid.
198 if cgpt show -q -n "${image}" >/dev/null; then
199 echo "Detected GPT image, extracting Chrome OS image."
200 extract_image_cros "$@"
201 return
202 fi
203
204 die "Couldn't detect the image format of ${image}"
205}
206
Alex Deymo89ff9e32015-09-15 19:29:01 -0700207# extract_image_cros <image.bin> <partitions_array>
208#
Alex Deymo48b502a2015-09-17 19:00:18 -0700209# Extract Chromium OS recovery images into new temporary files.
Alex Deymo89ff9e32015-09-15 19:29:01 -0700210extract_image_cros() {
211 local image="$1"
212 local partitions_array="$2"
213
214 local kernel root
215 kernel=$(create_tempfile "kernel.bin.XXXXXX")
216 CLEANUP_FILES+=("${kernel}")
217 root=$(create_tempfile "root.bin.XXXXXX")
218 CLEANUP_FILES+=("${root}")
219
220 cros_generate_update_payload --extract \
221 --image "${image}" \
222 --kern_path "${kernel}" --root_path "${root}" \
223 --work_dir "${FLAGS_work_dir}" --outside_chroot
224
Alex Deymo48b502a2015-09-17 19:00:18 -0700225 # When generating legacy Chrome OS images, we need to use "boot" and "system"
226 # for the partition names to be compatible with updating Brillo devices with
227 # Chrome OS images.
228 eval ${partitions_array}[boot]=\""${kernel}"\"
229 eval ${partitions_array}[system]=\""${root}"\"
Alex Deymo89ff9e32015-09-15 19:29:01 -0700230
231 local part varname
Alex Deymo48b502a2015-09-17 19:00:18 -0700232 for part in boot system; do
Alex Deymo89ff9e32015-09-15 19:29:01 -0700233 varname="${partitions_array}[${part}]"
234 printf "md5sum of %s: " "${varname}"
235 md5sum "${!varname}"
236 done
237}
238
Alex Deymo48b502a2015-09-17 19:00:18 -0700239# extract_image_brillo <target_files.zip> <partitions_array>
240#
241# Extract the A/B updated partitions from a Brillo target_files zip file into
242# new temporary files.
243extract_image_brillo() {
244 local image="$1"
245 local partitions_array="$2"
246
247 # TODO(deymo): Read the list of partitions from the metadata. We should
248 # sanitize the list of partition names to be in [a-zA-Z0-9-]+.
249 local partitions=( "boot" "system" )
250
251 if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then
Alex Deymoc97df432015-09-25 17:23:52 -0700252 ue_config=$(create_tempfile "ue_config.XXXXXX")
253 CLEANUP_FILES+=("${ue_config}")
254 if ! unzip -p "${image}" "META/update_engine_config.txt" \
255 >"${ue_config}"; then
256 warn "No update_engine_config.txt found. Assuming pre-release image, \
257using payload minor version 2"
258 fi
259 FORCE_MINOR_VERSION=$(read_option_uint "${ue_config}" \
260 "PAYLOAD_MINOR_VERSION" 2)
Alex Deymo48b502a2015-09-17 19:00:18 -0700261 fi
262
263 local part part_file temp_raw filesize
264 for part in "${partitions[@]}"; do
265 part_file=$(create_tempfile "${part}.img.XXXXXX")
266 CLEANUP_FILES+=("${part_file}")
267 unzip -p "${image}" "IMAGES/${part}.img" >"${part_file}"
268
269 # If the partition is stored as an Android sparse image file, we need to
270 # convert them to a raw image for the update.
271 local magic=$(head --bytes=4 "${part_file}" | hexdump -e '1/1 "%.2x"')
272 if [[ "${magic}" == "3aff26ed" ]]; then
273 temp_raw=$(create_tempfile "${part}.raw.XXXXXX")
274 CLEANUP_FILES+=("${temp_raw}")
275 echo "Converting Android sparse image ${part}.img to RAW."
276 simg2img "${part_file}" "${temp_raw}"
277 # At this point, we can drop the contents of the old part_file file, but
278 # we can't delete the file because it will be deleted in cleanup.
279 true >"${part_file}"
280 part_file="${temp_raw}"
281 fi
282
283 # delta_generator only supports images multiple of 4 KiB, so we pad with
284 # zeros if needed.
285 filesize=$(stat -c%s "${part_file}")
286 if [[ $(( filesize % 4096 )) -ne 0 ]]; then
287 echo "Rounding up partition ${part}.img to multiple of 4 KiB."
288 : $(( filesize = (filesize + 4095) & -4096 ))
289 truncate --size="${filesize}" "${part_file}"
290 fi
291
292 eval "${partitions_array}[\"${part}\"]=\"${part_file}\""
293 echo "Extracted ${partitions_array}[${part}]: ${filesize} bytes"
294 done
295}
296
Jason Kusumabe998f42015-09-03 15:53:13 -0700297validate_generate() {
298 [[ -n "${FLAGS_payload}" ]] ||
299 die "Error: you must specify an output filename with --payload FILENAME"
300
301 [[ -n "${FLAGS_target_image}" ]] ||
302 die "Error: you must specify a target image with --target_image FILENAME"
303}
304
305cmd_generate() {
Alex Deymo89ff9e32015-09-15 19:29:01 -0700306 local payload_type="delta"
Jason Kusumabe998f42015-09-03 15:53:13 -0700307 if [[ -z "${FLAGS_source_image}" ]]; then
Alex Deymo89ff9e32015-09-15 19:29:01 -0700308 payload_type="full"
Jason Kusumabe998f42015-09-03 15:53:13 -0700309 fi
310
Alex Deymo48b502a2015-09-17 19:00:18 -0700311 echo "Extracting images for ${payload_type} update."
Jason Kusumabe998f42015-09-03 15:53:13 -0700312
Alex Deymo48b502a2015-09-17 19:00:18 -0700313 extract_image "${FLAGS_target_image}" DST_PARTITIONS
Alex Deymo89ff9e32015-09-15 19:29:01 -0700314 if [[ "${payload_type}" == "delta" ]]; then
Alex Deymo48b502a2015-09-17 19:00:18 -0700315 extract_image "${FLAGS_source_image}" SRC_PARTITIONS
Jason Kusumabe998f42015-09-03 15:53:13 -0700316 fi
317
Alex Deymo48b502a2015-09-17 19:00:18 -0700318 echo "Generating ${payload_type} update."
Jason Kusumabe998f42015-09-03 15:53:13 -0700319 GENERATOR_ARGS=(
320 # Common payload args:
321 -out_file="${FLAGS_payload}"
322 # Target image args:
Alex Deymo89ff9e32015-09-15 19:29:01 -0700323 # TODO(deymo): Pass the list of partitions to the generator.
Alex Deymo48b502a2015-09-17 19:00:18 -0700324 -new_image="${DST_PARTITIONS[system]}"
325 -new_kernel="${DST_PARTITIONS[boot]}"
Jason Kusumabe998f42015-09-03 15:53:13 -0700326 )
327
Alex Deymo89ff9e32015-09-15 19:29:01 -0700328 if [[ "${payload_type}" == "delta" ]]; then
Jason Kusumabe998f42015-09-03 15:53:13 -0700329 GENERATOR_ARGS+=(
330 # Source image args:
Alex Deymo48b502a2015-09-17 19:00:18 -0700331 -old_image="${SRC_PARTITIONS[system]}"
332 -old_kernel="${SRC_PARTITIONS[boot]}"
Jason Kusumabe998f42015-09-03 15:53:13 -0700333 )
Alex Deymo48b502a2015-09-17 19:00:18 -0700334 if [[ -n "${FORCE_MINOR_VERSION}" ]]; then
335 GENERATOR_ARGS+=( --minor_version="${FORCE_MINOR_VERSION}" )
336 fi
337 fi
338
339 if [[ -n "${FORCE_MAJOR_VERSION}" ]]; then
340 GENERATOR_ARGS+=( --major_version="${FORCE_MAJOR_VERSION}" )
Jason Kusumabe998f42015-09-03 15:53:13 -0700341 fi
342
343 echo "Running delta_generator with args: ${GENERATOR_ARGS[@]}"
344 "${GENERATOR}" "${GENERATOR_ARGS[@]}"
345
Alex Deymo89ff9e32015-09-15 19:29:01 -0700346 echo "Done generating ${payload_type} update."
Jason Kusumabe998f42015-09-03 15:53:13 -0700347}
348
349validate_hash() {
350 [[ -n "${FLAGS_signature_size}" ]] ||
351 die "Error: you must specify signature size with --signature_size SIZES"
352
353 [[ -n "${FLAGS_unsigned_payload}" ]] ||
354 die "Error: you must specify the input unsigned payload with \
355--unsigned_payload FILENAME"
356
357 [[ -n "${FLAGS_metadata_hash_file}" ]] ||
358 [[ -n "${FLAGS_payload_hash_file}" ]] ||
359 die "Error: you must specify --metadata_hash_file FILENAME \
360or --payload_hash_file FILENAME"
361}
362
363cmd_hash() {
364 if [[ -n "${FLAGS_metadata_hash_file}" ]]; then
365 "${GENERATOR}" \
366 -in_file="${FLAGS_unsigned_payload}" \
367 -signature_size="${FLAGS_signature_size}" \
368 -out_metadata_hash_file="${FLAGS_metadata_hash_file}"
369 fi
370
371 if [[ -n "${FLAGS_payload_hash_file}" ]]; then
372 "${GENERATOR}" \
373 -in_file="${FLAGS_unsigned_payload}" \
374 -signature_size="${FLAGS_signature_size}" \
375 -out_hash_file="${FLAGS_payload_hash_file}"
376 fi
377 echo "Done generating hash."
378}
379
380validate_sign() {
381 [[ -n "${FLAGS_signature_size}" ]] ||
382 die "Error: you must specify signature size with --signature_size SIZES"
383
384 [[ -n "${FLAGS_unsigned_payload}" ]] ||
385 die "Error: you must specify the input unsigned payload with \
386--unsigned_payload FILENAME"
387
388 [[ -n "${FLAGS_payload}" ]] ||
389 die "Error: you must specify the output signed payload with \
390--payload FILENAME"
391
392 [[ -n "${FLAGS_payload_signature_file}" ]] ||
393 die "Error: you must specify the payload signature file with \
394--payload_signature_file SIGNATURES"
Alex Deymo89ff9e32015-09-15 19:29:01 -0700395
396 [[ -n "${FLAGS_metadata_signature_file}" ]] ||
397 die "Error: you must specify the metadata signature file with \
398--metadata_signature_file SIGNATURES"
Jason Kusumabe998f42015-09-03 15:53:13 -0700399}
400
401cmd_sign() {
402 "${GENERATOR}" \
403 -in_file="${FLAGS_unsigned_payload}" \
404 -signature_size="${FLAGS_signature_size}" \
405 -signature_file="${FLAGS_payload_signature_file}" \
Alex Deymo89ff9e32015-09-15 19:29:01 -0700406 -metadata_signature_file="${FLAGS_metadata_signature_file}" \
Jason Kusumabe998f42015-09-03 15:53:13 -0700407 -out_file="${FLAGS_payload}"
408 echo "Done signing payload."
409}
410
411# TODO: Extract the input zip files once the format is finalized
412
413# Sanity check that the real generator exists:
414GENERATOR="$(which delta_generator)"
415[[ -x "${GENERATOR}" ]] || die "can't find delta_generator"
416
417case "$COMMAND" in
418 generate) validate_generate
419 cmd_generate
420 ;;
421 hash) validate_hash
422 cmd_hash
423 ;;
424 sign) validate_sign
425 cmd_sign
426 ;;
427esac