blob: 156f659251eecc624c4ca594d101c22ac3af85c3 [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 Deymo89ff9e32015-09-15 19:29:01 -0700124# Create a temporary file in the work_dir with an optional pattern name.
125# Prints the name of the newly created file.
126create_tempfile() {
127 local pattern="${1:-tempfile.XXXXXX}"
128 mktemp --tmpdir="${FLAGS_work_dir}" "${pattern}"
129}
Jason Kusumabe998f42015-09-03 15:53:13 -0700130
131cleanup() {
132 local err=""
Alex Deymo89ff9e32015-09-15 19:29:01 -0700133 rm -f "${CLEANUP_FILES[@]}" || err=1
Jason Kusumabe998f42015-09-03 15:53:13 -0700134
135 # If we are cleaning up after an error, or if we got an error during
136 # cleanup (even if we eventually succeeded) return a non-zero exit
137 # code. This triggers additional logging in most environments that call
138 # this script.
139 if [[ -n "${err}" ]]; then
140 die "Cleanup encountered an error."
141 fi
142}
143
144cleanup_on_error() {
145 trap - INT TERM ERR EXIT
146 cleanup
147 die "Cleanup success after an error."
148}
149
150cleanup_on_exit() {
151 trap - INT TERM ERR EXIT
152 cleanup
153}
154
155trap cleanup_on_error INT TERM ERR
156trap cleanup_on_exit EXIT
157
Alex Deymo48b502a2015-09-17 19:00:18 -0700158
159# extract_image <image> <partitions_array>
160#
161# Detect the format of the |image| file and extract its updatable partitions
162# into new temporary files. Add the list of partition names and its files to the
163# associative array passed in |partitions_array|.
164extract_image() {
165 local image="$1"
166
167 # Brillo images are zip files. We detect the 4-byte magic header of the zip
168 # file.
169 local magic=$(head --bytes=4 "${image}" | hexdump -e '1/1 "%.2x"')
170 if [[ "${magic}" == "504b0304" ]]; then
171 echo "Detected .zip file, extracting Brillo image."
172 extract_image_brillo "$@"
173 return
174 fi
175
176 # Chrome OS images are GPT partitioned disks. We should have the cgpt binary
177 # bundled here and we will use it to extract the partitions, so the GPT
178 # headers must be valid.
179 if cgpt show -q -n "${image}" >/dev/null; then
180 echo "Detected GPT image, extracting Chrome OS image."
181 extract_image_cros "$@"
182 return
183 fi
184
185 die "Couldn't detect the image format of ${image}"
186}
187
Alex Deymo89ff9e32015-09-15 19:29:01 -0700188# extract_image_cros <image.bin> <partitions_array>
189#
Alex Deymo48b502a2015-09-17 19:00:18 -0700190# Extract Chromium OS recovery images into new temporary files.
Alex Deymo89ff9e32015-09-15 19:29:01 -0700191extract_image_cros() {
192 local image="$1"
193 local partitions_array="$2"
194
195 local kernel root
196 kernel=$(create_tempfile "kernel.bin.XXXXXX")
197 CLEANUP_FILES+=("${kernel}")
198 root=$(create_tempfile "root.bin.XXXXXX")
199 CLEANUP_FILES+=("${root}")
200
201 cros_generate_update_payload --extract \
202 --image "${image}" \
203 --kern_path "${kernel}" --root_path "${root}" \
204 --work_dir "${FLAGS_work_dir}" --outside_chroot
205
Alex Deymo48b502a2015-09-17 19:00:18 -0700206 # When generating legacy Chrome OS images, we need to use "boot" and "system"
207 # for the partition names to be compatible with updating Brillo devices with
208 # Chrome OS images.
209 eval ${partitions_array}[boot]=\""${kernel}"\"
210 eval ${partitions_array}[system]=\""${root}"\"
Alex Deymo89ff9e32015-09-15 19:29:01 -0700211
212 local part varname
Alex Deymo48b502a2015-09-17 19:00:18 -0700213 for part in boot system; do
Alex Deymo89ff9e32015-09-15 19:29:01 -0700214 varname="${partitions_array}[${part}]"
215 printf "md5sum of %s: " "${varname}"
216 md5sum "${!varname}"
217 done
218}
219
Alex Deymo48b502a2015-09-17 19:00:18 -0700220# extract_image_brillo <target_files.zip> <partitions_array>
221#
222# Extract the A/B updated partitions from a Brillo target_files zip file into
223# new temporary files.
224extract_image_brillo() {
225 local image="$1"
226 local partitions_array="$2"
227
228 # TODO(deymo): Read the list of partitions from the metadata. We should
229 # sanitize the list of partition names to be in [a-zA-Z0-9-]+.
230 local partitions=( "boot" "system" )
231
232 if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then
233 # TODO(deymo): Read the supported minor version from the .zip metadata.
234 FORCE_MINOR_VERSION="2"
235 fi
236
237 local part part_file temp_raw filesize
238 for part in "${partitions[@]}"; do
239 part_file=$(create_tempfile "${part}.img.XXXXXX")
240 CLEANUP_FILES+=("${part_file}")
241 unzip -p "${image}" "IMAGES/${part}.img" >"${part_file}"
242
243 # If the partition is stored as an Android sparse image file, we need to
244 # convert them to a raw image for the update.
245 local magic=$(head --bytes=4 "${part_file}" | hexdump -e '1/1 "%.2x"')
246 if [[ "${magic}" == "3aff26ed" ]]; then
247 temp_raw=$(create_tempfile "${part}.raw.XXXXXX")
248 CLEANUP_FILES+=("${temp_raw}")
249 echo "Converting Android sparse image ${part}.img to RAW."
250 simg2img "${part_file}" "${temp_raw}"
251 # At this point, we can drop the contents of the old part_file file, but
252 # we can't delete the file because it will be deleted in cleanup.
253 true >"${part_file}"
254 part_file="${temp_raw}"
255 fi
256
257 # delta_generator only supports images multiple of 4 KiB, so we pad with
258 # zeros if needed.
259 filesize=$(stat -c%s "${part_file}")
260 if [[ $(( filesize % 4096 )) -ne 0 ]]; then
261 echo "Rounding up partition ${part}.img to multiple of 4 KiB."
262 : $(( filesize = (filesize + 4095) & -4096 ))
263 truncate --size="${filesize}" "${part_file}"
264 fi
265
266 eval "${partitions_array}[\"${part}\"]=\"${part_file}\""
267 echo "Extracted ${partitions_array}[${part}]: ${filesize} bytes"
268 done
269}
270
Jason Kusumabe998f42015-09-03 15:53:13 -0700271validate_generate() {
272 [[ -n "${FLAGS_payload}" ]] ||
273 die "Error: you must specify an output filename with --payload FILENAME"
274
275 [[ -n "${FLAGS_target_image}" ]] ||
276 die "Error: you must specify a target image with --target_image FILENAME"
277}
278
279cmd_generate() {
Alex Deymo89ff9e32015-09-15 19:29:01 -0700280 local payload_type="delta"
Jason Kusumabe998f42015-09-03 15:53:13 -0700281 if [[ -z "${FLAGS_source_image}" ]]; then
Alex Deymo89ff9e32015-09-15 19:29:01 -0700282 payload_type="full"
Jason Kusumabe998f42015-09-03 15:53:13 -0700283 fi
284
Alex Deymo48b502a2015-09-17 19:00:18 -0700285 echo "Extracting images for ${payload_type} update."
Jason Kusumabe998f42015-09-03 15:53:13 -0700286
Alex Deymo48b502a2015-09-17 19:00:18 -0700287 extract_image "${FLAGS_target_image}" DST_PARTITIONS
Alex Deymo89ff9e32015-09-15 19:29:01 -0700288 if [[ "${payload_type}" == "delta" ]]; then
Alex Deymo48b502a2015-09-17 19:00:18 -0700289 extract_image "${FLAGS_source_image}" SRC_PARTITIONS
Jason Kusumabe998f42015-09-03 15:53:13 -0700290 fi
291
Alex Deymo48b502a2015-09-17 19:00:18 -0700292 echo "Generating ${payload_type} update."
Jason Kusumabe998f42015-09-03 15:53:13 -0700293 GENERATOR_ARGS=(
294 # Common payload args:
295 -out_file="${FLAGS_payload}"
296 # Target image args:
Alex Deymo89ff9e32015-09-15 19:29:01 -0700297 # TODO(deymo): Pass the list of partitions to the generator.
Alex Deymo48b502a2015-09-17 19:00:18 -0700298 -new_image="${DST_PARTITIONS[system]}"
299 -new_kernel="${DST_PARTITIONS[boot]}"
Jason Kusumabe998f42015-09-03 15:53:13 -0700300 )
301
Alex Deymo89ff9e32015-09-15 19:29:01 -0700302 if [[ "${payload_type}" == "delta" ]]; then
Jason Kusumabe998f42015-09-03 15:53:13 -0700303 GENERATOR_ARGS+=(
304 # Source image args:
Alex Deymo48b502a2015-09-17 19:00:18 -0700305 -old_image="${SRC_PARTITIONS[system]}"
306 -old_kernel="${SRC_PARTITIONS[boot]}"
Jason Kusumabe998f42015-09-03 15:53:13 -0700307 )
Alex Deymo48b502a2015-09-17 19:00:18 -0700308 if [[ -n "${FORCE_MINOR_VERSION}" ]]; then
309 GENERATOR_ARGS+=( --minor_version="${FORCE_MINOR_VERSION}" )
310 fi
311 fi
312
313 if [[ -n "${FORCE_MAJOR_VERSION}" ]]; then
314 GENERATOR_ARGS+=( --major_version="${FORCE_MAJOR_VERSION}" )
Jason Kusumabe998f42015-09-03 15:53:13 -0700315 fi
316
317 echo "Running delta_generator with args: ${GENERATOR_ARGS[@]}"
318 "${GENERATOR}" "${GENERATOR_ARGS[@]}"
319
Alex Deymo89ff9e32015-09-15 19:29:01 -0700320 echo "Done generating ${payload_type} update."
Jason Kusumabe998f42015-09-03 15:53:13 -0700321}
322
323validate_hash() {
324 [[ -n "${FLAGS_signature_size}" ]] ||
325 die "Error: you must specify signature size with --signature_size SIZES"
326
327 [[ -n "${FLAGS_unsigned_payload}" ]] ||
328 die "Error: you must specify the input unsigned payload with \
329--unsigned_payload FILENAME"
330
331 [[ -n "${FLAGS_metadata_hash_file}" ]] ||
332 [[ -n "${FLAGS_payload_hash_file}" ]] ||
333 die "Error: you must specify --metadata_hash_file FILENAME \
334or --payload_hash_file FILENAME"
335}
336
337cmd_hash() {
338 if [[ -n "${FLAGS_metadata_hash_file}" ]]; then
339 "${GENERATOR}" \
340 -in_file="${FLAGS_unsigned_payload}" \
341 -signature_size="${FLAGS_signature_size}" \
342 -out_metadata_hash_file="${FLAGS_metadata_hash_file}"
343 fi
344
345 if [[ -n "${FLAGS_payload_hash_file}" ]]; then
346 "${GENERATOR}" \
347 -in_file="${FLAGS_unsigned_payload}" \
348 -signature_size="${FLAGS_signature_size}" \
349 -out_hash_file="${FLAGS_payload_hash_file}"
350 fi
351 echo "Done generating hash."
352}
353
354validate_sign() {
355 [[ -n "${FLAGS_signature_size}" ]] ||
356 die "Error: you must specify signature size with --signature_size SIZES"
357
358 [[ -n "${FLAGS_unsigned_payload}" ]] ||
359 die "Error: you must specify the input unsigned payload with \
360--unsigned_payload FILENAME"
361
362 [[ -n "${FLAGS_payload}" ]] ||
363 die "Error: you must specify the output signed payload with \
364--payload FILENAME"
365
366 [[ -n "${FLAGS_payload_signature_file}" ]] ||
367 die "Error: you must specify the payload signature file with \
368--payload_signature_file SIGNATURES"
Alex Deymo89ff9e32015-09-15 19:29:01 -0700369
370 [[ -n "${FLAGS_metadata_signature_file}" ]] ||
371 die "Error: you must specify the metadata signature file with \
372--metadata_signature_file SIGNATURES"
Jason Kusumabe998f42015-09-03 15:53:13 -0700373}
374
375cmd_sign() {
376 "${GENERATOR}" \
377 -in_file="${FLAGS_unsigned_payload}" \
378 -signature_size="${FLAGS_signature_size}" \
379 -signature_file="${FLAGS_payload_signature_file}" \
Alex Deymo89ff9e32015-09-15 19:29:01 -0700380 -metadata_signature_file="${FLAGS_metadata_signature_file}" \
Jason Kusumabe998f42015-09-03 15:53:13 -0700381 -out_file="${FLAGS_payload}"
382 echo "Done signing payload."
383}
384
385# TODO: Extract the input zip files once the format is finalized
386
387# Sanity check that the real generator exists:
388GENERATOR="$(which delta_generator)"
389[[ -x "${GENERATOR}" ]] || die "can't find delta_generator"
390
391case "$COMMAND" in
392 generate) validate_generate
393 cmd_generate
394 ;;
395 hash) validate_hash
396 cmd_hash
397 ;;
398 sign) validate_sign
399 cmd_sign
400 ;;
401esac