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