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 # truncate_file <file_path> <file_size>
    223 #
    224 # Truncate the given |file_path| to |file_size| using perl.
    225 # The truncate binary might not be available.
    226 truncate_file() {
    227   local file_path="$1"
    228   local file_size="$2"
    229   perl -e "open(FILE, \"+<\", \$ARGV[0]); \
    230            truncate(FILE, ${file_size}); \
    231            close(FILE);" "${file_path}"
    232 }
    233 
    234 # Create a temporary file in the work_dir with an optional pattern name.
    235 # Prints the name of the newly created file.
    236 create_tempfile() {
    237   local pattern="${1:-tempfile.XXXXXX}"
    238   mktemp --tmpdir="${FLAGS_work_dir}" "${pattern}"
    239 }
    240 
    241 cleanup() {
    242   local err=""
    243   rm -f "${CLEANUP_FILES[@]}" || err=1
    244 
    245   # If we are cleaning up after an error, or if we got an error during
    246   # cleanup (even if we eventually succeeded) return a non-zero exit
    247   # code. This triggers additional logging in most environments that call
    248   # this script.
    249   if [[ -n "${err}" ]]; then
    250     die "Cleanup encountered an error."
    251   fi
    252 }
    253 
    254 cleanup_on_error() {
    255   trap - INT TERM ERR EXIT
    256   cleanup
    257   die "Cleanup success after an error."
    258 }
    259 
    260 cleanup_on_exit() {
    261   trap - INT TERM ERR EXIT
    262   cleanup
    263 }
    264 
    265 trap cleanup_on_error INT TERM ERR
    266 trap cleanup_on_exit EXIT
    267 
    268 
    269 # extract_image <image> <partitions_array>
    270 #
    271 # Detect the format of the |image| file and extract its updatable partitions
    272 # into new temporary files. Add the list of partition names and its files to the
    273 # associative array passed in |partitions_array|.
    274 extract_image() {
    275   local image="$1"
    276 
    277   # Brillo images are zip files. We detect the 4-byte magic header of the zip
    278   # file.
    279   local magic=$(head --bytes=4 "${image}" | hexdump -e '1/1 "%.2x"')
    280   if [[ "${magic}" == "504b0304" ]]; then
    281     echo "Detected .zip file, extracting Brillo image."
    282     extract_image_brillo "$@"
    283     return
    284   fi
    285 
    286   # Chrome OS images are GPT partitioned disks. We should have the cgpt binary
    287   # bundled here and we will use it to extract the partitions, so the GPT
    288   # headers must be valid.
    289   if cgpt show -q -n "${image}" >/dev/null; then
    290     echo "Detected GPT image, extracting Chrome OS image."
    291     extract_image_cros "$@"
    292     return
    293   fi
    294 
    295   die "Couldn't detect the image format of ${image}"
    296 }
    297 
    298 # extract_image_cros <image.bin> <partitions_array>
    299 #
    300 # Extract Chromium OS recovery images into new temporary files.
    301 extract_image_cros() {
    302   local image="$1"
    303   local partitions_array="$2"
    304 
    305   local kernel root
    306   kernel=$(create_tempfile "kernel.bin.XXXXXX")
    307   CLEANUP_FILES+=("${kernel}")
    308   root=$(create_tempfile "root.bin.XXXXXX")
    309   CLEANUP_FILES+=("${root}")
    310 
    311   cros_generate_update_payload --extract \
    312     --image "${image}" \
    313     --kern_path "${kernel}" --root_path "${root}" \
    314     --work_dir "${FLAGS_work_dir}" --outside_chroot
    315 
    316   # Chrome OS uses major_version 1 payloads for all versions, even if the
    317   # updater supports a newer major version.
    318   FORCE_MAJOR_VERSION="1"
    319 
    320   if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then
    321     # Copy from zlib_fingerprint in source image to stdout.
    322     ZLIB_FINGERPRINT=$(e2cp "${root}":/etc/zlib_fingerprint -)
    323   fi
    324 
    325   # When generating legacy Chrome OS images, we need to use "boot" and "system"
    326   # for the partition names to be compatible with updating Brillo devices with
    327   # Chrome OS images.
    328   eval ${partitions_array}[boot]=\""${kernel}"\"
    329   eval ${partitions_array}[system]=\""${root}"\"
    330 
    331   local part varname
    332   for part in boot system; do
    333     varname="${partitions_array}[${part}]"
    334     printf "md5sum of %s: " "${varname}"
    335     md5sum "${!varname}"
    336   done
    337 }
    338 
    339 # extract_image_brillo <target_files.zip> <partitions_array>
    340 #
    341 # Extract the A/B updated partitions from a Brillo target_files zip file into
    342 # new temporary files.
    343 extract_image_brillo() {
    344   local image="$1"
    345   local partitions_array="$2"
    346 
    347   local partitions=( "boot" "system" )
    348   local ab_partitions_list
    349   ab_partitions_list=$(create_tempfile "ab_partitions_list.XXXXXX")
    350   CLEANUP_FILES+=("${ab_partitions_list}")
    351   if unzip -p "${image}" "META/ab_partitions.txt" >"${ab_partitions_list}"; then
    352     if grep -v -E '^[a-zA-Z0-9_-]*$' "${ab_partitions_list}" >&2; then
    353       die "Invalid partition names found in the partition list."
    354     fi
    355     partitions=($(cat "${ab_partitions_list}"))
    356     if [[ ${#partitions[@]} -eq 0 ]]; then
    357       die "The list of partitions is empty. Can't generate a payload."
    358     fi
    359   else
    360     warn "No ab_partitions.txt found. Using default."
    361   fi
    362   echo "List of A/B partitions: ${partitions[@]}"
    363 
    364   # All Brillo updaters support major version 2.
    365   FORCE_MAJOR_VERSION="2"
    366 
    367   if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then
    368     # Source image
    369     local ue_config=$(create_tempfile "ue_config.XXXXXX")
    370     CLEANUP_FILES+=("${ue_config}")
    371     if ! unzip -p "${image}" "META/update_engine_config.txt" \
    372         >"${ue_config}"; then
    373       warn "No update_engine_config.txt found. Assuming pre-release image, \
    374 using payload minor version 2"
    375     fi
    376     # For delta payloads, we use the major and minor version supported by the
    377     # old updater.
    378     FORCE_MINOR_VERSION=$(read_option_uint "${ue_config}" \
    379       "PAYLOAD_MINOR_VERSION" 2)
    380     FORCE_MAJOR_VERSION=$(read_option_uint "${ue_config}" \
    381       "PAYLOAD_MAJOR_VERSION" 2)
    382 
    383     # Brillo support for deltas started with minor version 3.
    384     if [[ "${FORCE_MINOR_VERSION}" -le 2 ]]; then
    385       warn "No delta support from minor version ${FORCE_MINOR_VERSION}. \
    386 Disabling deltas for this source version."
    387       exit ${EX_UNSUPPORTED_DELTA}
    388     fi
    389 
    390     if [[ "${FORCE_MINOR_VERSION}" -ge 4 ]]; then
    391       ZLIB_FINGERPRINT=$(unzip -p "${image}" "META/zlib_fingerprint.txt")
    392     fi
    393   else
    394     # Target image
    395     local postinstall_config=$(create_tempfile "postinstall_config.XXXXXX")
    396     CLEANUP_FILES+=("${postinstall_config}")
    397     if unzip -p "${image}" "META/postinstall_config.txt" \
    398         >"${postinstall_config}"; then
    399       POSTINSTALL_CONFIG_FILE="${postinstall_config}"
    400     fi
    401   fi
    402 
    403   local part part_file temp_raw filesize
    404   for part in "${partitions[@]}"; do
    405     part_file=$(create_tempfile "${part}.img.XXXXXX")
    406     CLEANUP_FILES+=("${part_file}")
    407     unzip -p "${image}" "IMAGES/${part}.img" >"${part_file}"
    408 
    409     # If the partition is stored as an Android sparse image file, we need to
    410     # convert them to a raw image for the update.
    411     local magic=$(head --bytes=4 "${part_file}" | hexdump -e '1/1 "%.2x"')
    412     if [[ "${magic}" == "3aff26ed" ]]; then
    413       temp_raw=$(create_tempfile "${part}.raw.XXXXXX")
    414       CLEANUP_FILES+=("${temp_raw}")
    415       echo "Converting Android sparse image ${part}.img to RAW."
    416       simg2img "${part_file}" "${temp_raw}"
    417       # At this point, we can drop the contents of the old part_file file, but
    418       # we can't delete the file because it will be deleted in cleanup.
    419       true >"${part_file}"
    420       part_file="${temp_raw}"
    421     fi
    422 
    423     # delta_generator only supports images multiple of 4 KiB. For target images
    424     # we pad the data with zeros if needed, but for source images we truncate
    425     # down the data since the last block of the old image could be padded on
    426     # disk with unknown data.
    427     filesize=$(stat -c%s "${part_file}")
    428     if [[ $(( filesize % 4096 )) -ne 0 ]]; then
    429       if [[ "${partitions_array}" == "SRC_PARTITIONS" ]]; then
    430         echo "Rounding DOWN partition ${part}.img to a multiple of 4 KiB."
    431         : $(( filesize = filesize & -4096 ))
    432       else
    433         echo "Rounding UP partition ${part}.img to a multiple of 4 KiB."
    434         : $(( filesize = (filesize + 4095) & -4096 ))
    435       fi
    436       truncate_file "${part_file}" "${filesize}"
    437     fi
    438 
    439     eval "${partitions_array}[\"${part}\"]=\"${part_file}\""
    440     echo "Extracted ${partitions_array}[${part}]: ${filesize} bytes"
    441   done
    442 }
    443 
    444 validate_generate() {
    445   [[ -n "${FLAGS_payload}" ]] ||
    446     die "Error: you must specify an output filename with --payload FILENAME"
    447 
    448   [[ -n "${FLAGS_target_image}" ]] ||
    449     die "Error: you must specify a target image with --target_image FILENAME"
    450 }
    451 
    452 cmd_generate() {
    453   local payload_type="delta"
    454   if [[ -z "${FLAGS_source_image}" ]]; then
    455     payload_type="full"
    456   fi
    457 
    458   echo "Extracting images for ${payload_type} update."
    459 
    460   extract_image "${FLAGS_target_image}" DST_PARTITIONS
    461   if [[ "${payload_type}" == "delta" ]]; then
    462     extract_image "${FLAGS_source_image}" SRC_PARTITIONS
    463   fi
    464 
    465   echo "Generating ${payload_type} update."
    466   # Common payload args:
    467   GENERATOR_ARGS=( -out_file="${FLAGS_payload}" )
    468 
    469   local part old_partitions="" new_partitions="" partition_names=""
    470   for part in "${!DST_PARTITIONS[@]}"; do
    471     if [[ -n "${partition_names}" ]]; then
    472       partition_names+=":"
    473       new_partitions+=":"
    474       old_partitions+=":"
    475     fi
    476     partition_names+="${part}"
    477     new_partitions+="${DST_PARTITIONS[${part}]}"
    478     old_partitions+="${SRC_PARTITIONS[${part}]:-}"
    479   done
    480 
    481   # Target image args:
    482   GENERATOR_ARGS+=(
    483     -partition_names="${partition_names}"
    484     -new_partitions="${new_partitions}"
    485   )
    486 
    487   if [[ "${payload_type}" == "delta" ]]; then
    488     # Source image args:
    489     GENERATOR_ARGS+=(
    490       -old_partitions="${old_partitions}"
    491     )
    492     if [[ -n "${FORCE_MINOR_VERSION}" ]]; then
    493       GENERATOR_ARGS+=( --minor_version="${FORCE_MINOR_VERSION}" )
    494     fi
    495     if [[ -n "${ZLIB_FINGERPRINT}" ]]; then
    496       GENERATOR_ARGS+=( --zlib_fingerprint="${ZLIB_FINGERPRINT}" )
    497     fi
    498   fi
    499 
    500   if [[ -n "${FORCE_MAJOR_VERSION}" ]]; then
    501     GENERATOR_ARGS+=( --major_version="${FORCE_MAJOR_VERSION}" )
    502   fi
    503 
    504   if [[ -n "${FLAGS_metadata_size_file}" ]]; then
    505     GENERATOR_ARGS+=( --out_metadata_size_file="${FLAGS_metadata_size_file}" )
    506   fi
    507 
    508   if [[ -n "${POSTINSTALL_CONFIG_FILE}" ]]; then
    509     GENERATOR_ARGS+=(
    510       --new_postinstall_config_file="${POSTINSTALL_CONFIG_FILE}"
    511     )
    512   fi
    513 
    514   echo "Running delta_generator with args: ${GENERATOR_ARGS[@]}"
    515   "${GENERATOR}" "${GENERATOR_ARGS[@]}"
    516 
    517   echo "Done generating ${payload_type} update."
    518 }
    519 
    520 validate_hash() {
    521   [[ -n "${FLAGS_signature_size}" ]] ||
    522     die "Error: you must specify signature size with --signature_size SIZES"
    523 
    524   [[ -n "${FLAGS_unsigned_payload}" ]] ||
    525     die "Error: you must specify the input unsigned payload with \
    526 --unsigned_payload FILENAME"
    527 
    528   [[ -n "${FLAGS_payload_hash_file}" ]] ||
    529     die "Error: you must specify --payload_hash_file FILENAME"
    530 
    531   [[ -n "${FLAGS_metadata_hash_file}" ]] ||
    532     die "Error: you must specify --metadata_hash_file FILENAME"
    533 }
    534 
    535 cmd_hash() {
    536   "${GENERATOR}" \
    537       -in_file="${FLAGS_unsigned_payload}" \
    538       -signature_size="${FLAGS_signature_size}" \
    539       -out_hash_file="${FLAGS_payload_hash_file}" \
    540       -out_metadata_hash_file="${FLAGS_metadata_hash_file}"
    541 
    542   echo "Done generating hash."
    543 }
    544 
    545 validate_sign() {
    546   [[ -n "${FLAGS_signature_size}" ]] ||
    547     die "Error: you must specify signature size with --signature_size SIZES"
    548 
    549   [[ -n "${FLAGS_unsigned_payload}" ]] ||
    550     die "Error: you must specify the input unsigned payload with \
    551 --unsigned_payload FILENAME"
    552 
    553   [[ -n "${FLAGS_payload}" ]] ||
    554     die "Error: you must specify the output signed payload with \
    555 --payload FILENAME"
    556 
    557   [[ -n "${FLAGS_payload_signature_file}" ]] ||
    558     die "Error: you must specify the payload signature file with \
    559 --payload_signature_file SIGNATURES"
    560 
    561   [[ -n "${FLAGS_metadata_signature_file}" ]] ||
    562     die "Error: you must specify the metadata signature file with \
    563 --metadata_signature_file SIGNATURES"
    564 }
    565 
    566 cmd_sign() {
    567   GENERATOR_ARGS=(
    568     -in_file="${FLAGS_unsigned_payload}"
    569     -signature_size="${FLAGS_signature_size}"
    570     -signature_file="${FLAGS_payload_signature_file}"
    571     -metadata_signature_file="${FLAGS_metadata_signature_file}"
    572     -out_file="${FLAGS_payload}"
    573   )
    574 
    575   if [[ -n "${FLAGS_metadata_size_file}" ]]; then
    576     GENERATOR_ARGS+=( --out_metadata_size_file="${FLAGS_metadata_size_file}" )
    577   fi
    578 
    579   "${GENERATOR}" "${GENERATOR_ARGS[@]}"
    580   echo "Done signing payload."
    581 }
    582 
    583 validate_properties() {
    584   [[ -n "${FLAGS_payload}" ]] ||
    585     die "Error: you must specify the payload file with --payload FILENAME"
    586 
    587   [[ -n "${FLAGS_properties_file}" ]] ||
    588     die "Error: you must specify a non empty --properties_file FILENAME"
    589 }
    590 
    591 cmd_properties() {
    592   "${GENERATOR}" \
    593       -in_file="${FLAGS_payload}" \
    594       -properties_file="${FLAGS_properties_file}"
    595 }
    596 
    597 # Sanity check that the real generator exists:
    598 GENERATOR="$(which delta_generator)"
    599 [[ -x "${GENERATOR}" ]] || die "can't find delta_generator"
    600 
    601 case "$COMMAND" in
    602   generate) validate_generate
    603             cmd_generate
    604             ;;
    605   hash) validate_hash
    606         cmd_hash
    607         ;;
    608   sign) validate_sign
    609         cmd_sign
    610         ;;
    611   properties) validate_properties
    612               cmd_properties
    613               ;;
    614 esac
    615