diff options
author | Gabriele M <moto.falcon.git@gmail.com> | 2018-03-24 09:11:21 +0100 |
---|---|---|
committer | Gabriele M <moto.falcon.git@gmail.com> | 2018-03-24 17:45:00 +0100 |
commit | 8ad65076b68f4f96a81810195d347b49149d9c20 (patch) | |
tree | c196e949a1881516f4055cf515b9b24a6a4da5de | |
download | scripts-8ad65076b68f4f96a81810195d347b49149d9c20.tar.gz scripts-8ad65076b68f4f96a81810195d347b49149d9c20.tar.bz2 scripts-8ad65076b68f4f96a81810195d347b49149d9c20.zip |
Import update_payload library code
From https://android.googlesource.com/platform/system/update_engine
at ee67a8d57519112f62f439a8644ea497ab6898c2.
-rw-r--r-- | update_payload/__init__.py | 22 | ||||
-rw-r--r-- | update_payload/applier.py | 667 | ||||
-rw-r--r-- | update_payload/checker.py | 1336 | ||||
-rwxr-xr-x | update_payload/checker_unittest.py | 1364 | ||||
-rw-r--r-- | update_payload/common.py | 218 | ||||
-rw-r--r-- | update_payload/error.py | 21 | ||||
-rw-r--r-- | update_payload/format_utils.py | 109 | ||||
-rwxr-xr-x | update_payload/format_utils_unittest.py | 89 | ||||
-rw-r--r-- | update_payload/histogram.py | 129 | ||||
-rwxr-xr-x | update_payload/histogram_unittest.py | 72 | ||||
-rw-r--r-- | update_payload/payload-test-key.pem | 27 | ||||
-rw-r--r-- | update_payload/payload-test-key.pub | 9 | ||||
-rw-r--r-- | update_payload/payload.py | 336 | ||||
-rw-r--r-- | update_payload/test_utils.py | 369 | ||||
-rw-r--r-- | update_payload/update-payload-key.pub.pem | 9 | ||||
-rw-r--r-- | update_payload/update_metadata_pb2.py | 631 |
16 files changed, 5408 insertions, 0 deletions
diff --git a/update_payload/__init__.py b/update_payload/__init__.py new file mode 100644 index 0000000..8ee95e2 --- /dev/null +++ b/update_payload/__init__.py @@ -0,0 +1,22 @@ +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Library for processing, verifying and applying Chrome OS update payloads.""" + +# Just raise the interface classes to the root namespace. +from update_payload.checker import CHECKS_TO_DISABLE +from update_payload.error import PayloadError +from update_payload.payload import Payload diff --git a/update_payload/applier.py b/update_payload/applier.py new file mode 100644 index 0000000..9582b3d --- /dev/null +++ b/update_payload/applier.py @@ -0,0 +1,667 @@ +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Applying a Chrome OS update payload. + +This module is used internally by the main Payload class for applying an update +payload. The interface for invoking the applier is as follows: + + applier = PayloadApplier(payload) + applier.Run(...) + +""" + +from __future__ import print_function + +import array +import bz2 +import hashlib +import itertools +# Not everywhere we can have the lzma library so we ignore it if we didn't have +# it because it is not going to be used. For example, 'cros flash' uses +# devserver code which eventually loads this file, but the lzma library is not +# included in the client test devices, and it is not necessary to do so. But +# lzma is not used in 'cros flash' so it should be fine. Python 3.x include +# lzma, but for backward compatibility with Python 2.7, backports-lzma is +# needed. +try: + import lzma +except ImportError: + try: + from backports import lzma + except ImportError: + pass +import os +import shutil +import subprocess +import sys +import tempfile + +from update_payload import common +from update_payload.error import PayloadError + + +# +# Helper functions. +# +def _VerifySha256(file_obj, expected_hash, name, length=-1): + """Verifies the SHA256 hash of a file. + + Args: + file_obj: file object to read + expected_hash: the hash digest we expect to be getting + name: name string of this hash, for error reporting + length: precise length of data to verify (optional) + + Raises: + PayloadError if computed hash doesn't match expected one, or if fails to + read the specified length of data. + """ + hasher = hashlib.sha256() + block_length = 1024 * 1024 + max_length = length if length >= 0 else sys.maxint + + while max_length > 0: + read_length = min(max_length, block_length) + data = file_obj.read(read_length) + if not data: + break + max_length -= len(data) + hasher.update(data) + + if length >= 0 and max_length > 0: + raise PayloadError( + 'insufficient data (%d instead of %d) when verifying %s' % + (length - max_length, length, name)) + + actual_hash = hasher.digest() + if actual_hash != expected_hash: + raise PayloadError('%s hash (%s) not as expected (%s)' % + (name, common.FormatSha256(actual_hash), + common.FormatSha256(expected_hash))) + + +def _ReadExtents(file_obj, extents, block_size, max_length=-1): + """Reads data from file as defined by extent sequence. + + This tries to be efficient by not copying data as it is read in chunks. + + Args: + file_obj: file object + extents: sequence of block extents (offset and length) + block_size: size of each block + max_length: maximum length to read (optional) + + Returns: + A character array containing the concatenated read data. + """ + data = array.array('c') + if max_length < 0: + max_length = sys.maxint + for ex in extents: + if max_length == 0: + break + read_length = min(max_length, ex.num_blocks * block_size) + + # Fill with zeros or read from file, depending on the type of extent. + if ex.start_block == common.PSEUDO_EXTENT_MARKER: + data.extend(itertools.repeat('\0', read_length)) + else: + file_obj.seek(ex.start_block * block_size) + data.fromfile(file_obj, read_length) + + max_length -= read_length + + return data + + +def _WriteExtents(file_obj, data, extents, block_size, base_name): + """Writes data to file as defined by extent sequence. + + This tries to be efficient by not copy data as it is written in chunks. + + Args: + file_obj: file object + data: data to write + extents: sequence of block extents (offset and length) + block_size: size of each block + base_name: name string of extent sequence for error reporting + + Raises: + PayloadError when things don't add up. + """ + data_offset = 0 + data_length = len(data) + for ex, ex_name in common.ExtentIter(extents, base_name): + if not data_length: + raise PayloadError('%s: more write extents than data' % ex_name) + write_length = min(data_length, ex.num_blocks * block_size) + + # Only do actual writing if this is not a pseudo-extent. + if ex.start_block != common.PSEUDO_EXTENT_MARKER: + file_obj.seek(ex.start_block * block_size) + data_view = buffer(data, data_offset, write_length) + file_obj.write(data_view) + + data_offset += write_length + data_length -= write_length + + if data_length: + raise PayloadError('%s: more data than write extents' % base_name) + + +def _ExtentsToBspatchArg(extents, block_size, base_name, data_length=-1): + """Translates an extent sequence into a bspatch-compatible string argument. + + Args: + extents: sequence of block extents (offset and length) + block_size: size of each block + base_name: name string of extent sequence for error reporting + data_length: the actual total length of the data in bytes (optional) + + Returns: + A tuple consisting of (i) a string of the form + "off_1:len_1,...,off_n:len_n", (ii) an offset where zero padding is needed + for filling the last extent, (iii) the length of the padding (zero means no + padding is needed and the extents cover the full length of data). + + Raises: + PayloadError if data_length is too short or too long. + """ + arg = '' + pad_off = pad_len = 0 + if data_length < 0: + data_length = sys.maxint + for ex, ex_name in common.ExtentIter(extents, base_name): + if not data_length: + raise PayloadError('%s: more extents than total data length' % ex_name) + + is_pseudo = ex.start_block == common.PSEUDO_EXTENT_MARKER + start_byte = -1 if is_pseudo else ex.start_block * block_size + num_bytes = ex.num_blocks * block_size + if data_length < num_bytes: + # We're only padding a real extent. + if not is_pseudo: + pad_off = start_byte + data_length + pad_len = num_bytes - data_length + + num_bytes = data_length + + arg += '%s%d:%d' % (arg and ',', start_byte, num_bytes) + data_length -= num_bytes + + if data_length: + raise PayloadError('%s: extents not covering full data length' % base_name) + + return arg, pad_off, pad_len + + +# +# Payload application. +# +class PayloadApplier(object): + """Applying an update payload. + + This is a short-lived object whose purpose is to isolate the logic used for + applying an update payload. + """ + + def __init__(self, payload, bsdiff_in_place=True, bspatch_path=None, + puffpatch_path=None, truncate_to_expected_size=True): + """Initialize the applier. + + Args: + payload: the payload object to check + bsdiff_in_place: whether to perform BSDIFF operation in-place (optional) + bspatch_path: path to the bspatch binary (optional) + puffpatch_path: path to the puffpatch binary (optional) + truncate_to_expected_size: whether to truncate the resulting partitions + to their expected sizes, as specified in the + payload (optional) + """ + assert payload.is_init, 'uninitialized update payload' + self.payload = payload + self.block_size = payload.manifest.block_size + self.minor_version = payload.manifest.minor_version + self.bsdiff_in_place = bsdiff_in_place + self.bspatch_path = bspatch_path or 'bspatch' + self.puffpatch_path = puffpatch_path or 'puffin' + self.truncate_to_expected_size = truncate_to_expected_size + + def _ApplyReplaceOperation(self, op, op_name, out_data, part_file, part_size): + """Applies a REPLACE{,_BZ,_XZ} operation. + + Args: + op: the operation object + op_name: name string for error reporting + out_data: the data to be written + part_file: the partition file object + part_size: the size of the partition + + Raises: + PayloadError if something goes wrong. + """ + block_size = self.block_size + data_length = len(out_data) + + # Decompress data if needed. + if op.type == common.OpType.REPLACE_BZ: + out_data = bz2.decompress(out_data) + data_length = len(out_data) + elif op.type == common.OpType.REPLACE_XZ: + # pylint: disable=no-member + out_data = lzma.decompress(out_data) + data_length = len(out_data) + + # Write data to blocks specified in dst extents. + data_start = 0 + for ex, ex_name in common.ExtentIter(op.dst_extents, + '%s.dst_extents' % op_name): + start_block = ex.start_block + num_blocks = ex.num_blocks + count = num_blocks * block_size + + # Make sure it's not a fake (signature) operation. + if start_block != common.PSEUDO_EXTENT_MARKER: + data_end = data_start + count + + # Make sure we're not running past partition boundary. + if (start_block + num_blocks) * block_size > part_size: + raise PayloadError( + '%s: extent (%s) exceeds partition size (%d)' % + (ex_name, common.FormatExtent(ex, block_size), + part_size)) + + # Make sure that we have enough data to write. + if data_end >= data_length + block_size: + raise PayloadError( + '%s: more dst blocks than data (even with padding)') + + # Pad with zeros if necessary. + if data_end > data_length: + padding = data_end - data_length + out_data += '\0' * padding + + self.payload.payload_file.seek(start_block * block_size) + part_file.seek(start_block * block_size) + part_file.write(out_data[data_start:data_end]) + + data_start += count + + # Make sure we wrote all data. + if data_start < data_length: + raise PayloadError('%s: wrote fewer bytes (%d) than expected (%d)' % + (op_name, data_start, data_length)) + + def _ApplyMoveOperation(self, op, op_name, part_file): + """Applies a MOVE operation. + + Note that this operation must read the whole block data from the input and + only then dump it, due to our in-place update semantics; otherwise, it + might clobber data midway through. + + Args: + op: the operation object + op_name: name string for error reporting + part_file: the partition file object + + Raises: + PayloadError if something goes wrong. + """ + block_size = self.block_size + + # Gather input raw data from src extents. + in_data = _ReadExtents(part_file, op.src_extents, block_size) + + # Dump extracted data to dst extents. + _WriteExtents(part_file, in_data, op.dst_extents, block_size, + '%s.dst_extents' % op_name) + + def _ApplyZeroOperation(self, op, op_name, part_file): + """Applies a ZERO operation. + + Args: + op: the operation object + op_name: name string for error reporting + part_file: the partition file object + + Raises: + PayloadError if something goes wrong. + """ + block_size = self.block_size + base_name = '%s.dst_extents' % op_name + + # Iterate over the extents and write zero. + # pylint: disable=unused-variable + for ex, ex_name in common.ExtentIter(op.dst_extents, base_name): + # Only do actual writing if this is not a pseudo-extent. + if ex.start_block != common.PSEUDO_EXTENT_MARKER: + part_file.seek(ex.start_block * block_size) + part_file.write('\0' * (ex.num_blocks * block_size)) + + def _ApplySourceCopyOperation(self, op, op_name, old_part_file, + new_part_file): + """Applies a SOURCE_COPY operation. + + Args: + op: the operation object + op_name: name string for error reporting + old_part_file: the old partition file object + new_part_file: the new partition file object + + Raises: + PayloadError if something goes wrong. + """ + if not old_part_file: + raise PayloadError( + '%s: no source partition file provided for operation type (%d)' % + (op_name, op.type)) + + block_size = self.block_size + + # Gather input raw data from src extents. + in_data = _ReadExtents(old_part_file, op.src_extents, block_size) + + # Dump extracted data to dst extents. + _WriteExtents(new_part_file, in_data, op.dst_extents, block_size, + '%s.dst_extents' % op_name) + + def _BytesInExtents(self, extents, base_name): + """Counts the length of extents in bytes. + + Args: + extents: The list of Extents. + base_name: For error reporting. + + Returns: + The number of bytes in extents. + """ + + length = 0 + # pylint: disable=unused-variable + for ex, ex_name in common.ExtentIter(extents, base_name): + length += ex.num_blocks * self.block_size + return length + + def _ApplyDiffOperation(self, op, op_name, patch_data, old_part_file, + new_part_file): + """Applies a SOURCE_BSDIFF, BROTLI_BSDIFF or PUFFDIFF operation. + + Args: + op: the operation object + op_name: name string for error reporting + patch_data: the binary patch content + old_part_file: the source partition file object + new_part_file: the target partition file object + + Raises: + PayloadError if something goes wrong. + """ + if not old_part_file: + raise PayloadError( + '%s: no source partition file provided for operation type (%d)' % + (op_name, op.type)) + + block_size = self.block_size + + # Dump patch data to file. + with tempfile.NamedTemporaryFile(delete=False) as patch_file: + patch_file_name = patch_file.name + patch_file.write(patch_data) + + if (hasattr(new_part_file, 'fileno') and + ((not old_part_file) or hasattr(old_part_file, 'fileno'))): + # Construct input and output extents argument for bspatch. + + in_extents_arg, _, _ = _ExtentsToBspatchArg( + op.src_extents, block_size, '%s.src_extents' % op_name, + data_length=op.src_length if op.src_length else + self._BytesInExtents(op.src_extents, "%s.src_extents")) + out_extents_arg, pad_off, pad_len = _ExtentsToBspatchArg( + op.dst_extents, block_size, '%s.dst_extents' % op_name, + data_length=op.dst_length if op.dst_length else + self._BytesInExtents(op.dst_extents, "%s.dst_extents")) + + new_file_name = '/dev/fd/%d' % new_part_file.fileno() + # Diff from source partition. + old_file_name = '/dev/fd/%d' % old_part_file.fileno() + + if op.type in (common.OpType.BSDIFF, common.OpType.SOURCE_BSDIFF, + common.OpType.BROTLI_BSDIFF): + # Invoke bspatch on partition file with extents args. + bspatch_cmd = [self.bspatch_path, old_file_name, new_file_name, + patch_file_name, in_extents_arg, out_extents_arg] + subprocess.check_call(bspatch_cmd) + elif op.type == common.OpType.PUFFDIFF: + # Invoke puffpatch on partition file with extents args. + puffpatch_cmd = [self.puffpatch_path, + "--operation=puffpatch", + "--src_file=%s" % old_file_name, + "--dst_file=%s" % new_file_name, + "--patch_file=%s" % patch_file_name, + "--src_extents=%s" % in_extents_arg, + "--dst_extents=%s" % out_extents_arg] + subprocess.check_call(puffpatch_cmd) + else: + raise PayloadError("Unknown operation %s", op.type) + + # Pad with zeros past the total output length. + if pad_len: + new_part_file.seek(pad_off) + new_part_file.write('\0' * pad_len) + else: + # Gather input raw data and write to a temp file. + input_part_file = old_part_file if old_part_file else new_part_file + in_data = _ReadExtents(input_part_file, op.src_extents, block_size, + max_length=op.src_length if op.src_length else + self._BytesInExtents(op.src_extents, + "%s.src_extents")) + with tempfile.NamedTemporaryFile(delete=False) as in_file: + in_file_name = in_file.name + in_file.write(in_data) + + # Allocate temporary output file. + with tempfile.NamedTemporaryFile(delete=False) as out_file: + out_file_name = out_file.name + + if op.type in (common.OpType.BSDIFF, common.OpType.SOURCE_BSDIFF, + common.OpType.BROTLI_BSDIFF): + # Invoke bspatch. + bspatch_cmd = [self.bspatch_path, in_file_name, out_file_name, + patch_file_name] + subprocess.check_call(bspatch_cmd) + elif op.type == common.OpType.PUFFDIFF: + # Invoke puffpatch. + puffpatch_cmd = [self.puffpatch_path, + "--operation=puffpatch", + "--src_file=%s" % in_file_name, + "--dst_file=%s" % out_file_name, + "--patch_file=%s" % patch_file_name] + subprocess.check_call(puffpatch_cmd) + else: + raise PayloadError("Unknown operation %s", op.type) + + # Read output. + with open(out_file_name, 'rb') as out_file: + out_data = out_file.read() + if len(out_data) != op.dst_length: + raise PayloadError( + '%s: actual patched data length (%d) not as expected (%d)' % + (op_name, len(out_data), op.dst_length)) + + # Write output back to partition, with padding. + unaligned_out_len = len(out_data) % block_size + if unaligned_out_len: + out_data += '\0' * (block_size - unaligned_out_len) + _WriteExtents(new_part_file, out_data, op.dst_extents, block_size, + '%s.dst_extents' % op_name) + + # Delete input/output files. + os.remove(in_file_name) + os.remove(out_file_name) + + # Delete patch file. + os.remove(patch_file_name) + + def _ApplyOperations(self, operations, base_name, old_part_file, + new_part_file, part_size): + """Applies a sequence of update operations to a partition. + + This assumes an in-place update semantics for MOVE and BSDIFF, namely all + reads are performed first, then the data is processed and written back to + the same file. + + Args: + operations: the sequence of operations + base_name: the name of the operation sequence + old_part_file: the old partition file object, open for reading/writing + new_part_file: the new partition file object, open for reading/writing + part_size: the partition size + + Raises: + PayloadError if anything goes wrong while processing the payload. + """ + for op, op_name in common.OperationIter(operations, base_name): + # Read data blob. + data = self.payload.ReadDataBlob(op.data_offset, op.data_length) + + if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ, + common.OpType.REPLACE_XZ): + self._ApplyReplaceOperation(op, op_name, data, new_part_file, part_size) + elif op.type == common.OpType.MOVE: + self._ApplyMoveOperation(op, op_name, new_part_file) + elif op.type == common.OpType.ZERO: + self._ApplyZeroOperation(op, op_name, new_part_file) + elif op.type == common.OpType.BSDIFF: + self._ApplyDiffOperation(op, op_name, data, new_part_file, + new_part_file) + elif op.type == common.OpType.SOURCE_COPY: + self._ApplySourceCopyOperation(op, op_name, old_part_file, + new_part_file) + elif op.type in (common.OpType.SOURCE_BSDIFF, common.OpType.PUFFDIFF, + common.OpType.BROTLI_BSDIFF): + self._ApplyDiffOperation(op, op_name, data, old_part_file, + new_part_file) + else: + raise PayloadError('%s: unknown operation type (%d)' % + (op_name, op.type)) + + def _ApplyToPartition(self, operations, part_name, base_name, + new_part_file_name, new_part_info, + old_part_file_name=None, old_part_info=None): + """Applies an update to a partition. + + Args: + operations: the sequence of update operations to apply + part_name: the name of the partition, for error reporting + base_name: the name of the operation sequence + new_part_file_name: file name to write partition data to + new_part_info: size and expected hash of dest partition + old_part_file_name: file name of source partition (optional) + old_part_info: size and expected hash of source partition (optional) + + Raises: + PayloadError if anything goes wrong with the update. + """ + # Do we have a source partition? + if old_part_file_name: + # Verify the source partition. + with open(old_part_file_name, 'rb') as old_part_file: + _VerifySha256(old_part_file, old_part_info.hash, + 'old ' + part_name, length=old_part_info.size) + new_part_file_mode = 'r+b' + if self.minor_version == common.INPLACE_MINOR_PAYLOAD_VERSION: + # Copy the src partition to the dst one; make sure we don't truncate it. + shutil.copyfile(old_part_file_name, new_part_file_name) + elif (self.minor_version == common.SOURCE_MINOR_PAYLOAD_VERSION or + self.minor_version == common.OPSRCHASH_MINOR_PAYLOAD_VERSION or + self.minor_version == common.BROTLI_BSDIFF_MINOR_PAYLOAD_VERSION or + self.minor_version == common.PUFFDIFF_MINOR_PAYLOAD_VERSION): + # In minor version >= 2, we don't want to copy the partitions, so + # instead just make the new partition file. + open(new_part_file_name, 'w').close() + else: + raise PayloadError("Unknown minor version: %d" % self.minor_version) + else: + # We need to create/truncate the dst partition file. + new_part_file_mode = 'w+b' + + # Apply operations. + with open(new_part_file_name, new_part_file_mode) as new_part_file: + old_part_file = (open(old_part_file_name, 'r+b') + if old_part_file_name else None) + try: + self._ApplyOperations(operations, base_name, old_part_file, + new_part_file, new_part_info.size) + finally: + if old_part_file: + old_part_file.close() + + # Truncate the result, if so instructed. + if self.truncate_to_expected_size: + new_part_file.seek(0, 2) + if new_part_file.tell() > new_part_info.size: + new_part_file.seek(new_part_info.size) + new_part_file.truncate() + + # Verify the resulting partition. + with open(new_part_file_name, 'rb') as new_part_file: + _VerifySha256(new_part_file, new_part_info.hash, + 'new ' + part_name, length=new_part_info.size) + + def Run(self, new_kernel_part, new_rootfs_part, old_kernel_part=None, + old_rootfs_part=None): + """Applier entry point, invoking all update operations. + + Args: + new_kernel_part: name of dest kernel partition file + new_rootfs_part: name of dest rootfs partition file + old_kernel_part: name of source kernel partition file (optional) + old_rootfs_part: name of source rootfs partition file (optional) + + Raises: + PayloadError if payload application failed. + """ + self.payload.ResetFile() + + # Make sure the arguments are sane and match the payload. + if not (new_kernel_part and new_rootfs_part): + raise PayloadError('missing dst {kernel,rootfs} partitions') + + if not (old_kernel_part or old_rootfs_part): + if not self.payload.IsFull(): + raise PayloadError('trying to apply a non-full update without src ' + '{kernel,rootfs} partitions') + elif old_kernel_part and old_rootfs_part: + if not self.payload.IsDelta(): + raise PayloadError('trying to apply a non-delta update onto src ' + '{kernel,rootfs} partitions') + else: + raise PayloadError('not all src partitions provided') + + # Apply update to rootfs. + self._ApplyToPartition( + self.payload.manifest.install_operations, 'rootfs', + 'install_operations', new_rootfs_part, + self.payload.manifest.new_rootfs_info, old_rootfs_part, + self.payload.manifest.old_rootfs_info) + + # Apply update to kernel update. + self._ApplyToPartition( + self.payload.manifest.kernel_install_operations, 'kernel', + 'kernel_install_operations', new_kernel_part, + self.payload.manifest.new_kernel_info, old_kernel_part, + self.payload.manifest.old_kernel_info) diff --git a/update_payload/checker.py b/update_payload/checker.py new file mode 100644 index 0000000..e241b0b --- /dev/null +++ b/update_payload/checker.py @@ -0,0 +1,1336 @@ +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Verifying the integrity of a Chrome OS update payload. + +This module is used internally by the main Payload class for verifying the +integrity of an update payload. The interface for invoking the checks is as +follows: + + checker = PayloadChecker(payload) + checker.Run(...) +""" + +from __future__ import print_function + +import array +import base64 +import hashlib +import itertools +import os +import subprocess + +from update_payload import common +from update_payload import error +from update_payload import format_utils +from update_payload import histogram +from update_payload import update_metadata_pb2 + + +# +# Constants. +# + +_CHECK_DST_PSEUDO_EXTENTS = 'dst-pseudo-extents' +_CHECK_MOVE_SAME_SRC_DST_BLOCK = 'move-same-src-dst-block' +_CHECK_PAYLOAD_SIG = 'payload-sig' +CHECKS_TO_DISABLE = ( + _CHECK_DST_PSEUDO_EXTENTS, + _CHECK_MOVE_SAME_SRC_DST_BLOCK, + _CHECK_PAYLOAD_SIG, +) + +_TYPE_FULL = 'full' +_TYPE_DELTA = 'delta' + +_DEFAULT_BLOCK_SIZE = 4096 + +_DEFAULT_PUBKEY_BASE_NAME = 'update-payload-key.pub.pem' +_DEFAULT_PUBKEY_FILE_NAME = os.path.join(os.path.dirname(__file__), + _DEFAULT_PUBKEY_BASE_NAME) + +# Supported minor version map to payload types allowed to be using them. +_SUPPORTED_MINOR_VERSIONS = { + 0: (_TYPE_FULL,), + 1: (_TYPE_DELTA,), + 2: (_TYPE_DELTA,), + 3: (_TYPE_DELTA,), + 4: (_TYPE_DELTA,), + 5: (_TYPE_DELTA,), +} + +_OLD_DELTA_USABLE_PART_SIZE = 2 * 1024 * 1024 * 1024 + +# +# Helper functions. +# + +def _IsPowerOfTwo(val): + """Returns True iff val is a power of two.""" + return val > 0 and (val & (val - 1)) == 0 + + +def _AddFormat(format_func, value): + """Adds a custom formatted representation to ordinary string representation. + + Args: + format_func: A value formatter. + value: Value to be formatted and returned. + + Returns: + A string 'x (y)' where x = str(value) and y = format_func(value). + """ + ret = str(value) + formatted_str = format_func(value) + if formatted_str: + ret += ' (%s)' % formatted_str + return ret + + +def _AddHumanReadableSize(size): + """Adds a human readable representation to a byte size value.""" + return _AddFormat(format_utils.BytesToHumanReadable, size) + + +# +# Payload report generator. +# + +class _PayloadReport(object): + """A payload report generator. + + A report is essentially a sequence of nodes, which represent data points. It + is initialized to have a "global", untitled section. A node may be a + sub-report itself. + """ + + # Report nodes: Field, sub-report, section. + class Node(object): + """A report node interface.""" + + @staticmethod + def _Indent(indent, line): + """Indents a line by a given indentation amount. + + Args: + indent: The indentation amount. + line: The line content (string). + + Returns: + The properly indented line (string). + """ + return '%*s%s' % (indent, '', line) + + def GenerateLines(self, base_indent, sub_indent, curr_section): + """Generates the report lines for this node. + + Args: + base_indent: Base indentation for each line. + sub_indent: Additional indentation for sub-nodes. + curr_section: The current report section object. + + Returns: + A pair consisting of a list of properly indented report lines and a new + current section object. + """ + raise NotImplementedError + + class FieldNode(Node): + """A field report node, representing a (name, value) pair.""" + + def __init__(self, name, value, linebreak, indent): + super(_PayloadReport.FieldNode, self).__init__() + self.name = name + self.value = value + self.linebreak = linebreak + self.indent = indent + + def GenerateLines(self, base_indent, sub_indent, curr_section): + """Generates a properly formatted 'name : value' entry.""" + report_output = '' + if self.name: + report_output += self.name.ljust(curr_section.max_field_name_len) + ' :' + value_lines = str(self.value).splitlines() + if self.linebreak and self.name: + report_output += '\n' + '\n'.join( + ['%*s%s' % (self.indent, '', line) for line in value_lines]) + else: + if self.name: + report_output += ' ' + report_output += '%*s' % (self.indent, '') + cont_line_indent = len(report_output) + indented_value_lines = [value_lines[0]] + indented_value_lines.extend(['%*s%s' % (cont_line_indent, '', line) + for line in value_lines[1:]]) + report_output += '\n'.join(indented_value_lines) + + report_lines = [self._Indent(base_indent, line + '\n') + for line in report_output.split('\n')] + return report_lines, curr_section + + class SubReportNode(Node): + """A sub-report node, representing a nested report.""" + + def __init__(self, title, report): + super(_PayloadReport.SubReportNode, self).__init__() + self.title = title + self.report = report + + def GenerateLines(self, base_indent, sub_indent, curr_section): + """Recurse with indentation.""" + report_lines = [self._Indent(base_indent, self.title + ' =>\n')] + report_lines.extend(self.report.GenerateLines(base_indent + sub_indent, + sub_indent)) + return report_lines, curr_section + + class SectionNode(Node): + """A section header node.""" + + def __init__(self, title=None): + super(_PayloadReport.SectionNode, self).__init__() + self.title = title + self.max_field_name_len = 0 + + def GenerateLines(self, base_indent, sub_indent, curr_section): + """Dump a title line, return self as the (new) current section.""" + report_lines = [] + if self.title: + report_lines.append(self._Indent(base_indent, + '=== %s ===\n' % self.title)) + return report_lines, self + + def __init__(self): + self.report = [] + self.last_section = self.global_section = self.SectionNode() + self.is_finalized = False + + def GenerateLines(self, base_indent, sub_indent): + """Generates the lines in the report, properly indented. + + Args: + base_indent: The indentation used for root-level report lines. + sub_indent: The indentation offset used for sub-reports. + + Returns: + A list of indented report lines. + """ + report_lines = [] + curr_section = self.global_section + for node in self.report: + node_report_lines, curr_section = node.GenerateLines( + base_indent, sub_indent, curr_section) + report_lines.extend(node_report_lines) + + return report_lines + + def Dump(self, out_file, base_indent=0, sub_indent=2): + """Dumps the report to a file. + + Args: + out_file: File object to output the content to. + base_indent: Base indentation for report lines. + sub_indent: Added indentation for sub-reports. + """ + report_lines = self.GenerateLines(base_indent, sub_indent) + if report_lines and not self.is_finalized: + report_lines.append('(incomplete report)\n') + + for line in report_lines: + out_file.write(line) + + def AddField(self, name, value, linebreak=False, indent=0): + """Adds a field/value pair to the payload report. + + Args: + name: The field's name. + value: The field's value. + linebreak: Whether the value should be printed on a new line. + indent: Amount of extra indent for each line of the value. + """ + assert not self.is_finalized + if name and self.last_section.max_field_name_len < len(name): + self.last_section.max_field_name_len = len(name) + self.report.append(self.FieldNode(name, value, linebreak, indent)) + + def AddSubReport(self, title): + """Adds and returns a sub-report with a title.""" + assert not self.is_finalized + sub_report = self.SubReportNode(title, type(self)()) + self.report.append(sub_report) + return sub_report.report + + def AddSection(self, title): + """Adds a new section title.""" + assert not self.is_finalized + self.last_section = self.SectionNode(title) + self.report.append(self.last_section) + + def Finalize(self): + """Seals the report, marking it as complete.""" + self.is_finalized = True + + +# +# Payload verification. +# + +class PayloadChecker(object): + """Checking the integrity of an update payload. + + This is a short-lived object whose purpose is to isolate the logic used for + verifying the integrity of an update payload. + """ + + def __init__(self, payload, assert_type=None, block_size=0, + allow_unhashed=False, disabled_tests=()): + """Initialize the checker. + + Args: + payload: The payload object to check. + assert_type: Assert that payload is either 'full' or 'delta' (optional). + block_size: Expected filesystem / payload block size (optional). + allow_unhashed: Allow operations with unhashed data blobs. + disabled_tests: Sequence of tests to disable. + """ + if not payload.is_init: + raise ValueError('Uninitialized update payload.') + + # Set checker configuration. + self.payload = payload + self.block_size = block_size if block_size else _DEFAULT_BLOCK_SIZE + if not _IsPowerOfTwo(self.block_size): + raise error.PayloadError( + 'Expected block (%d) size is not a power of two.' % self.block_size) + if assert_type not in (None, _TYPE_FULL, _TYPE_DELTA): + raise error.PayloadError('Invalid assert_type value (%r).' % + assert_type) + self.payload_type = assert_type + self.allow_unhashed = allow_unhashed + + # Disable specific tests. + self.check_dst_pseudo_extents = ( + _CHECK_DST_PSEUDO_EXTENTS not in disabled_tests) + self.check_move_same_src_dst_block = ( + _CHECK_MOVE_SAME_SRC_DST_BLOCK not in disabled_tests) + self.check_payload_sig = _CHECK_PAYLOAD_SIG not in disabled_tests + + # Reset state; these will be assigned when the manifest is checked. + self.sigs_offset = 0 + self.sigs_size = 0 + self.old_rootfs_fs_size = 0 + self.old_kernel_fs_size = 0 + self.new_rootfs_fs_size = 0 + self.new_kernel_fs_size = 0 + self.minor_version = None + # TODO(*): When fixing crbug.com/794404, the major version should be + # correclty handled in update_payload scripts. So stop forcing + # major_verions=1 here and set it to the correct value. + self.major_version = 1 + + @staticmethod + def _CheckElem(msg, name, report, is_mandatory, is_submsg, convert=str, + msg_name=None, linebreak=False, indent=0): + """Adds an element from a protobuf message to the payload report. + + Checks to see whether a message contains a given element, and if so adds + the element value to the provided report. A missing mandatory element + causes an exception to be raised. + + Args: + msg: The message containing the element. + name: The name of the element. + report: A report object to add the element name/value to. + is_mandatory: Whether or not this element must be present. + is_submsg: Whether this element is itself a message. + convert: A function for converting the element value for reporting. + msg_name: The name of the message object (for error reporting). + linebreak: Whether the value report should induce a line break. + indent: Amount of indent used for reporting the value. + + Returns: + A pair consisting of the element value and the generated sub-report for + it (if the element is a sub-message, None otherwise). If the element is + missing, returns (None, None). + + Raises: + error.PayloadError if a mandatory element is missing. + """ + if not msg.HasField(name): + if is_mandatory: + raise error.PayloadError('%smissing mandatory %s %r.' % + (msg_name + ' ' if msg_name else '', + 'sub-message' if is_submsg else 'field', + name)) + return None, None + + value = getattr(msg, name) + if is_submsg: + return value, report and report.AddSubReport(name) + else: + if report: + report.AddField(name, convert(value), linebreak=linebreak, + indent=indent) + return value, None + + @staticmethod + def _CheckMandatoryField(msg, field_name, report, msg_name, convert=str, + linebreak=False, indent=0): + """Adds a mandatory field; returning first component from _CheckElem.""" + return PayloadChecker._CheckElem(msg, field_name, report, True, False, + convert=convert, msg_name=msg_name, + linebreak=linebreak, indent=indent)[0] + + @staticmethod + def _CheckOptionalField(msg, field_name, report, convert=str, + linebreak=False, indent=0): + """Adds an optional field; returning first component from _CheckElem.""" + return PayloadChecker._CheckElem(msg, field_name, report, False, False, + convert=convert, linebreak=linebreak, + indent=indent)[0] + + @staticmethod + def _CheckMandatorySubMsg(msg, submsg_name, report, msg_name): + """Adds a mandatory sub-message; wrapper for _CheckElem.""" + return PayloadChecker._CheckElem(msg, submsg_name, report, True, True, + msg_name) + + @staticmethod + def _CheckOptionalSubMsg(msg, submsg_name, report): + """Adds an optional sub-message; wrapper for _CheckElem.""" + return PayloadChecker._CheckElem(msg, submsg_name, report, False, True) + + @staticmethod + def _CheckPresentIff(val1, val2, name1, name2, obj_name): + """Checks that val1 is None iff val2 is None. + + Args: + val1: first value to be compared. + val2: second value to be compared. + name1: name of object holding the first value. + name2: name of object holding the second value. + obj_name: Name of the object containing these values. + + Raises: + error.PayloadError if assertion does not hold. + """ + if None in (val1, val2) and val1 is not val2: + present, missing = (name1, name2) if val2 is None else (name2, name1) + raise error.PayloadError('%r present without %r%s.' % + (present, missing, + ' in ' + obj_name if obj_name else '')) + + @staticmethod + def _Run(cmd, send_data=None): + """Runs a subprocess, returns its output. + + Args: + cmd: Sequence of command-line argument for invoking the subprocess. + send_data: Data to feed to the process via its stdin. + + Returns: + A tuple containing the stdout and stderr output of the process. + """ + run_process = subprocess.Popen(cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + try: + result = run_process.communicate(input=send_data) + finally: + exit_code = run_process.wait() + + if exit_code: + raise RuntimeError('Subprocess %r failed with code %r.' % + (cmd, exit_code)) + + return result + + @staticmethod + def _CheckSha256Signature(sig_data, pubkey_file_name, actual_hash, sig_name): + """Verifies an actual hash against a signed one. + + Args: + sig_data: The raw signature data. + pubkey_file_name: Public key used for verifying signature. + actual_hash: The actual hash digest. + sig_name: Signature name for error reporting. + + Raises: + error.PayloadError if signature could not be verified. + """ + if len(sig_data) != 256: + raise error.PayloadError( + '%s: signature size (%d) not as expected (256).' % + (sig_name, len(sig_data))) + signed_data, _ = PayloadChecker._Run( + ['openssl', 'rsautl', '-verify', '-pubin', '-inkey', pubkey_file_name], + send_data=sig_data) + + if len(signed_data) != len(common.SIG_ASN1_HEADER) + 32: + raise error.PayloadError('%s: unexpected signed data length (%d).' % + (sig_name, len(signed_data))) + + if not signed_data.startswith(common.SIG_ASN1_HEADER): + raise error.PayloadError('%s: not containing standard ASN.1 prefix.' % + sig_name) + + signed_hash = signed_data[len(common.SIG_ASN1_HEADER):] + if signed_hash != actual_hash: + raise error.PayloadError( + '%s: signed hash (%s) different from actual (%s).' % + (sig_name, common.FormatSha256(signed_hash), + common.FormatSha256(actual_hash))) + + @staticmethod + def _CheckBlocksFitLength(length, num_blocks, block_size, length_name, + block_name=None): + """Checks that a given length fits given block space. + + This ensures that the number of blocks allocated is appropriate for the + length of the data residing in these blocks. + + Args: + length: The actual length of the data. + num_blocks: The number of blocks allocated for it. + block_size: The size of each block in bytes. + length_name: Name of length (used for error reporting). + block_name: Name of block (used for error reporting). + + Raises: + error.PayloadError if the aforementioned invariant is not satisfied. + """ + # Check: length <= num_blocks * block_size. + if length > num_blocks * block_size: + raise error.PayloadError( + '%s (%d) > num %sblocks (%d) * block_size (%d).' % + (length_name, length, block_name or '', num_blocks, block_size)) + + # Check: length > (num_blocks - 1) * block_size. + if length <= (num_blocks - 1) * block_size: + raise error.PayloadError( + '%s (%d) <= (num %sblocks - 1 (%d)) * block_size (%d).' % + (length_name, length, block_name or '', num_blocks - 1, block_size)) + + def _CheckManifestMinorVersion(self, report): + """Checks the payload manifest minor_version field. + + Args: + report: The report object to add to. + + Raises: + error.PayloadError if any of the checks fail. + """ + self.minor_version = self._CheckOptionalField(self.payload.manifest, + 'minor_version', report) + if self.minor_version in _SUPPORTED_MINOR_VERSIONS: + if self.payload_type not in _SUPPORTED_MINOR_VERSIONS[self.minor_version]: + raise error.PayloadError( + 'Minor version %d not compatible with payload type %s.' % + (self.minor_version, self.payload_type)) + elif self.minor_version is None: + raise error.PayloadError('Minor version is not set.') + else: + raise error.PayloadError('Unsupported minor version: %d' % + self.minor_version) + + def _CheckManifest(self, report, rootfs_part_size=0, kernel_part_size=0): + """Checks the payload manifest. + + Args: + report: A report object to add to. + rootfs_part_size: Size of the rootfs partition in bytes. + kernel_part_size: Size of the kernel partition in bytes. + + Returns: + A tuple consisting of the partition block size used during the update + (integer), the signatures block offset and size. + + Raises: + error.PayloadError if any of the checks fail. + """ + manifest = self.payload.manifest + report.AddSection('manifest') + + # Check: block_size must exist and match the expected value. + actual_block_size = self._CheckMandatoryField(manifest, 'block_size', + report, 'manifest') + if actual_block_size != self.block_size: + raise error.PayloadError('Block_size (%d) not as expected (%d).' % + (actual_block_size, self.block_size)) + + # Check: signatures_offset <==> signatures_size. + self.sigs_offset = self._CheckOptionalField(manifest, 'signatures_offset', + report) + self.sigs_size = self._CheckOptionalField(manifest, 'signatures_size', + report) + self._CheckPresentIff(self.sigs_offset, self.sigs_size, + 'signatures_offset', 'signatures_size', 'manifest') + + # Check: old_kernel_info <==> old_rootfs_info. + oki_msg, oki_report = self._CheckOptionalSubMsg(manifest, + 'old_kernel_info', report) + ori_msg, ori_report = self._CheckOptionalSubMsg(manifest, + 'old_rootfs_info', report) + self._CheckPresentIff(oki_msg, ori_msg, 'old_kernel_info', + 'old_rootfs_info', 'manifest') + if oki_msg: # equivalently, ori_msg + # Assert/mark delta payload. + if self.payload_type == _TYPE_FULL: + raise error.PayloadError( + 'Apparent full payload contains old_{kernel,rootfs}_info.') + self.payload_type = _TYPE_DELTA + + # Check: {size, hash} present in old_{kernel,rootfs}_info. + self.old_kernel_fs_size = self._CheckMandatoryField( + oki_msg, 'size', oki_report, 'old_kernel_info') + self._CheckMandatoryField(oki_msg, 'hash', oki_report, 'old_kernel_info', + convert=common.FormatSha256) + self.old_rootfs_fs_size = self._CheckMandatoryField( + ori_msg, 'size', ori_report, 'old_rootfs_info') + self._CheckMandatoryField(ori_msg, 'hash', ori_report, 'old_rootfs_info', + convert=common.FormatSha256) + + # Check: old_{kernel,rootfs} size must fit in respective partition. + if kernel_part_size and self.old_kernel_fs_size > kernel_part_size: + raise error.PayloadError( + 'Old kernel content (%d) exceed partition size (%d).' % + (self.old_kernel_fs_size, kernel_part_size)) + if rootfs_part_size and self.old_rootfs_fs_size > rootfs_part_size: + raise error.PayloadError( + 'Old rootfs content (%d) exceed partition size (%d).' % + (self.old_rootfs_fs_size, rootfs_part_size)) + else: + # Assert/mark full payload. + if self.payload_type == _TYPE_DELTA: + raise error.PayloadError( + 'Apparent delta payload missing old_{kernel,rootfs}_info.') + self.payload_type = _TYPE_FULL + + # Check: new_kernel_info present; contains {size, hash}. + nki_msg, nki_report = self._CheckMandatorySubMsg( + manifest, 'new_kernel_info', report, 'manifest') + self.new_kernel_fs_size = self._CheckMandatoryField( + nki_msg, 'size', nki_report, 'new_kernel_info') + self._CheckMandatoryField(nki_msg, 'hash', nki_report, 'new_kernel_info', + convert=common.FormatSha256) + + # Check: new_rootfs_info present; contains {size, hash}. + nri_msg, nri_report = self._CheckMandatorySubMsg( + manifest, 'new_rootfs_info', report, 'manifest') + self.new_rootfs_fs_size = self._CheckMandatoryField( + nri_msg, 'size', nri_report, 'new_rootfs_info') + self._CheckMandatoryField(nri_msg, 'hash', nri_report, 'new_rootfs_info', + convert=common.FormatSha256) + + # Check: new_{kernel,rootfs} size must fit in respective partition. + if kernel_part_size and self.new_kernel_fs_size > kernel_part_size: + raise error.PayloadError( + 'New kernel content (%d) exceed partition size (%d).' % + (self.new_kernel_fs_size, kernel_part_size)) + if rootfs_part_size and self.new_rootfs_fs_size > rootfs_part_size: + raise error.PayloadError( + 'New rootfs content (%d) exceed partition size (%d).' % + (self.new_rootfs_fs_size, rootfs_part_size)) + + # Check: minor_version makes sense for the payload type. This check should + # run after the payload type has been set. + self._CheckManifestMinorVersion(report) + + def _CheckLength(self, length, total_blocks, op_name, length_name): + """Checks whether a length matches the space designated in extents. + + Args: + length: The total length of the data. + total_blocks: The total number of blocks in extents. + op_name: Operation name (for error reporting). + length_name: Length name (for error reporting). + + Raises: + error.PayloadError is there a problem with the length. + """ + # Check: length is non-zero. + if length == 0: + raise error.PayloadError('%s: %s is zero.' % (op_name, length_name)) + + # Check that length matches number of blocks. + self._CheckBlocksFitLength(length, total_blocks, self.block_size, + '%s: %s' % (op_name, length_name)) + + def _CheckExtents(self, extents, usable_size, block_counters, name, + allow_pseudo=False, allow_signature=False): + """Checks a sequence of extents. + + Args: + extents: The sequence of extents to check. + usable_size: The usable size of the partition to which the extents apply. + block_counters: Array of counters corresponding to the number of blocks. + name: The name of the extent block. + allow_pseudo: Whether or not pseudo block numbers are allowed. + allow_signature: Whether or not the extents are used for a signature. + + Returns: + The total number of blocks in the extents. + + Raises: + error.PayloadError if any of the entailed checks fails. + """ + total_num_blocks = 0 + for ex, ex_name in common.ExtentIter(extents, name): + # Check: Mandatory fields. + start_block = PayloadChecker._CheckMandatoryField(ex, 'start_block', + None, ex_name) + num_blocks = PayloadChecker._CheckMandatoryField(ex, 'num_blocks', None, + ex_name) + end_block = start_block + num_blocks + + # Check: num_blocks > 0. + if num_blocks == 0: + raise error.PayloadError('%s: extent length is zero.' % ex_name) + + if start_block != common.PSEUDO_EXTENT_MARKER: + # Check: Make sure we're within the partition limit. + if usable_size and end_block * self.block_size > usable_size: + raise error.PayloadError( + '%s: extent (%s) exceeds usable partition size (%d).' % + (ex_name, common.FormatExtent(ex, self.block_size), usable_size)) + + # Record block usage. + for i in xrange(start_block, end_block): + block_counters[i] += 1 + elif not (allow_pseudo or (allow_signature and len(extents) == 1)): + # Pseudo-extents must be allowed explicitly, or otherwise be part of a + # signature operation (in which case there has to be exactly one). + raise error.PayloadError('%s: unexpected pseudo-extent.' % ex_name) + + total_num_blocks += num_blocks + + return total_num_blocks + + def _CheckReplaceOperation(self, op, data_length, total_dst_blocks, op_name): + """Specific checks for REPLACE/REPLACE_BZ/REPLACE_XZ operations. + + Args: + op: The operation object from the manifest. + data_length: The length of the data blob associated with the operation. + total_dst_blocks: Total number of blocks in dst_extents. + op_name: Operation name for error reporting. + + Raises: + error.PayloadError if any check fails. + """ + # Check: Does not contain src extents. + if op.src_extents: + raise error.PayloadError('%s: contains src_extents.' % op_name) + + # Check: Contains data. + if data_length is None: + raise error.PayloadError('%s: missing data_{offset,length}.' % op_name) + + if op.type == common.OpType.REPLACE: + PayloadChecker._CheckBlocksFitLength(data_length, total_dst_blocks, + self.block_size, + op_name + '.data_length', 'dst') + else: + # Check: data_length must be smaller than the alotted dst blocks. + if data_length >= total_dst_blocks * self.block_size: + raise error.PayloadError( + '%s: data_length (%d) must be less than allotted dst block ' + 'space (%d * %d).' % + (op_name, data_length, total_dst_blocks, self.block_size)) + + def _CheckMoveOperation(self, op, data_offset, total_src_blocks, + total_dst_blocks, op_name): + """Specific checks for MOVE operations. + + Args: + op: The operation object from the manifest. + data_offset: The offset of a data blob for the operation. + total_src_blocks: Total number of blocks in src_extents. + total_dst_blocks: Total number of blocks in dst_extents. + op_name: Operation name for error reporting. + + Raises: + error.PayloadError if any check fails. + """ + # Check: No data_{offset,length}. + if data_offset is not None: + raise error.PayloadError('%s: contains data_{offset,length}.' % op_name) + + # Check: total_src_blocks == total_dst_blocks. + if total_src_blocks != total_dst_blocks: + raise error.PayloadError( + '%s: total src blocks (%d) != total dst blocks (%d).' % + (op_name, total_src_blocks, total_dst_blocks)) + + # Check: For all i, i-th src block index != i-th dst block index. + i = 0 + src_extent_iter = iter(op.src_extents) + dst_extent_iter = iter(op.dst_extents) + src_extent = dst_extent = None + src_idx = src_num = dst_idx = dst_num = 0 + while i < total_src_blocks: + # Get the next source extent, if needed. + if not src_extent: + try: + src_extent = src_extent_iter.next() + except StopIteration: + raise error.PayloadError('%s: ran out of src extents (%d/%d).' % + (op_name, i, total_src_blocks)) + src_idx = src_extent.start_block + src_num = src_extent.num_blocks + + # Get the next dest extent, if needed. + if not dst_extent: + try: + dst_extent = dst_extent_iter.next() + except StopIteration: + raise error.PayloadError('%s: ran out of dst extents (%d/%d).' % + (op_name, i, total_dst_blocks)) + dst_idx = dst_extent.start_block + dst_num = dst_extent.num_blocks + + # Check: start block is not 0. See crbug/480751; there are still versions + # of update_engine which fail when seeking to 0 in PReadAll and PWriteAll, + # so we need to fail payloads that try to MOVE to/from block 0. + if src_idx == 0 or dst_idx == 0: + raise error.PayloadError( + '%s: MOVE operation cannot have extent with start block 0' % + op_name) + + if self.check_move_same_src_dst_block and src_idx == dst_idx: + raise error.PayloadError( + '%s: src/dst block number %d is the same (%d).' % + (op_name, i, src_idx)) + + advance = min(src_num, dst_num) + i += advance + + src_idx += advance + src_num -= advance + if src_num == 0: + src_extent = None + + dst_idx += advance + dst_num -= advance + if dst_num == 0: + dst_extent = None + + # Make sure we've exhausted all src/dst extents. + if src_extent: + raise error.PayloadError('%s: excess src blocks.' % op_name) + if dst_extent: + raise error.PayloadError('%s: excess dst blocks.' % op_name) + + def _CheckZeroOperation(self, op, op_name): + """Specific checks for ZERO operations. + + Args: + op: The operation object from the manifest. + op_name: Operation name for error reporting. + + Raises: + error.PayloadError if any check fails. + """ + # Check: Does not contain src extents, data_length and data_offset. + if op.src_extents: + raise error.PayloadError('%s: contains src_extents.' % op_name) + if op.data_length: + raise error.PayloadError('%s: contains data_length.' % op_name) + if op.data_offset: + raise error.PayloadError('%s: contains data_offset.' % op_name) + + def _CheckAnyDiffOperation(self, op, data_length, total_dst_blocks, op_name): + """Specific checks for BSDIFF, SOURCE_BSDIFF, PUFFDIFF and BROTLI_BSDIFF + operations. + + Args: + op: The operation. + data_length: The length of the data blob associated with the operation. + total_dst_blocks: Total number of blocks in dst_extents. + op_name: Operation name for error reporting. + + Raises: + error.PayloadError if any check fails. + """ + # Check: data_{offset,length} present. + if data_length is None: + raise error.PayloadError('%s: missing data_{offset,length}.' % op_name) + + # Check: data_length is strictly smaller than the alotted dst blocks. + if data_length >= total_dst_blocks * self.block_size: + raise error.PayloadError( + '%s: data_length (%d) must be smaller than allotted dst space ' + '(%d * %d = %d).' % + (op_name, data_length, total_dst_blocks, self.block_size, + total_dst_blocks * self.block_size)) + + # Check the existence of src_length and dst_length for legacy bsdiffs. + if (op.type == common.OpType.BSDIFF or + (op.type == common.OpType.SOURCE_BSDIFF and self.minor_version <= 3)): + if not op.HasField('src_length') or not op.HasField('dst_length'): + raise error.PayloadError('%s: require {src,dst}_length.' % op_name) + else: + if op.HasField('src_length') or op.HasField('dst_length'): + raise error.PayloadError('%s: unneeded {src,dst}_length.' % op_name) + + def _CheckSourceCopyOperation(self, data_offset, total_src_blocks, + total_dst_blocks, op_name): + """Specific checks for SOURCE_COPY. + + Args: + data_offset: The offset of a data blob for the operation. + total_src_blocks: Total number of blocks in src_extents. + total_dst_blocks: Total number of blocks in dst_extents. + op_name: Operation name for error reporting. + + Raises: + error.PayloadError if any check fails. + """ + # Check: No data_{offset,length}. + if data_offset is not None: + raise error.PayloadError('%s: contains data_{offset,length}.' % op_name) + + # Check: total_src_blocks == total_dst_blocks. + if total_src_blocks != total_dst_blocks: + raise error.PayloadError( + '%s: total src blocks (%d) != total dst blocks (%d).' % + (op_name, total_src_blocks, total_dst_blocks)) + + def _CheckAnySourceOperation(self, op, total_src_blocks, op_name): + """Specific checks for SOURCE_* operations. + + Args: + op: The operation object from the manifest. + total_src_blocks: Total number of blocks in src_extents. + op_name: Operation name for error reporting. + + Raises: + error.PayloadError if any check fails. + """ + # Check: total_src_blocks != 0. + if total_src_blocks == 0: + raise error.PayloadError('%s: no src blocks in a source op.' % op_name) + + # Check: src_sha256_hash present in minor version >= 3. + if self.minor_version >= 3 and op.src_sha256_hash is None: + raise error.PayloadError('%s: source hash missing.' % op_name) + + def _CheckOperation(self, op, op_name, is_last, old_block_counters, + new_block_counters, old_usable_size, new_usable_size, + prev_data_offset, allow_signature, blob_hash_counts): + """Checks a single update operation. + + Args: + op: The operation object. + op_name: Operation name string for error reporting. + is_last: Whether this is the last operation in the sequence. + old_block_counters: Arrays of block read counters. + new_block_counters: Arrays of block write counters. + old_usable_size: The overall usable size for src data in bytes. + new_usable_size: The overall usable size for dst data in bytes. + prev_data_offset: Offset of last used data bytes. + allow_signature: Whether this may be a signature operation. + blob_hash_counts: Counters for hashed/unhashed blobs. + + Returns: + The amount of data blob associated with the operation. + + Raises: + error.PayloadError if any check has failed. + """ + # Check extents. + total_src_blocks = self._CheckExtents( + op.src_extents, old_usable_size, old_block_counters, + op_name + '.src_extents', allow_pseudo=True) + allow_signature_in_extents = (allow_signature and is_last and + op.type == common.OpType.REPLACE) + total_dst_blocks = self._CheckExtents( + op.dst_extents, new_usable_size, new_block_counters, + op_name + '.dst_extents', + allow_pseudo=(not self.check_dst_pseudo_extents), + allow_signature=allow_signature_in_extents) + + # Check: data_offset present <==> data_length present. + data_offset = self._CheckOptionalField(op, 'data_offset', None) + data_length = self._CheckOptionalField(op, 'data_length', None) + self._CheckPresentIff(data_offset, data_length, 'data_offset', + 'data_length', op_name) + + # Check: At least one dst_extent. + if not op.dst_extents: + raise error.PayloadError('%s: dst_extents is empty.' % op_name) + + # Check {src,dst}_length, if present. + if op.HasField('src_length'): + self._CheckLength(op.src_length, total_src_blocks, op_name, 'src_length') + if op.HasField('dst_length'): + self._CheckLength(op.dst_length, total_dst_blocks, op_name, 'dst_length') + + if op.HasField('data_sha256_hash'): + blob_hash_counts['hashed'] += 1 + + # Check: Operation carries data. + if data_offset is None: + raise error.PayloadError( + '%s: data_sha256_hash present but no data_{offset,length}.' % + op_name) + + # Check: Hash verifies correctly. + actual_hash = hashlib.sha256(self.payload.ReadDataBlob(data_offset, + data_length)) + if op.data_sha256_hash != actual_hash.digest(): + raise error.PayloadError( + '%s: data_sha256_hash (%s) does not match actual hash (%s).' % + (op_name, common.FormatSha256(op.data_sha256_hash), + common.FormatSha256(actual_hash.digest()))) + elif data_offset is not None: + if allow_signature_in_extents: + blob_hash_counts['signature'] += 1 + elif self.allow_unhashed: + blob_hash_counts['unhashed'] += 1 + else: + raise error.PayloadError('%s: unhashed operation not allowed.' % + op_name) + + if data_offset is not None: + # Check: Contiguous use of data section. + if data_offset != prev_data_offset: + raise error.PayloadError( + '%s: data offset (%d) not matching amount used so far (%d).' % + (op_name, data_offset, prev_data_offset)) + + # Type-specific checks. + if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ): + self._CheckReplaceOperation(op, data_length, total_dst_blocks, op_name) + elif op.type == common.OpType.REPLACE_XZ and (self.minor_version >= 3 or + self.major_version >= 2): + self._CheckReplaceOperation(op, data_length, total_dst_blocks, op_name) + elif op.type == common.OpType.MOVE and self.minor_version == 1: + self._CheckMoveOperation(op, data_offset, total_src_blocks, + total_dst_blocks, op_name) + elif op.type == common.OpType.ZERO and self.minor_version >= 4: + self._CheckZeroOperation(op, op_name) + elif op.type == common.OpType.BSDIFF and self.minor_version == 1: + self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name) + elif op.type == common.OpType.SOURCE_COPY and self.minor_version >= 2: + self._CheckSourceCopyOperation(data_offset, total_src_blocks, + total_dst_blocks, op_name) + self._CheckAnySourceOperation(op, total_src_blocks, op_name) + elif op.type == common.OpType.SOURCE_BSDIFF and self.minor_version >= 2: + self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name) + self._CheckAnySourceOperation(op, total_src_blocks, op_name) + elif op.type == common.OpType.BROTLI_BSDIFF and self.minor_version >= 4: + self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name) + self._CheckAnySourceOperation(op, total_src_blocks, op_name) + elif op.type == common.OpType.PUFFDIFF and self.minor_version >= 5: + self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name) + self._CheckAnySourceOperation(op, total_src_blocks, op_name) + else: + raise error.PayloadError( + 'Operation %s (type %d) not allowed in minor version %d' % + (op_name, op.type, self.minor_version)) + return data_length if data_length is not None else 0 + + def _SizeToNumBlocks(self, size): + """Returns the number of blocks needed to contain a given byte size.""" + return (size + self.block_size - 1) / self.block_size + + def _AllocBlockCounters(self, total_size): + """Returns a freshly initialized array of block counters. + + Note that the generated array is not portable as is due to byte-ordering + issues, hence it should not be serialized. + + Args: + total_size: The total block size in bytes. + + Returns: + An array of unsigned short elements initialized to zero, one for each of + the blocks necessary for containing the partition. + """ + return array.array('H', + itertools.repeat(0, self._SizeToNumBlocks(total_size))) + + def _CheckOperations(self, operations, report, base_name, old_fs_size, + new_fs_size, old_usable_size, new_usable_size, + prev_data_offset, allow_signature): + """Checks a sequence of update operations. + + Args: + operations: The sequence of operations to check. + report: The report object to add to. + base_name: The name of the operation block. + old_fs_size: The old filesystem size in bytes. + new_fs_size: The new filesystem size in bytes. + old_usable_size: The overall usable size of the old partition in bytes. + new_usable_size: The overall usable size of the new partition in bytes. + prev_data_offset: Offset of last used data bytes. + allow_signature: Whether this sequence may contain signature operations. + + Returns: + The total data blob size used. + + Raises: + error.PayloadError if any of the checks fails. + """ + # The total size of data blobs used by operations scanned thus far. + total_data_used = 0 + # Counts of specific operation types. + op_counts = { + common.OpType.REPLACE: 0, + common.OpType.REPLACE_BZ: 0, + common.OpType.REPLACE_XZ: 0, + common.OpType.MOVE: 0, + common.OpType.ZERO: 0, + common.OpType.BSDIFF: 0, + common.OpType.SOURCE_COPY: 0, + common.OpType.SOURCE_BSDIFF: 0, + common.OpType.PUFFDIFF: 0, + common.OpType.BROTLI_BSDIFF: 0, + } + # Total blob sizes for each operation type. + op_blob_totals = { + common.OpType.REPLACE: 0, + common.OpType.REPLACE_BZ: 0, + common.OpType.REPLACE_XZ: 0, + # MOVE operations don't have blobs. + common.OpType.BSDIFF: 0, + # SOURCE_COPY operations don't have blobs. + common.OpType.SOURCE_BSDIFF: 0, + common.OpType.PUFFDIFF: 0, + common.OpType.BROTLI_BSDIFF: 0, + } + # Counts of hashed vs unhashed operations. + blob_hash_counts = { + 'hashed': 0, + 'unhashed': 0, + } + if allow_signature: + blob_hash_counts['signature'] = 0 + + # Allocate old and new block counters. + old_block_counters = (self._AllocBlockCounters(old_usable_size) + if old_fs_size else None) + new_block_counters = self._AllocBlockCounters(new_usable_size) + + # Process and verify each operation. + op_num = 0 + for op, op_name in common.OperationIter(operations, base_name): + op_num += 1 + + # Check: Type is valid. + if op.type not in op_counts.keys(): + raise error.PayloadError('%s: invalid type (%d).' % (op_name, op.type)) + op_counts[op.type] += 1 + + is_last = op_num == len(operations) + curr_data_used = self._CheckOperation( + op, op_name, is_last, old_block_counters, new_block_counters, + old_usable_size, new_usable_size, + prev_data_offset + total_data_used, allow_signature, + blob_hash_counts) + if curr_data_used: + op_blob_totals[op.type] += curr_data_used + total_data_used += curr_data_used + + # Report totals and breakdown statistics. + report.AddField('total operations', op_num) + report.AddField( + None, + histogram.Histogram.FromCountDict(op_counts, + key_names=common.OpType.NAMES), + indent=1) + report.AddField('total blobs', sum(blob_hash_counts.values())) + report.AddField(None, + histogram.Histogram.FromCountDict(blob_hash_counts), + indent=1) + report.AddField('total blob size', _AddHumanReadableSize(total_data_used)) + report.AddField( + None, + histogram.Histogram.FromCountDict(op_blob_totals, + formatter=_AddHumanReadableSize, + key_names=common.OpType.NAMES), + indent=1) + + # Report read/write histograms. + if old_block_counters: + report.AddField('block read hist', + histogram.Histogram.FromKeyList(old_block_counters), + linebreak=True, indent=1) + + new_write_hist = histogram.Histogram.FromKeyList( + new_block_counters[:self._SizeToNumBlocks(new_fs_size)]) + report.AddField('block write hist', new_write_hist, linebreak=True, + indent=1) + + # Check: Full update must write each dst block once. + if self.payload_type == _TYPE_FULL and new_write_hist.GetKeys() != [1]: + raise error.PayloadError( + '%s: not all blocks written exactly once during full update.' % + base_name) + + return total_data_used + + def _CheckSignatures(self, report, pubkey_file_name): + """Checks a payload's signature block.""" + sigs_raw = self.payload.ReadDataBlob(self.sigs_offset, self.sigs_size) + sigs = update_metadata_pb2.Signatures() + sigs.ParseFromString(sigs_raw) + report.AddSection('signatures') + + # Check: At least one signature present. + if not sigs.signatures: + raise error.PayloadError('Signature block is empty.') + + last_ops_section = (self.payload.manifest.kernel_install_operations or + self.payload.manifest.install_operations) + fake_sig_op = last_ops_section[-1] + # Check: signatures_{offset,size} must match the last (fake) operation. + if not (fake_sig_op.type == common.OpType.REPLACE and + self.sigs_offset == fake_sig_op.data_offset and + self.sigs_size == fake_sig_op.data_length): + raise error.PayloadError( + 'Signatures_{offset,size} (%d+%d) does not match last operation ' + '(%d+%d).' % + (self.sigs_offset, self.sigs_size, fake_sig_op.data_offset, + fake_sig_op.data_length)) + + # Compute the checksum of all data up to signature blob. + # TODO(garnold) we're re-reading the whole data section into a string + # just to compute the checksum; instead, we could do it incrementally as + # we read the blobs one-by-one, under the assumption that we're reading + # them in order (which currently holds). This should be reconsidered. + payload_hasher = self.payload.manifest_hasher.copy() + common.Read(self.payload.payload_file, self.sigs_offset, + offset=self.payload.data_offset, hasher=payload_hasher) + + for sig, sig_name in common.SignatureIter(sigs.signatures, 'signatures'): + sig_report = report.AddSubReport(sig_name) + + # Check: Signature contains mandatory fields. + self._CheckMandatoryField(sig, 'version', sig_report, sig_name) + self._CheckMandatoryField(sig, 'data', None, sig_name) + sig_report.AddField('data len', len(sig.data)) + + # Check: Signatures pertains to actual payload hash. + if sig.version == 1: + self._CheckSha256Signature(sig.data, pubkey_file_name, + payload_hasher.digest(), sig_name) + else: + raise error.PayloadError('Unknown signature version (%d).' % + sig.version) + + def Run(self, pubkey_file_name=None, metadata_sig_file=None, + rootfs_part_size=0, kernel_part_size=0, report_out_file=None): + """Checker entry point, invoking all checks. + + Args: + pubkey_file_name: Public key used for signature verification. + metadata_sig_file: Metadata signature, if verification is desired. + rootfs_part_size: The size of rootfs partitions in bytes (default: infer + based on payload type and version). + kernel_part_size: The size of kernel partitions in bytes (default: use + reported filesystem size). + report_out_file: File object to dump the report to. + + Raises: + error.PayloadError if payload verification failed. + """ + if not pubkey_file_name: + pubkey_file_name = _DEFAULT_PUBKEY_FILE_NAME + + report = _PayloadReport() + + # Get payload file size. + self.payload.payload_file.seek(0, 2) + payload_file_size = self.payload.payload_file.tell() + self.payload.ResetFile() + + try: + # Check metadata signature (if provided). + if metadata_sig_file: + metadata_sig = base64.b64decode(metadata_sig_file.read()) + self._CheckSha256Signature(metadata_sig, pubkey_file_name, + self.payload.manifest_hasher.digest(), + 'metadata signature') + + # Part 1: Check the file header. + report.AddSection('header') + # Check: Payload version is valid. + if self.payload.header.version != 1: + raise error.PayloadError('Unknown payload version (%d).' % + self.payload.header.version) + report.AddField('version', self.payload.header.version) + report.AddField('manifest len', self.payload.header.manifest_len) + + # Part 2: Check the manifest. + self._CheckManifest(report, rootfs_part_size, kernel_part_size) + assert self.payload_type, 'payload type should be known by now' + + # Infer the usable partition size when validating rootfs operations: + # - If rootfs partition size was provided, use that. + # - Otherwise, if this is an older delta (minor version < 2), stick with + # a known constant size. This is necessary because older deltas may + # exceed the filesystem size when moving data blocks around. + # - Otherwise, use the encoded filesystem size. + new_rootfs_usable_size = self.new_rootfs_fs_size + old_rootfs_usable_size = self.old_rootfs_fs_size + if rootfs_part_size: + new_rootfs_usable_size = rootfs_part_size + old_rootfs_usable_size = rootfs_part_size + elif self.payload_type == _TYPE_DELTA and self.minor_version in (None, 1): + new_rootfs_usable_size = _OLD_DELTA_USABLE_PART_SIZE + old_rootfs_usable_size = _OLD_DELTA_USABLE_PART_SIZE + + # Part 3: Examine rootfs operations. + # TODO(garnold)(chromium:243559) only default to the filesystem size if + # no explicit size provided *and* the partition size is not embedded in + # the payload; see issue for more details. + report.AddSection('rootfs operations') + total_blob_size = self._CheckOperations( + self.payload.manifest.install_operations, report, + 'install_operations', self.old_rootfs_fs_size, + self.new_rootfs_fs_size, old_rootfs_usable_size, + new_rootfs_usable_size, 0, False) + + # Part 4: Examine kernel operations. + # TODO(garnold)(chromium:243559) as above. + report.AddSection('kernel operations') + total_blob_size += self._CheckOperations( + self.payload.manifest.kernel_install_operations, report, + 'kernel_install_operations', self.old_kernel_fs_size, + self.new_kernel_fs_size, + kernel_part_size if kernel_part_size else self.old_kernel_fs_size, + kernel_part_size if kernel_part_size else self.new_kernel_fs_size, + total_blob_size, True) + + # Check: Operations data reach the end of the payload file. + used_payload_size = self.payload.data_offset + total_blob_size + if used_payload_size != payload_file_size: + raise error.PayloadError( + 'Used payload size (%d) different from actual file size (%d).' % + (used_payload_size, payload_file_size)) + + # Part 5: Handle payload signatures message. + if self.check_payload_sig and self.sigs_size: + self._CheckSignatures(report, pubkey_file_name) + + # Part 6: Summary. + report.AddSection('summary') + report.AddField('update type', self.payload_type) + + report.Finalize() + finally: + if report_out_file: + report.Dump(report_out_file) diff --git a/update_payload/checker_unittest.py b/update_payload/checker_unittest.py new file mode 100755 index 0000000..f718234 --- /dev/null +++ b/update_payload/checker_unittest.py @@ -0,0 +1,1364 @@ +#!/usr/bin/python2 +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Unit testing checker.py.""" + +from __future__ import print_function + +import array +import collections +import cStringIO +import hashlib +import itertools +import os +import unittest + +# pylint cannot find mox. +# pylint: disable=F0401 +import mox + +from update_payload import checker +from update_payload import common +from update_payload import test_utils +from update_payload import update_metadata_pb2 +from update_payload.error import PayloadError +from update_payload.payload import Payload # Avoid name conflicts later. + + +def _OpTypeByName(op_name): + """Returns the type of an operation from itsname.""" + op_name_to_type = { + 'REPLACE': common.OpType.REPLACE, + 'REPLACE_BZ': common.OpType.REPLACE_BZ, + 'MOVE': common.OpType.MOVE, + 'BSDIFF': common.OpType.BSDIFF, + 'SOURCE_COPY': common.OpType.SOURCE_COPY, + 'SOURCE_BSDIFF': common.OpType.SOURCE_BSDIFF, + 'ZERO': common.OpType.ZERO, + 'DISCARD': common.OpType.DISCARD, + 'REPLACE_XZ': common.OpType.REPLACE_XZ, + 'PUFFDIFF': common.OpType.PUFFDIFF, + 'BROTLI_BSDIFF': common.OpType.BROTLI_BSDIFF, + } + return op_name_to_type[op_name] + + +def _GetPayloadChecker(payload_gen_write_to_file_func, payload_gen_dargs=None, + checker_init_dargs=None): + """Returns a payload checker from a given payload generator.""" + if payload_gen_dargs is None: + payload_gen_dargs = {} + if checker_init_dargs is None: + checker_init_dargs = {} + + payload_file = cStringIO.StringIO() + payload_gen_write_to_file_func(payload_file, **payload_gen_dargs) + payload_file.seek(0) + payload = Payload(payload_file) + payload.Init() + return checker.PayloadChecker(payload, **checker_init_dargs) + + +def _GetPayloadCheckerWithData(payload_gen): + """Returns a payload checker from a given payload generator.""" + payload_file = cStringIO.StringIO() + payload_gen.WriteToFile(payload_file) + payload_file.seek(0) + payload = Payload(payload_file) + payload.Init() + return checker.PayloadChecker(payload) + + +# This class doesn't need an __init__(). +# pylint: disable=W0232 +# Unit testing is all about running protected methods. +# pylint: disable=W0212 +# Don't bark about missing members of classes you cannot import. +# pylint: disable=E1101 +class PayloadCheckerTest(mox.MoxTestBase): + """Tests the PayloadChecker class. + + In addition to ordinary testFoo() methods, which are automatically invoked by + the unittest framework, in this class we make use of DoBarTest() calls that + implement parametric tests of certain features. In order to invoke each test, + which embodies a unique combination of parameter values, as a complete unit + test, we perform explicit enumeration of the parameter space and create + individual invocation contexts for each, which are then bound as + testBar__param1=val1__param2=val2(). The enumeration of parameter spaces for + all such tests is done in AddAllParametricTests(). + """ + + def MockPayload(self): + """Create a mock payload object, complete with a mock manifest.""" + payload = self.mox.CreateMock(Payload) + payload.is_init = True + payload.manifest = self.mox.CreateMock( + update_metadata_pb2.DeltaArchiveManifest) + return payload + + @staticmethod + def NewExtent(start_block, num_blocks): + """Returns an Extent message. + + Each of the provided fields is set iff it is >= 0; otherwise, it's left at + its default state. + + Args: + start_block: The starting block of the extent. + num_blocks: The number of blocks in the extent. + + Returns: + An Extent message. + """ + ex = update_metadata_pb2.Extent() + if start_block >= 0: + ex.start_block = start_block + if num_blocks >= 0: + ex.num_blocks = num_blocks + return ex + + @staticmethod + def NewExtentList(*args): + """Returns an list of extents. + + Args: + *args: (start_block, num_blocks) pairs defining the extents. + + Returns: + A list of Extent objects. + """ + ex_list = [] + for start_block, num_blocks in args: + ex_list.append(PayloadCheckerTest.NewExtent(start_block, num_blocks)) + return ex_list + + @staticmethod + def AddToMessage(repeated_field, field_vals): + for field_val in field_vals: + new_field = repeated_field.add() + new_field.CopyFrom(field_val) + + def SetupAddElemTest(self, is_present, is_submsg, convert=str, + linebreak=False, indent=0): + """Setup for testing of _CheckElem() and its derivatives. + + Args: + is_present: Whether or not the element is found in the message. + is_submsg: Whether the element is a sub-message itself. + convert: A representation conversion function. + linebreak: Whether or not a linebreak is to be used in the report. + indent: Indentation used for the report. + + Returns: + msg: A mock message object. + report: A mock report object. + subreport: A mock sub-report object. + name: An element name to check. + val: Expected element value. + """ + name = 'foo' + val = 'fake submsg' if is_submsg else 'fake field' + subreport = 'fake subreport' + + # Create a mock message. + msg = self.mox.CreateMock(update_metadata_pb2._message.Message) + msg.HasField(name).AndReturn(is_present) + setattr(msg, name, val) + + # Create a mock report. + report = self.mox.CreateMock(checker._PayloadReport) + if is_present: + if is_submsg: + report.AddSubReport(name).AndReturn(subreport) + else: + report.AddField(name, convert(val), linebreak=linebreak, indent=indent) + + self.mox.ReplayAll() + return (msg, report, subreport, name, val) + + def DoAddElemTest(self, is_present, is_mandatory, is_submsg, convert, + linebreak, indent): + """Parametric testing of _CheckElem(). + + Args: + is_present: Whether or not the element is found in the message. + is_mandatory: Whether or not it's a mandatory element. + is_submsg: Whether the element is a sub-message itself. + convert: A representation conversion function. + linebreak: Whether or not a linebreak is to be used in the report. + indent: Indentation used for the report. + """ + msg, report, subreport, name, val = self.SetupAddElemTest( + is_present, is_submsg, convert, linebreak, indent) + + args = (msg, name, report, is_mandatory, is_submsg) + kwargs = {'convert': convert, 'linebreak': linebreak, 'indent': indent} + if is_mandatory and not is_present: + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckElem, *args, **kwargs) + else: + ret_val, ret_subreport = checker.PayloadChecker._CheckElem(*args, + **kwargs) + self.assertEquals(val if is_present else None, ret_val) + self.assertEquals(subreport if is_present and is_submsg else None, + ret_subreport) + + def DoAddFieldTest(self, is_mandatory, is_present, convert, linebreak, + indent): + """Parametric testing of _Check{Mandatory,Optional}Field(). + + Args: + is_mandatory: Whether we're testing a mandatory call. + is_present: Whether or not the element is found in the message. + convert: A representation conversion function. + linebreak: Whether or not a linebreak is to be used in the report. + indent: Indentation used for the report. + """ + msg, report, _, name, val = self.SetupAddElemTest( + is_present, False, convert, linebreak, indent) + + # Prepare for invocation of the tested method. + args = [msg, name, report] + kwargs = {'convert': convert, 'linebreak': linebreak, 'indent': indent} + if is_mandatory: + args.append('bar') + tested_func = checker.PayloadChecker._CheckMandatoryField + else: + tested_func = checker.PayloadChecker._CheckOptionalField + + # Test the method call. + if is_mandatory and not is_present: + self.assertRaises(PayloadError, tested_func, *args, **kwargs) + else: + ret_val = tested_func(*args, **kwargs) + self.assertEquals(val if is_present else None, ret_val) + + def DoAddSubMsgTest(self, is_mandatory, is_present): + """Parametrized testing of _Check{Mandatory,Optional}SubMsg(). + + Args: + is_mandatory: Whether we're testing a mandatory call. + is_present: Whether or not the element is found in the message. + """ + msg, report, subreport, name, val = self.SetupAddElemTest(is_present, True) + + # Prepare for invocation of the tested method. + args = [msg, name, report] + if is_mandatory: + args.append('bar') + tested_func = checker.PayloadChecker._CheckMandatorySubMsg + else: + tested_func = checker.PayloadChecker._CheckOptionalSubMsg + + # Test the method call. + if is_mandatory and not is_present: + self.assertRaises(PayloadError, tested_func, *args) + else: + ret_val, ret_subreport = tested_func(*args) + self.assertEquals(val if is_present else None, ret_val) + self.assertEquals(subreport if is_present else None, ret_subreport) + + def testCheckPresentIff(self): + """Tests _CheckPresentIff().""" + self.assertIsNone(checker.PayloadChecker._CheckPresentIff( + None, None, 'foo', 'bar', 'baz')) + self.assertIsNone(checker.PayloadChecker._CheckPresentIff( + 'a', 'b', 'foo', 'bar', 'baz')) + self.assertRaises(PayloadError, checker.PayloadChecker._CheckPresentIff, + 'a', None, 'foo', 'bar', 'baz') + self.assertRaises(PayloadError, checker.PayloadChecker._CheckPresentIff, + None, 'b', 'foo', 'bar', 'baz') + + def DoCheckSha256SignatureTest(self, expect_pass, expect_subprocess_call, + sig_data, sig_asn1_header, + returned_signed_hash, expected_signed_hash): + """Parametric testing of _CheckSha256SignatureTest(). + + Args: + expect_pass: Whether or not it should pass. + expect_subprocess_call: Whether to expect the openssl call to happen. + sig_data: The signature raw data. + sig_asn1_header: The ASN1 header. + returned_signed_hash: The signed hash data retuned by openssl. + expected_signed_hash: The signed hash data to compare against. + """ + try: + # Stub out the subprocess invocation. + self.mox.StubOutWithMock(checker.PayloadChecker, '_Run') + if expect_subprocess_call: + checker.PayloadChecker._Run( + mox.IsA(list), send_data=sig_data).AndReturn( + (sig_asn1_header + returned_signed_hash, None)) + + self.mox.ReplayAll() + if expect_pass: + self.assertIsNone(checker.PayloadChecker._CheckSha256Signature( + sig_data, 'foo', expected_signed_hash, 'bar')) + else: + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckSha256Signature, + sig_data, 'foo', expected_signed_hash, 'bar') + finally: + self.mox.UnsetStubs() + + def testCheckSha256Signature_Pass(self): + """Tests _CheckSha256Signature(); pass case.""" + sig_data = 'fake-signature'.ljust(256) + signed_hash = hashlib.sha256('fake-data').digest() + self.DoCheckSha256SignatureTest(True, True, sig_data, + common.SIG_ASN1_HEADER, signed_hash, + signed_hash) + + def testCheckSha256Signature_FailBadSignature(self): + """Tests _CheckSha256Signature(); fails due to malformed signature.""" + sig_data = 'fake-signature' # Malformed (not 256 bytes in length). + signed_hash = hashlib.sha256('fake-data').digest() + self.DoCheckSha256SignatureTest(False, False, sig_data, + common.SIG_ASN1_HEADER, signed_hash, + signed_hash) + + def testCheckSha256Signature_FailBadOutputLength(self): + """Tests _CheckSha256Signature(); fails due to unexpected output length.""" + sig_data = 'fake-signature'.ljust(256) + signed_hash = 'fake-hash' # Malformed (not 32 bytes in length). + self.DoCheckSha256SignatureTest(False, True, sig_data, + common.SIG_ASN1_HEADER, signed_hash, + signed_hash) + + def testCheckSha256Signature_FailBadAsnHeader(self): + """Tests _CheckSha256Signature(); fails due to bad ASN1 header.""" + sig_data = 'fake-signature'.ljust(256) + signed_hash = hashlib.sha256('fake-data').digest() + bad_asn1_header = 'bad-asn-header'.ljust(len(common.SIG_ASN1_HEADER)) + self.DoCheckSha256SignatureTest(False, True, sig_data, bad_asn1_header, + signed_hash, signed_hash) + + def testCheckSha256Signature_FailBadHash(self): + """Tests _CheckSha256Signature(); fails due to bad hash returned.""" + sig_data = 'fake-signature'.ljust(256) + expected_signed_hash = hashlib.sha256('fake-data').digest() + returned_signed_hash = hashlib.sha256('bad-fake-data').digest() + self.DoCheckSha256SignatureTest(False, True, sig_data, + common.SIG_ASN1_HEADER, + expected_signed_hash, returned_signed_hash) + + def testCheckBlocksFitLength_Pass(self): + """Tests _CheckBlocksFitLength(); pass case.""" + self.assertIsNone(checker.PayloadChecker._CheckBlocksFitLength( + 64, 4, 16, 'foo')) + self.assertIsNone(checker.PayloadChecker._CheckBlocksFitLength( + 60, 4, 16, 'foo')) + self.assertIsNone(checker.PayloadChecker._CheckBlocksFitLength( + 49, 4, 16, 'foo')) + self.assertIsNone(checker.PayloadChecker._CheckBlocksFitLength( + 48, 3, 16, 'foo')) + + def testCheckBlocksFitLength_TooManyBlocks(self): + """Tests _CheckBlocksFitLength(); fails due to excess blocks.""" + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckBlocksFitLength, + 64, 5, 16, 'foo') + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckBlocksFitLength, + 60, 5, 16, 'foo') + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckBlocksFitLength, + 49, 5, 16, 'foo') + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckBlocksFitLength, + 48, 4, 16, 'foo') + + def testCheckBlocksFitLength_TooFewBlocks(self): + """Tests _CheckBlocksFitLength(); fails due to insufficient blocks.""" + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckBlocksFitLength, + 64, 3, 16, 'foo') + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckBlocksFitLength, + 60, 3, 16, 'foo') + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckBlocksFitLength, + 49, 3, 16, 'foo') + self.assertRaises(PayloadError, + checker.PayloadChecker._CheckBlocksFitLength, + 48, 2, 16, 'foo') + + def DoCheckManifestTest(self, fail_mismatched_block_size, fail_bad_sigs, + fail_mismatched_oki_ori, fail_bad_oki, fail_bad_ori, + fail_bad_nki, fail_bad_nri, fail_old_kernel_fs_size, + fail_old_rootfs_fs_size, fail_new_kernel_fs_size, + fail_new_rootfs_fs_size): + """Parametric testing of _CheckManifest(). + + Args: + fail_mismatched_block_size: Simulate a missing block_size field. + fail_bad_sigs: Make signatures descriptor inconsistent. + fail_mismatched_oki_ori: Make old rootfs/kernel info partially present. + fail_bad_oki: Tamper with old kernel info. + fail_bad_ori: Tamper with old rootfs info. + fail_bad_nki: Tamper with new kernel info. + fail_bad_nri: Tamper with new rootfs info. + fail_old_kernel_fs_size: Make old kernel fs size too big. + fail_old_rootfs_fs_size: Make old rootfs fs size too big. + fail_new_kernel_fs_size: Make new kernel fs size too big. + fail_new_rootfs_fs_size: Make new rootfs fs size too big. + """ + # Generate a test payload. For this test, we only care about the manifest + # and don't need any data blobs, hence we can use a plain paylaod generator + # (which also gives us more control on things that can be screwed up). + payload_gen = test_utils.PayloadGenerator() + + # Tamper with block size, if required. + if fail_mismatched_block_size: + payload_gen.SetBlockSize(test_utils.KiB(1)) + else: + payload_gen.SetBlockSize(test_utils.KiB(4)) + + # Add some operations. + payload_gen.AddOperation(False, common.OpType.MOVE, + src_extents=[(0, 16), (16, 497)], + dst_extents=[(16, 496), (0, 16)]) + payload_gen.AddOperation(True, common.OpType.MOVE, + src_extents=[(0, 8), (8, 8)], + dst_extents=[(8, 8), (0, 8)]) + + # Set an invalid signatures block (offset but no size), if required. + if fail_bad_sigs: + payload_gen.SetSignatures(32, None) + + # Set partition / filesystem sizes. + rootfs_part_size = test_utils.MiB(8) + kernel_part_size = test_utils.KiB(512) + old_rootfs_fs_size = new_rootfs_fs_size = rootfs_part_size + old_kernel_fs_size = new_kernel_fs_size = kernel_part_size + if fail_old_kernel_fs_size: + old_kernel_fs_size += 100 + if fail_old_rootfs_fs_size: + old_rootfs_fs_size += 100 + if fail_new_kernel_fs_size: + new_kernel_fs_size += 100 + if fail_new_rootfs_fs_size: + new_rootfs_fs_size += 100 + + # Add old kernel/rootfs partition info, as required. + if fail_mismatched_oki_ori or fail_old_kernel_fs_size or fail_bad_oki: + oki_hash = (None if fail_bad_oki + else hashlib.sha256('fake-oki-content').digest()) + payload_gen.SetPartInfo(True, False, old_kernel_fs_size, oki_hash) + if not fail_mismatched_oki_ori and (fail_old_rootfs_fs_size or + fail_bad_ori): + ori_hash = (None if fail_bad_ori + else hashlib.sha256('fake-ori-content').digest()) + payload_gen.SetPartInfo(False, False, old_rootfs_fs_size, ori_hash) + + # Add new kernel/rootfs partition info. + payload_gen.SetPartInfo( + True, True, new_kernel_fs_size, + None if fail_bad_nki else hashlib.sha256('fake-nki-content').digest()) + payload_gen.SetPartInfo( + False, True, new_rootfs_fs_size, + None if fail_bad_nri else hashlib.sha256('fake-nri-content').digest()) + + # Set the minor version. + payload_gen.SetMinorVersion(0) + + # Create the test object. + payload_checker = _GetPayloadChecker(payload_gen.WriteToFile) + report = checker._PayloadReport() + + should_fail = (fail_mismatched_block_size or fail_bad_sigs or + fail_mismatched_oki_ori or fail_bad_oki or fail_bad_ori or + fail_bad_nki or fail_bad_nri or fail_old_kernel_fs_size or + fail_old_rootfs_fs_size or fail_new_kernel_fs_size or + fail_new_rootfs_fs_size) + if should_fail: + self.assertRaises(PayloadError, payload_checker._CheckManifest, report, + rootfs_part_size, kernel_part_size) + else: + self.assertIsNone(payload_checker._CheckManifest(report, + rootfs_part_size, + kernel_part_size)) + + def testCheckLength(self): + """Tests _CheckLength().""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + block_size = payload_checker.block_size + + # Passes. + self.assertIsNone(payload_checker._CheckLength( + int(3.5 * block_size), 4, 'foo', 'bar')) + # Fails, too few blocks. + self.assertRaises(PayloadError, payload_checker._CheckLength, + int(3.5 * block_size), 3, 'foo', 'bar') + # Fails, too many blocks. + self.assertRaises(PayloadError, payload_checker._CheckLength, + int(3.5 * block_size), 5, 'foo', 'bar') + + def testCheckExtents(self): + """Tests _CheckExtents().""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + block_size = payload_checker.block_size + + # Passes w/ all real extents. + extents = self.NewExtentList((0, 4), (8, 3), (1024, 16)) + self.assertEquals( + 23, + payload_checker._CheckExtents(extents, (1024 + 16) * block_size, + collections.defaultdict(int), 'foo')) + + # Passes w/ pseudo-extents (aka sparse holes). + extents = self.NewExtentList((0, 4), (common.PSEUDO_EXTENT_MARKER, 5), + (8, 3)) + self.assertEquals( + 12, + payload_checker._CheckExtents(extents, (1024 + 16) * block_size, + collections.defaultdict(int), 'foo', + allow_pseudo=True)) + + # Passes w/ pseudo-extent due to a signature. + extents = self.NewExtentList((common.PSEUDO_EXTENT_MARKER, 2)) + self.assertEquals( + 2, + payload_checker._CheckExtents(extents, (1024 + 16) * block_size, + collections.defaultdict(int), 'foo', + allow_signature=True)) + + # Fails, extent missing a start block. + extents = self.NewExtentList((-1, 4), (8, 3), (1024, 16)) + self.assertRaises( + PayloadError, payload_checker._CheckExtents, extents, + (1024 + 16) * block_size, collections.defaultdict(int), 'foo') + + # Fails, extent missing block count. + extents = self.NewExtentList((0, -1), (8, 3), (1024, 16)) + self.assertRaises( + PayloadError, payload_checker._CheckExtents, extents, + (1024 + 16) * block_size, collections.defaultdict(int), 'foo') + + # Fails, extent has zero blocks. + extents = self.NewExtentList((0, 4), (8, 3), (1024, 0)) + self.assertRaises( + PayloadError, payload_checker._CheckExtents, extents, + (1024 + 16) * block_size, collections.defaultdict(int), 'foo') + + # Fails, extent exceeds partition boundaries. + extents = self.NewExtentList((0, 4), (8, 3), (1024, 16)) + self.assertRaises( + PayloadError, payload_checker._CheckExtents, extents, + (1024 + 15) * block_size, collections.defaultdict(int), 'foo') + + def testCheckReplaceOperation(self): + """Tests _CheckReplaceOperation() where op.type == REPLACE.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + block_size = payload_checker.block_size + data_length = 10000 + + op = self.mox.CreateMock( + update_metadata_pb2.InstallOperation) + op.type = common.OpType.REPLACE + + # Pass. + op.src_extents = [] + self.assertIsNone( + payload_checker._CheckReplaceOperation( + op, data_length, (data_length + block_size - 1) / block_size, + 'foo')) + + # Fail, src extents founds. + op.src_extents = ['bar'] + self.assertRaises( + PayloadError, payload_checker._CheckReplaceOperation, + op, data_length, (data_length + block_size - 1) / block_size, 'foo') + + # Fail, missing data. + op.src_extents = [] + self.assertRaises( + PayloadError, payload_checker._CheckReplaceOperation, + op, None, (data_length + block_size - 1) / block_size, 'foo') + + # Fail, length / block number mismatch. + op.src_extents = ['bar'] + self.assertRaises( + PayloadError, payload_checker._CheckReplaceOperation, + op, data_length, (data_length + block_size - 1) / block_size + 1, 'foo') + + def testCheckReplaceBzOperation(self): + """Tests _CheckReplaceOperation() where op.type == REPLACE_BZ.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + block_size = payload_checker.block_size + data_length = block_size * 3 + + op = self.mox.CreateMock( + update_metadata_pb2.InstallOperation) + op.type = common.OpType.REPLACE_BZ + + # Pass. + op.src_extents = [] + self.assertIsNone( + payload_checker._CheckReplaceOperation( + op, data_length, (data_length + block_size - 1) / block_size + 5, + 'foo')) + + # Fail, src extents founds. + op.src_extents = ['bar'] + self.assertRaises( + PayloadError, payload_checker._CheckReplaceOperation, + op, data_length, (data_length + block_size - 1) / block_size + 5, 'foo') + + # Fail, missing data. + op.src_extents = [] + self.assertRaises( + PayloadError, payload_checker._CheckReplaceOperation, + op, None, (data_length + block_size - 1) / block_size, 'foo') + + # Fail, too few blocks to justify BZ. + op.src_extents = [] + self.assertRaises( + PayloadError, payload_checker._CheckReplaceOperation, + op, data_length, (data_length + block_size - 1) / block_size, 'foo') + + def testCheckReplaceXzOperation(self): + """Tests _CheckReplaceOperation() where op.type == REPLACE_XZ.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + block_size = payload_checker.block_size + data_length = block_size * 3 + + op = self.mox.CreateMock( + update_metadata_pb2.InstallOperation) + op.type = common.OpType.REPLACE_XZ + + # Pass. + op.src_extents = [] + self.assertIsNone( + payload_checker._CheckReplaceOperation( + op, data_length, (data_length + block_size - 1) / block_size + 5, + 'foo')) + + # Fail, src extents founds. + op.src_extents = ['bar'] + self.assertRaises( + PayloadError, payload_checker._CheckReplaceOperation, + op, data_length, (data_length + block_size - 1) / block_size + 5, 'foo') + + # Fail, missing data. + op.src_extents = [] + self.assertRaises( + PayloadError, payload_checker._CheckReplaceOperation, + op, None, (data_length + block_size - 1) / block_size, 'foo') + + # Fail, too few blocks to justify XZ. + op.src_extents = [] + self.assertRaises( + PayloadError, payload_checker._CheckReplaceOperation, + op, data_length, (data_length + block_size - 1) / block_size, 'foo') + + def testCheckMoveOperation_Pass(self): + """Tests _CheckMoveOperation(); pass case.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + op = update_metadata_pb2.InstallOperation() + op.type = common.OpType.MOVE + + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 4), (12, 2), (1024, 128))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((16, 128), (512, 6))) + self.assertIsNone( + payload_checker._CheckMoveOperation(op, None, 134, 134, 'foo')) + + def testCheckMoveOperation_FailContainsData(self): + """Tests _CheckMoveOperation(); fails, message contains data.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + op = update_metadata_pb2.InstallOperation() + op.type = common.OpType.MOVE + + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 4), (12, 2), (1024, 128))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((16, 128), (512, 6))) + self.assertRaises( + PayloadError, payload_checker._CheckMoveOperation, + op, 1024, 134, 134, 'foo') + + def testCheckMoveOperation_FailInsufficientSrcBlocks(self): + """Tests _CheckMoveOperation(); fails, not enough actual src blocks.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + op = update_metadata_pb2.InstallOperation() + op.type = common.OpType.MOVE + + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 4), (12, 2), (1024, 127))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((16, 128), (512, 6))) + self.assertRaises( + PayloadError, payload_checker._CheckMoveOperation, + op, None, 134, 134, 'foo') + + def testCheckMoveOperation_FailInsufficientDstBlocks(self): + """Tests _CheckMoveOperation(); fails, not enough actual dst blocks.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + op = update_metadata_pb2.InstallOperation() + op.type = common.OpType.MOVE + + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 4), (12, 2), (1024, 128))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((16, 128), (512, 5))) + self.assertRaises( + PayloadError, payload_checker._CheckMoveOperation, + op, None, 134, 134, 'foo') + + def testCheckMoveOperation_FailExcessSrcBlocks(self): + """Tests _CheckMoveOperation(); fails, too many actual src blocks.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + op = update_metadata_pb2.InstallOperation() + op.type = common.OpType.MOVE + + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 4), (12, 2), (1024, 128))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((16, 128), (512, 5))) + self.assertRaises( + PayloadError, payload_checker._CheckMoveOperation, + op, None, 134, 134, 'foo') + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 4), (12, 2), (1024, 129))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((16, 128), (512, 6))) + self.assertRaises( + PayloadError, payload_checker._CheckMoveOperation, + op, None, 134, 134, 'foo') + + def testCheckMoveOperation_FailExcessDstBlocks(self): + """Tests _CheckMoveOperation(); fails, too many actual dst blocks.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + op = update_metadata_pb2.InstallOperation() + op.type = common.OpType.MOVE + + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 4), (12, 2), (1024, 128))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((16, 128), (512, 7))) + self.assertRaises( + PayloadError, payload_checker._CheckMoveOperation, + op, None, 134, 134, 'foo') + + def testCheckMoveOperation_FailStagnantBlocks(self): + """Tests _CheckMoveOperation(); fails, there are blocks that do not move.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + op = update_metadata_pb2.InstallOperation() + op.type = common.OpType.MOVE + + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 4), (12, 2), (1024, 128))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((8, 128), (512, 6))) + self.assertRaises( + PayloadError, payload_checker._CheckMoveOperation, + op, None, 134, 134, 'foo') + + def testCheckMoveOperation_FailZeroStartBlock(self): + """Tests _CheckMoveOperation(); fails, has extent with start block 0.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + op = update_metadata_pb2.InstallOperation() + op.type = common.OpType.MOVE + + self.AddToMessage(op.src_extents, + self.NewExtentList((0, 4), (12, 2), (1024, 128))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((8, 128), (512, 6))) + self.assertRaises( + PayloadError, payload_checker._CheckMoveOperation, + op, None, 134, 134, 'foo') + + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 4), (12, 2), (1024, 128))) + self.AddToMessage(op.dst_extents, + self.NewExtentList((0, 128), (512, 6))) + self.assertRaises( + PayloadError, payload_checker._CheckMoveOperation, + op, None, 134, 134, 'foo') + + def testCheckAnyDiff(self): + """Tests _CheckAnyDiffOperation().""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + op = update_metadata_pb2.InstallOperation() + + # Pass. + self.assertIsNone( + payload_checker._CheckAnyDiffOperation(op, 10000, 3, 'foo')) + + # Fail, missing data blob. + self.assertRaises( + PayloadError, payload_checker._CheckAnyDiffOperation, + op, None, 3, 'foo') + + # Fail, too big of a diff blob (unjustified). + self.assertRaises( + PayloadError, payload_checker._CheckAnyDiffOperation, + op, 10000, 2, 'foo') + + def testCheckSourceCopyOperation_Pass(self): + """Tests _CheckSourceCopyOperation(); pass case.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + self.assertIsNone( + payload_checker._CheckSourceCopyOperation(None, 134, 134, 'foo')) + + def testCheckSourceCopyOperation_FailContainsData(self): + """Tests _CheckSourceCopyOperation(); message contains data.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + self.assertRaises(PayloadError, payload_checker._CheckSourceCopyOperation, + 134, 0, 0, 'foo') + + def testCheckSourceCopyOperation_FailBlockCountsMismatch(self): + """Tests _CheckSourceCopyOperation(); src and dst block totals not equal.""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + self.assertRaises(PayloadError, payload_checker._CheckSourceCopyOperation, + None, 0, 1, 'foo') + + def DoCheckOperationTest(self, op_type_name, is_last, allow_signature, + allow_unhashed, fail_src_extents, fail_dst_extents, + fail_mismatched_data_offset_length, + fail_missing_dst_extents, fail_src_length, + fail_dst_length, fail_data_hash, + fail_prev_data_offset, fail_bad_minor_version): + """Parametric testing of _CheckOperation(). + + Args: + op_type_name: 'REPLACE', 'REPLACE_BZ', 'REPLACE_XZ', 'MOVE', 'BSDIFF', + 'SOURCE_COPY', 'SOURCE_BSDIFF', BROTLI_BSDIFF or 'PUFFDIFF'. + is_last: Whether we're testing the last operation in a sequence. + allow_signature: Whether we're testing a signature-capable operation. + allow_unhashed: Whether we're allowing to not hash the data. + fail_src_extents: Tamper with src extents. + fail_dst_extents: Tamper with dst extents. + fail_mismatched_data_offset_length: Make data_{offset,length} + inconsistent. + fail_missing_dst_extents: Do not include dst extents. + fail_src_length: Make src length inconsistent. + fail_dst_length: Make dst length inconsistent. + fail_data_hash: Tamper with the data blob hash. + fail_prev_data_offset: Make data space uses incontiguous. + fail_bad_minor_version: Make minor version incompatible with op. + """ + op_type = _OpTypeByName(op_type_name) + + # Create the test object. + payload = self.MockPayload() + payload_checker = checker.PayloadChecker(payload, + allow_unhashed=allow_unhashed) + block_size = payload_checker.block_size + + # Create auxiliary arguments. + old_part_size = test_utils.MiB(4) + new_part_size = test_utils.MiB(8) + old_block_counters = array.array( + 'B', [0] * ((old_part_size + block_size - 1) / block_size)) + new_block_counters = array.array( + 'B', [0] * ((new_part_size + block_size - 1) / block_size)) + prev_data_offset = 1876 + blob_hash_counts = collections.defaultdict(int) + + # Create the operation object for the test. + op = update_metadata_pb2.InstallOperation() + op.type = op_type + + total_src_blocks = 0 + if op_type in (common.OpType.MOVE, common.OpType.BSDIFF, + common.OpType.SOURCE_COPY, common.OpType.SOURCE_BSDIFF, + common.OpType.PUFFDIFF, common.OpType.BROTLI_BSDIFF): + if fail_src_extents: + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 0))) + else: + self.AddToMessage(op.src_extents, + self.NewExtentList((1, 16))) + total_src_blocks = 16 + + if op_type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ): + payload_checker.minor_version = 0 + elif op_type in (common.OpType.MOVE, common.OpType.BSDIFF): + payload_checker.minor_version = 2 if fail_bad_minor_version else 1 + elif op_type in (common.OpType.SOURCE_COPY, common.OpType.SOURCE_BSDIFF): + payload_checker.minor_version = 1 if fail_bad_minor_version else 2 + if op_type == common.OpType.REPLACE_XZ: + payload_checker.minor_version = 2 if fail_bad_minor_version else 3 + elif op_type in (common.OpType.ZERO, common.OpType.DISCARD, + common.OpType.BROTLI_BSDIFF): + payload_checker.minor_version = 3 if fail_bad_minor_version else 4 + elif op_type == common.OpType.PUFFDIFF: + payload_checker.minor_version = 4 if fail_bad_minor_version else 5 + + if op_type not in (common.OpType.MOVE, common.OpType.SOURCE_COPY): + if not fail_mismatched_data_offset_length: + op.data_length = 16 * block_size - 8 + if fail_prev_data_offset: + op.data_offset = prev_data_offset + 16 + else: + op.data_offset = prev_data_offset + + fake_data = 'fake-data'.ljust(op.data_length) + if not (allow_unhashed or (is_last and allow_signature and + op_type == common.OpType.REPLACE)): + if not fail_data_hash: + # Create a valid data blob hash. + op.data_sha256_hash = hashlib.sha256(fake_data).digest() + payload.ReadDataBlob(op.data_offset, op.data_length).AndReturn( + fake_data) + + elif fail_data_hash: + # Create an invalid data blob hash. + op.data_sha256_hash = hashlib.sha256( + fake_data.replace(' ', '-')).digest() + payload.ReadDataBlob(op.data_offset, op.data_length).AndReturn( + fake_data) + + total_dst_blocks = 0 + if not fail_missing_dst_extents: + total_dst_blocks = 16 + if fail_dst_extents: + self.AddToMessage(op.dst_extents, + self.NewExtentList((4, 16), (32, 0))) + else: + self.AddToMessage(op.dst_extents, + self.NewExtentList((4, 8), (64, 8))) + + if total_src_blocks: + if fail_src_length: + op.src_length = total_src_blocks * block_size + 8 + elif (op_type in (common.OpType.MOVE, common.OpType.BSDIFF, + common.OpType.SOURCE_BSDIFF) and + payload_checker.minor_version <= 3): + op.src_length = total_src_blocks * block_size + elif fail_src_length: + # Add an orphaned src_length. + op.src_length = 16 + + if total_dst_blocks: + if fail_dst_length: + op.dst_length = total_dst_blocks * block_size + 8 + elif (op_type in (common.OpType.MOVE, common.OpType.BSDIFF, + common.OpType.SOURCE_BSDIFF) and + payload_checker.minor_version <= 3): + op.dst_length = total_dst_blocks * block_size + + self.mox.ReplayAll() + should_fail = (fail_src_extents or fail_dst_extents or + fail_mismatched_data_offset_length or + fail_missing_dst_extents or fail_src_length or + fail_dst_length or fail_data_hash or fail_prev_data_offset or + fail_bad_minor_version) + args = (op, 'foo', is_last, old_block_counters, new_block_counters, + old_part_size, new_part_size, prev_data_offset, allow_signature, + blob_hash_counts) + if should_fail: + self.assertRaises(PayloadError, payload_checker._CheckOperation, *args) + else: + self.assertEqual(op.data_length if op.HasField('data_length') else 0, + payload_checker._CheckOperation(*args)) + + def testAllocBlockCounters(self): + """Tests _CheckMoveOperation().""" + payload_checker = checker.PayloadChecker(self.MockPayload()) + block_size = payload_checker.block_size + + # Check allocation for block-aligned partition size, ensure it's integers. + result = payload_checker._AllocBlockCounters(16 * block_size) + self.assertEqual(16, len(result)) + self.assertEqual(int, type(result[0])) + + # Check allocation of unaligned partition sizes. + result = payload_checker._AllocBlockCounters(16 * block_size - 1) + self.assertEqual(16, len(result)) + result = payload_checker._AllocBlockCounters(16 * block_size + 1) + self.assertEqual(17, len(result)) + + def DoCheckOperationsTest(self, fail_nonexhaustive_full_update): + """Tests _CheckOperations().""" + # Generate a test payload. For this test, we only care about one + # (arbitrary) set of operations, so we'll only be generating kernel and + # test with them. + payload_gen = test_utils.PayloadGenerator() + + block_size = test_utils.KiB(4) + payload_gen.SetBlockSize(block_size) + + rootfs_part_size = test_utils.MiB(8) + + # Fake rootfs operations in a full update, tampered with as required. + rootfs_op_type = common.OpType.REPLACE + rootfs_data_length = rootfs_part_size + if fail_nonexhaustive_full_update: + rootfs_data_length -= block_size + + payload_gen.AddOperation(False, rootfs_op_type, + dst_extents=[(0, rootfs_data_length / block_size)], + data_offset=0, + data_length=rootfs_data_length) + + # Create the test object. + payload_checker = _GetPayloadChecker(payload_gen.WriteToFile, + checker_init_dargs={ + 'allow_unhashed': True}) + payload_checker.payload_type = checker._TYPE_FULL + report = checker._PayloadReport() + + args = (payload_checker.payload.manifest.install_operations, report, 'foo', + 0, rootfs_part_size, rootfs_part_size, rootfs_part_size, 0, False) + if fail_nonexhaustive_full_update: + self.assertRaises(PayloadError, payload_checker._CheckOperations, *args) + else: + self.assertEqual(rootfs_data_length, + payload_checker._CheckOperations(*args)) + + def DoCheckSignaturesTest(self, fail_empty_sigs_blob, fail_missing_pseudo_op, + fail_mismatched_pseudo_op, fail_sig_missing_fields, + fail_unknown_sig_version, fail_incorrect_sig): + """Tests _CheckSignatures().""" + # Generate a test payload. For this test, we only care about the signature + # block and how it relates to the payload hash. Therefore, we're generating + # a random (otherwise useless) payload for this purpose. + payload_gen = test_utils.EnhancedPayloadGenerator() + block_size = test_utils.KiB(4) + payload_gen.SetBlockSize(block_size) + rootfs_part_size = test_utils.MiB(2) + kernel_part_size = test_utils.KiB(16) + payload_gen.SetPartInfo(False, True, rootfs_part_size, + hashlib.sha256('fake-new-rootfs-content').digest()) + payload_gen.SetPartInfo(True, True, kernel_part_size, + hashlib.sha256('fake-new-kernel-content').digest()) + payload_gen.SetMinorVersion(0) + payload_gen.AddOperationWithData( + False, common.OpType.REPLACE, + dst_extents=[(0, rootfs_part_size / block_size)], + data_blob=os.urandom(rootfs_part_size)) + + do_forge_pseudo_op = (fail_missing_pseudo_op or fail_mismatched_pseudo_op) + do_forge_sigs_data = (do_forge_pseudo_op or fail_empty_sigs_blob or + fail_sig_missing_fields or fail_unknown_sig_version + or fail_incorrect_sig) + + sigs_data = None + if do_forge_sigs_data: + sigs_gen = test_utils.SignaturesGenerator() + if not fail_empty_sigs_blob: + if fail_sig_missing_fields: + sig_data = None + else: + sig_data = test_utils.SignSha256('fake-payload-content', + test_utils._PRIVKEY_FILE_NAME) + sigs_gen.AddSig(5 if fail_unknown_sig_version else 1, sig_data) + + sigs_data = sigs_gen.ToBinary() + payload_gen.SetSignatures(payload_gen.curr_offset, len(sigs_data)) + + if do_forge_pseudo_op: + assert sigs_data is not None, 'should have forged signatures blob by now' + sigs_len = len(sigs_data) + payload_gen.AddOperation( + False, common.OpType.REPLACE, + data_offset=payload_gen.curr_offset / 2, + data_length=sigs_len / 2, + dst_extents=[(0, (sigs_len / 2 + block_size - 1) / block_size)]) + + # Generate payload (complete w/ signature) and create the test object. + payload_checker = _GetPayloadChecker( + payload_gen.WriteToFileWithData, + payload_gen_dargs={ + 'sigs_data': sigs_data, + 'privkey_file_name': test_utils._PRIVKEY_FILE_NAME, + 'do_add_pseudo_operation': not do_forge_pseudo_op}) + payload_checker.payload_type = checker._TYPE_FULL + report = checker._PayloadReport() + + # We have to check the manifest first in order to set signature attributes. + payload_checker._CheckManifest(report, rootfs_part_size, kernel_part_size) + + should_fail = (fail_empty_sigs_blob or fail_missing_pseudo_op or + fail_mismatched_pseudo_op or fail_sig_missing_fields or + fail_unknown_sig_version or fail_incorrect_sig) + args = (report, test_utils._PUBKEY_FILE_NAME) + if should_fail: + self.assertRaises(PayloadError, payload_checker._CheckSignatures, *args) + else: + self.assertIsNone(payload_checker._CheckSignatures(*args)) + + def DoCheckManifestMinorVersionTest(self, minor_version, payload_type): + """Parametric testing for CheckManifestMinorVersion(). + + Args: + minor_version: The payload minor version to test with. + payload_type: The type of the payload we're testing, delta or full. + """ + # Create the test object. + payload = self.MockPayload() + payload.manifest.minor_version = minor_version + payload_checker = checker.PayloadChecker(payload) + payload_checker.payload_type = payload_type + report = checker._PayloadReport() + + should_succeed = ( + (minor_version == 0 and payload_type == checker._TYPE_FULL) or + (minor_version == 1 and payload_type == checker._TYPE_DELTA) or + (minor_version == 2 and payload_type == checker._TYPE_DELTA) or + (minor_version == 3 and payload_type == checker._TYPE_DELTA) or + (minor_version == 4 and payload_type == checker._TYPE_DELTA) or + (minor_version == 5 and payload_type == checker._TYPE_DELTA)) + args = (report,) + + if should_succeed: + self.assertIsNone(payload_checker._CheckManifestMinorVersion(*args)) + else: + self.assertRaises(PayloadError, + payload_checker._CheckManifestMinorVersion, *args) + + def DoRunTest(self, rootfs_part_size_provided, kernel_part_size_provided, + fail_wrong_payload_type, fail_invalid_block_size, + fail_mismatched_block_size, fail_excess_data, + fail_rootfs_part_size_exceeded, + fail_kernel_part_size_exceeded): + """Tests Run().""" + # Generate a test payload. For this test, we generate a full update that + # has sample kernel and rootfs operations. Since most testing is done with + # internal PayloadChecker methods that are tested elsewhere, here we only + # tamper with what's actually being manipulated and/or tested in the Run() + # method itself. Note that the checker doesn't verify partition hashes, so + # they're safe to fake. + payload_gen = test_utils.EnhancedPayloadGenerator() + block_size = test_utils.KiB(4) + payload_gen.SetBlockSize(block_size) + kernel_filesystem_size = test_utils.KiB(16) + rootfs_filesystem_size = test_utils.MiB(2) + payload_gen.SetPartInfo(False, True, rootfs_filesystem_size, + hashlib.sha256('fake-new-rootfs-content').digest()) + payload_gen.SetPartInfo(True, True, kernel_filesystem_size, + hashlib.sha256('fake-new-kernel-content').digest()) + payload_gen.SetMinorVersion(0) + + rootfs_part_size = 0 + if rootfs_part_size_provided: + rootfs_part_size = rootfs_filesystem_size + block_size + rootfs_op_size = rootfs_part_size or rootfs_filesystem_size + if fail_rootfs_part_size_exceeded: + rootfs_op_size += block_size + payload_gen.AddOperationWithData( + False, common.OpType.REPLACE, + dst_extents=[(0, rootfs_op_size / block_size)], + data_blob=os.urandom(rootfs_op_size)) + + kernel_part_size = 0 + if kernel_part_size_provided: + kernel_part_size = kernel_filesystem_size + block_size + kernel_op_size = kernel_part_size or kernel_filesystem_size + if fail_kernel_part_size_exceeded: + kernel_op_size += block_size + payload_gen.AddOperationWithData( + True, common.OpType.REPLACE, + dst_extents=[(0, kernel_op_size / block_size)], + data_blob=os.urandom(kernel_op_size)) + + # Generate payload (complete w/ signature) and create the test object. + if fail_invalid_block_size: + use_block_size = block_size + 5 # Not a power of two. + elif fail_mismatched_block_size: + use_block_size = block_size * 2 # Different that payload stated. + else: + use_block_size = block_size + + kwargs = { + 'payload_gen_dargs': { + 'privkey_file_name': test_utils._PRIVKEY_FILE_NAME, + 'do_add_pseudo_operation': True, + 'is_pseudo_in_kernel': True, + 'padding': os.urandom(1024) if fail_excess_data else None}, + 'checker_init_dargs': { + 'assert_type': 'delta' if fail_wrong_payload_type else 'full', + 'block_size': use_block_size}} + if fail_invalid_block_size: + self.assertRaises(PayloadError, _GetPayloadChecker, + payload_gen.WriteToFileWithData, **kwargs) + else: + payload_checker = _GetPayloadChecker(payload_gen.WriteToFileWithData, + **kwargs) + + kwargs = {'pubkey_file_name': test_utils._PUBKEY_FILE_NAME, + 'rootfs_part_size': rootfs_part_size, + 'kernel_part_size': kernel_part_size} + should_fail = (fail_wrong_payload_type or fail_mismatched_block_size or + fail_excess_data or + fail_rootfs_part_size_exceeded or + fail_kernel_part_size_exceeded) + if should_fail: + self.assertRaises(PayloadError, payload_checker.Run, **kwargs) + else: + self.assertIsNone(payload_checker.Run(**kwargs)) + +# This implements a generic API, hence the occasional unused args. +# pylint: disable=W0613 +def ValidateCheckOperationTest(op_type_name, is_last, allow_signature, + allow_unhashed, fail_src_extents, + fail_dst_extents, + fail_mismatched_data_offset_length, + fail_missing_dst_extents, fail_src_length, + fail_dst_length, fail_data_hash, + fail_prev_data_offset, fail_bad_minor_version): + """Returns True iff the combination of arguments represents a valid test.""" + op_type = _OpTypeByName(op_type_name) + + # REPLACE/REPLACE_BZ/REPLACE_XZ operations don't read data from src + # partition. They are compatible with all valid minor versions, so we don't + # need to check that. + if (op_type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ, + common.OpType.REPLACE_XZ) and (fail_src_extents or + fail_src_length or + fail_bad_minor_version)): + return False + + # MOVE and SOURCE_COPY operations don't carry data. + if (op_type in (common.OpType.MOVE, common.OpType.SOURCE_COPY) and ( + fail_mismatched_data_offset_length or fail_data_hash or + fail_prev_data_offset)): + return False + + return True + + +def TestMethodBody(run_method_name, run_dargs): + """Returns a function that invokes a named method with named arguments.""" + return lambda self: getattr(self, run_method_name)(**run_dargs) + + +def AddParametricTests(tested_method_name, arg_space, validate_func=None): + """Enumerates and adds specific parametric tests to PayloadCheckerTest. + + This function enumerates a space of test parameters (defined by arg_space), + then binds a new, unique method name in PayloadCheckerTest to a test function + that gets handed the said parameters. This is a preferable approach to doing + the enumeration and invocation during the tests because this way each test is + treated as a complete run by the unittest framework, and so benefits from the + usual setUp/tearDown mechanics. + + Args: + tested_method_name: Name of the tested PayloadChecker method. + arg_space: A dictionary containing variables (keys) and lists of values + (values) associated with them. + validate_func: A function used for validating test argument combinations. + """ + for value_tuple in itertools.product(*arg_space.itervalues()): + run_dargs = dict(zip(arg_space.iterkeys(), value_tuple)) + if validate_func and not validate_func(**run_dargs): + continue + run_method_name = 'Do%sTest' % tested_method_name + test_method_name = 'test%s' % tested_method_name + for arg_key, arg_val in run_dargs.iteritems(): + if arg_val or type(arg_val) is int: + test_method_name += '__%s=%s' % (arg_key, arg_val) + setattr(PayloadCheckerTest, test_method_name, + TestMethodBody(run_method_name, run_dargs)) + + +def AddAllParametricTests(): + """Enumerates and adds all parametric tests to PayloadCheckerTest.""" + # Add all _CheckElem() test cases. + AddParametricTests('AddElem', + {'linebreak': (True, False), + 'indent': (0, 1, 2), + 'convert': (str, lambda s: s[::-1]), + 'is_present': (True, False), + 'is_mandatory': (True, False), + 'is_submsg': (True, False)}) + + # Add all _Add{Mandatory,Optional}Field tests. + AddParametricTests('AddField', + {'is_mandatory': (True, False), + 'linebreak': (True, False), + 'indent': (0, 1, 2), + 'convert': (str, lambda s: s[::-1]), + 'is_present': (True, False)}) + + # Add all _Add{Mandatory,Optional}SubMsg tests. + AddParametricTests('AddSubMsg', + {'is_mandatory': (True, False), + 'is_present': (True, False)}) + + # Add all _CheckManifest() test cases. + AddParametricTests('CheckManifest', + {'fail_mismatched_block_size': (True, False), + 'fail_bad_sigs': (True, False), + 'fail_mismatched_oki_ori': (True, False), + 'fail_bad_oki': (True, False), + 'fail_bad_ori': (True, False), + 'fail_bad_nki': (True, False), + 'fail_bad_nri': (True, False), + 'fail_old_kernel_fs_size': (True, False), + 'fail_old_rootfs_fs_size': (True, False), + 'fail_new_kernel_fs_size': (True, False), + 'fail_new_rootfs_fs_size': (True, False)}) + + # Add all _CheckOperation() test cases. + AddParametricTests('CheckOperation', + {'op_type_name': ('REPLACE', 'REPLACE_BZ', 'REPLACE_XZ', + 'MOVE', 'BSDIFF', 'SOURCE_COPY', + 'SOURCE_BSDIFF', 'PUFFDIFF', + 'BROTLI_BSDIFF'), + 'is_last': (True, False), + 'allow_signature': (True, False), + 'allow_unhashed': (True, False), + 'fail_src_extents': (True, False), + 'fail_dst_extents': (True, False), + 'fail_mismatched_data_offset_length': (True, False), + 'fail_missing_dst_extents': (True, False), + 'fail_src_length': (True, False), + 'fail_dst_length': (True, False), + 'fail_data_hash': (True, False), + 'fail_prev_data_offset': (True, False), + 'fail_bad_minor_version': (True, False)}, + validate_func=ValidateCheckOperationTest) + + # Add all _CheckOperations() test cases. + AddParametricTests('CheckOperations', + {'fail_nonexhaustive_full_update': (True, False)}) + + # Add all _CheckOperations() test cases. + AddParametricTests('CheckSignatures', + {'fail_empty_sigs_blob': (True, False), + 'fail_missing_pseudo_op': (True, False), + 'fail_mismatched_pseudo_op': (True, False), + 'fail_sig_missing_fields': (True, False), + 'fail_unknown_sig_version': (True, False), + 'fail_incorrect_sig': (True, False)}) + + # Add all _CheckManifestMinorVersion() test cases. + AddParametricTests('CheckManifestMinorVersion', + {'minor_version': (None, 0, 1, 2, 3, 4, 5, 555), + 'payload_type': (checker._TYPE_FULL, + checker._TYPE_DELTA)}) + + # Add all Run() test cases. + AddParametricTests('Run', + {'rootfs_part_size_provided': (True, False), + 'kernel_part_size_provided': (True, False), + 'fail_wrong_payload_type': (True, False), + 'fail_invalid_block_size': (True, False), + 'fail_mismatched_block_size': (True, False), + 'fail_excess_data': (True, False), + 'fail_rootfs_part_size_exceeded': (True, False), + 'fail_kernel_part_size_exceeded': (True, False)}) + + +if __name__ == '__main__': + AddAllParametricTests() + unittest.main() diff --git a/update_payload/common.py b/update_payload/common.py new file mode 100644 index 0000000..4e7b2e3 --- /dev/null +++ b/update_payload/common.py @@ -0,0 +1,218 @@ +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Utilities for update payload processing.""" + +from __future__ import print_function + +from update_payload import update_metadata_pb2 +from update_payload.error import PayloadError + + +# +# Constants. +# +PSEUDO_EXTENT_MARKER = (1L << 64) - 1 # UINT64_MAX + +SIG_ASN1_HEADER = ( + '\x30\x31\x30\x0d\x06\x09\x60\x86' + '\x48\x01\x65\x03\x04\x02\x01\x05' + '\x00\x04\x20' +) + +CHROMEOS_MAJOR_PAYLOAD_VERSION = 1 +BRILLO_MAJOR_PAYLOAD_VERSION = 2 + +INPLACE_MINOR_PAYLOAD_VERSION = 1 +SOURCE_MINOR_PAYLOAD_VERSION = 2 +OPSRCHASH_MINOR_PAYLOAD_VERSION = 3 +BROTLI_BSDIFF_MINOR_PAYLOAD_VERSION = 4 +PUFFDIFF_MINOR_PAYLOAD_VERSION = 5 + +# +# Payload operation types. +# +class OpType(object): + """Container for operation type constants.""" + _CLASS = update_metadata_pb2.InstallOperation + REPLACE = _CLASS.REPLACE + REPLACE_BZ = _CLASS.REPLACE_BZ + MOVE = _CLASS.MOVE + BSDIFF = _CLASS.BSDIFF + SOURCE_COPY = _CLASS.SOURCE_COPY + SOURCE_BSDIFF = _CLASS.SOURCE_BSDIFF + ZERO = _CLASS.ZERO + DISCARD = _CLASS.DISCARD + REPLACE_XZ = _CLASS.REPLACE_XZ + PUFFDIFF = _CLASS.PUFFDIFF + BROTLI_BSDIFF = _CLASS.BROTLI_BSDIFF + ALL = (REPLACE, REPLACE_BZ, MOVE, BSDIFF, SOURCE_COPY, SOURCE_BSDIFF, ZERO, + DISCARD, REPLACE_XZ, PUFFDIFF, BROTLI_BSDIFF) + NAMES = { + REPLACE: 'REPLACE', + REPLACE_BZ: 'REPLACE_BZ', + MOVE: 'MOVE', + BSDIFF: 'BSDIFF', + SOURCE_COPY: 'SOURCE_COPY', + SOURCE_BSDIFF: 'SOURCE_BSDIFF', + ZERO: 'ZERO', + DISCARD: 'DISCARD', + REPLACE_XZ: 'REPLACE_XZ', + PUFFDIFF: 'PUFFDIFF', + BROTLI_BSDIFF: 'BROTLI_BSDIFF', + } + + def __init__(self): + pass + + +# +# Checked and hashed reading of data. +# +def IntPackingFmtStr(size, is_unsigned): + """Returns an integer format string for use by the struct module. + + Args: + size: the integer size in bytes (2, 4 or 8) + is_unsigned: whether it is signed or not + + Returns: + A format string for packing/unpacking integer values; assumes network byte + order (big-endian). + + Raises: + PayloadError if something is wrong with the arguments. + """ + # Determine the base conversion format. + if size == 2: + fmt = 'h' + elif size == 4: + fmt = 'i' + elif size == 8: + fmt = 'q' + else: + raise PayloadError('unsupport numeric field size (%s)' % size) + + # Signed or unsigned? + if is_unsigned: + fmt = fmt.upper() + + # Make it network byte order (big-endian). + fmt = '!' + fmt + + return fmt + + +def Read(file_obj, length, offset=None, hasher=None): + """Reads binary data from a file. + + Args: + file_obj: an open file object + length: the length of the data to read + offset: an offset to seek to prior to reading; this is an absolute offset + from either the beginning (non-negative) or end (negative) of the + file. (optional) + hasher: a hashing object to pass the read data through (optional) + + Returns: + A string containing the read data. + + Raises: + PayloadError if a read error occurred or not enough data was read. + """ + if offset is not None: + if offset >= 0: + file_obj.seek(offset) + else: + file_obj.seek(offset, 2) + + try: + data = file_obj.read(length) + except IOError, e: + raise PayloadError('error reading from file (%s): %s' % (file_obj.name, e)) + + if len(data) != length: + raise PayloadError( + 'reading from file (%s) too short (%d instead of %d bytes)' % + (file_obj.name, len(data), length)) + + if hasher: + hasher.update(data) + + return data + + +# +# Formatting functions. +# +def FormatExtent(ex, block_size=0): + end_block = ex.start_block + ex.num_blocks + if block_size: + return '%d->%d * %d' % (ex.start_block, end_block, block_size) + else: + return '%d->%d' % (ex.start_block, end_block) + + +def FormatSha256(digest): + """Returns a canonical string representation of a SHA256 digest.""" + return digest.encode('base64').strip() + + +# +# Useful iterators. +# +def _ObjNameIter(items, base_name, reverse=False, name_format_func=None): + """A generic (item, name) tuple iterators. + + Args: + items: the sequence of objects to iterate on + base_name: the base name for all objects + reverse: whether iteration should be in reverse order + name_format_func: a function to apply to the name string + + Yields: + An iterator whose i-th invocation returns (items[i], name), where name == + base_name + '[i]' (with a formatting function optionally applied to it). + """ + idx, inc = (len(items), -1) if reverse else (1, 1) + if reverse: + items = reversed(items) + for item in items: + item_name = '%s[%d]' % (base_name, idx) + if name_format_func: + item_name = name_format_func(item, item_name) + yield (item, item_name) + idx += inc + + +def _OperationNameFormatter(op, op_name): + return '%s(%s)' % (op_name, OpType.NAMES.get(op.type, '?')) + + +def OperationIter(operations, base_name, reverse=False): + """An (item, name) iterator for update operations.""" + return _ObjNameIter(operations, base_name, reverse=reverse, + name_format_func=_OperationNameFormatter) + + +def ExtentIter(extents, base_name, reverse=False): + """An (item, name) iterator for operation extents.""" + return _ObjNameIter(extents, base_name, reverse=reverse) + + +def SignatureIter(sigs, base_name, reverse=False): + """An (item, name) iterator for signatures.""" + return _ObjNameIter(sigs, base_name, reverse=reverse) diff --git a/update_payload/error.py b/update_payload/error.py new file mode 100644 index 0000000..6f95433 --- /dev/null +++ b/update_payload/error.py @@ -0,0 +1,21 @@ +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Payload handling errors.""" + + +class PayloadError(Exception): + """An update payload general processing error.""" diff --git a/update_payload/format_utils.py b/update_payload/format_utils.py new file mode 100644 index 0000000..6248ba9 --- /dev/null +++ b/update_payload/format_utils.py @@ -0,0 +1,109 @@ +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Various formatting functions.""" + + +def NumToPercent(num, total, min_precision=1, max_precision=5): + """Returns the percentage (string) of |num| out of |total|. + + If the percentage includes a fraction, it will be computed down to the least + precision that yields a non-zero and ranging between |min_precision| and + |max_precision|. Values are always rounded down. All arithmetic operations + are integer built-ins. Examples (using default precision): + + (1, 1) => 100% + (3, 10) => 30% + (3, 9) => 33.3% + (3, 900) => 0.3% + (3, 9000000) => 0.00003% + (3, 900000000) => 0% + (5, 2) => 250% + + Args: + num: the value of the part + total: the value of the whole + min_precision: minimum precision for fractional percentage + max_precision: maximum precision for fractional percentage + Returns: + Percentage string, or None if percent cannot be computed (i.e. total is + zero). + + """ + if total == 0: + return None + + percent = 0 + precision = min(min_precision, max_precision) + factor = 10 ** precision + while precision <= max_precision: + percent = num * 100 * factor / total + if percent: + break + factor *= 10 + precision += 1 + + whole, frac = divmod(percent, factor) + while frac and not frac % 10: + frac /= 10 + precision -= 1 + + return '%d%s%%' % (whole, '.%0*d' % (precision, frac) if frac else '') + + +def BytesToHumanReadable(size, precision=1, decimal=False): + """Returns a human readable representation of a given |size|. + + The returned string includes unit notations in either binary (KiB, MiB, etc) + or decimal (kB, MB, etc), based on the value of |decimal|. The chosen unit is + the largest that yields a whole (or mixed) number. It may contain up to + |precision| fractional digits. Values are always rounded down. Largest unit + is an exabyte. All arithmetic operations are integer built-ins. Examples + (using default precision and binary units): + + 4096 => 4 KiB + 5000 => 4.8 KiB + 500000 => 488.2 KiB + 5000000 => 4.7 MiB + + Args: + size: the size in bytes + precision: the number of digits past the decimal point + decimal: whether to compute/present decimal or binary units + Returns: + Readable size string, or None if no conversion is applicable (i.e. size is + less than the smallest unit). + + """ + constants = ( + (('KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'), 1024), + (('kB', 'MB', 'GB', 'TB', 'PB', 'EB'), 1000) + ) + suffixes, base = constants[decimal] + exp, magnitude = 0, 1 + while exp < len(suffixes): + next_magnitude = magnitude * base + if size < next_magnitude: + break + exp += 1 + magnitude = next_magnitude + + if exp != 0: + whole = size / magnitude + frac = (size % magnitude) * (10 ** precision) / magnitude + while frac and not frac % 10: + frac /= 10 + return '%d%s %s' % (whole, '.%d' % frac if frac else '', suffixes[exp - 1]) diff --git a/update_payload/format_utils_unittest.py b/update_payload/format_utils_unittest.py new file mode 100755 index 0000000..42ea621 --- /dev/null +++ b/update_payload/format_utils_unittest.py @@ -0,0 +1,89 @@ +#!/usr/bin/python2 +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Unit tests for format_utils.py.""" + +import unittest + +from update_payload import format_utils + + +class NumToPercentTest(unittest.TestCase): + """ Tests number conversion to percentage format.""" + def testHundredPercent(self): + self.assertEqual(format_utils.NumToPercent(1, 1), '100%') + + def testOverHundredPercent(self): + self.assertEqual(format_utils.NumToPercent(5, 2), '250%') + + def testWholePercent(self): + self.assertEqual(format_utils.NumToPercent(3, 10), '30%') + + def testDefaultMinPrecision(self): + self.assertEqual(format_utils.NumToPercent(3, 9), '33.3%') + self.assertEqual(format_utils.NumToPercent(3, 900), '0.3%') + + def testDefaultMaxPrecision(self): + self.assertEqual(format_utils.NumToPercent(3, 9000000), '0.00003%') + self.assertEqual(format_utils.NumToPercent(3, 90000000), '0%') + + def testCustomMinPrecision(self): + self.assertEqual(format_utils.NumToPercent(3, 9, min_precision=3), + '33.333%') + self.assertEqual(format_utils.NumToPercent(3, 9, min_precision=0), + '33%') + + def testCustomMaxPrecision(self): + self.assertEqual(format_utils.NumToPercent(3, 900, max_precision=1), + '0.3%') + self.assertEqual(format_utils.NumToPercent(3, 9000, max_precision=1), + '0%') + + +class BytesToHumanReadableTest(unittest.TestCase): + """ Tests number conversion to human readable format.""" + def testBaseTwo(self): + self.assertEqual(format_utils.BytesToHumanReadable(0x1000), '4 KiB') + self.assertEqual(format_utils.BytesToHumanReadable(0x400000), '4 MiB') + self.assertEqual(format_utils.BytesToHumanReadable(0x100000000), '4 GiB') + self.assertEqual(format_utils.BytesToHumanReadable(0x40000000000), '4 TiB') + + def testDecimal(self): + self.assertEqual(format_utils.BytesToHumanReadable(5000, decimal=True), + '5 kB') + self.assertEqual(format_utils.BytesToHumanReadable(5000000, decimal=True), + '5 MB') + self.assertEqual(format_utils.BytesToHumanReadable(5000000000, + decimal=True), + '5 GB') + + def testDefaultPrecision(self): + self.assertEqual(format_utils.BytesToHumanReadable(5000), '4.8 KiB') + self.assertEqual(format_utils.BytesToHumanReadable(500000), '488.2 KiB') + self.assertEqual(format_utils.BytesToHumanReadable(5000000), '4.7 MiB') + + def testCustomPrecision(self): + self.assertEqual(format_utils.BytesToHumanReadable(5000, precision=3), + '4.882 KiB') + self.assertEqual(format_utils.BytesToHumanReadable(500000, precision=0), + '488 KiB') + self.assertEqual(format_utils.BytesToHumanReadable(5000000, precision=5), + '4.76837 MiB') + + +if __name__ == '__main__': + unittest.main() diff --git a/update_payload/histogram.py b/update_payload/histogram.py new file mode 100644 index 0000000..1ac2ab5 --- /dev/null +++ b/update_payload/histogram.py @@ -0,0 +1,129 @@ +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Histogram generation tools.""" + +from collections import defaultdict + +from update_payload import format_utils + + +class Histogram(object): + """A histogram generating object. + + This object serves the sole purpose of formatting (key, val) pairs as an + ASCII histogram, including bars and percentage markers, and taking care of + label alignment, scaling, etc. In addition to the standard __init__ + interface, two static methods are provided for conveniently converting data + in different formats into a histogram. Histogram generation is exported via + its __str__ method, and looks as follows: + + Yes |################ | 5 (83.3%) + No |### | 1 (16.6%) + + TODO(garnold) we may want to add actual methods for adding data or tweaking + the output layout and formatting. For now, though, this is fine. + + """ + + def __init__(self, data, scale=20, formatter=None): + """Initialize a histogram object. + + Args: + data: list of (key, count) pairs constituting the histogram + scale: number of characters used to indicate 100% + formatter: function used for formatting raw histogram values + + """ + self.data = data + self.scale = scale + self.formatter = formatter or str + self.max_key_len = max([len(str(key)) for key, count in self.data]) + self.total = sum([count for key, count in self.data]) + + @staticmethod + def FromCountDict(count_dict, scale=20, formatter=None, key_names=None): + """Takes a dictionary of counts and returns a histogram object. + + This simply converts a mapping from names to counts into a list of (key, + count) pairs, optionally translating keys into name strings, then + generating and returning a histogram for them. This is a useful convenience + call for clients that update a dictionary of counters as they (say) scan a + data stream. + + Args: + count_dict: dictionary mapping keys to occurrence counts + scale: number of characters used to indicate 100% + formatter: function used for formatting raw histogram values + key_names: dictionary mapping keys to name strings + Returns: + A histogram object based on the given data. + + """ + namer = None + if key_names: + namer = lambda key: key_names[key] + else: + namer = lambda key: key + + hist = [(namer(key), count) for key, count in count_dict.items()] + return Histogram(hist, scale, formatter) + + @staticmethod + def FromKeyList(key_list, scale=20, formatter=None, key_names=None): + """Takes a list of (possibly recurring) keys and returns a histogram object. + + This converts the list into a dictionary of counters, then uses + FromCountDict() to generate the actual histogram. For example: + + ['a', 'a', 'b', 'a', 'b'] --> {'a': 3, 'b': 2} --> ... + + Args: + key_list: list of (possibly recurring) keys + scale: number of characters used to indicate 100% + formatter: function used for formatting raw histogram values + key_names: dictionary mapping keys to name strings + Returns: + A histogram object based on the given data. + + """ + count_dict = defaultdict(int) # Unset items default to zero + for key in key_list: + count_dict[key] += 1 + return Histogram.FromCountDict(count_dict, scale, formatter, key_names) + + def __str__(self): + hist_lines = [] + hist_bar = '|' + for key, count in self.data: + if self.total: + bar_len = count * self.scale / self.total + hist_bar = '|%s|' % ('#' * bar_len).ljust(self.scale) + + line = '%s %s %s' % ( + str(key).ljust(self.max_key_len), + hist_bar, + self.formatter(count)) + percent_str = format_utils.NumToPercent(count, self.total) + if percent_str: + line += ' (%s)' % percent_str + hist_lines.append(line) + + return '\n'.join(hist_lines) + + def GetKeys(self): + """Returns the keys of the histogram.""" + return [key for key, _ in self.data] diff --git a/update_payload/histogram_unittest.py b/update_payload/histogram_unittest.py new file mode 100755 index 0000000..e757dd0 --- /dev/null +++ b/update_payload/histogram_unittest.py @@ -0,0 +1,72 @@ +#!/usr/bin/python2 +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Unit tests for histogram.py.""" + +import unittest + +from update_payload import format_utils +from update_payload import histogram + + +class HistogramTest(unittest.TestCase): + """ Tests histogram""" + + @staticmethod + def AddHumanReadableSize(size): + fmt = format_utils.BytesToHumanReadable(size) + return '%s (%s)' % (size, fmt) if fmt else str(size) + + def CompareToExpectedDefault(self, actual_str): + expected_str = ( + 'Yes |################ | 5 (83.3%)\n' + 'No |### | 1 (16.6%)' + ) + self.assertEqual(actual_str, expected_str) + + def testExampleHistogram(self): + self.CompareToExpectedDefault(str(histogram.Histogram( + [('Yes', 5), ('No', 1)]))) + + def testFromCountDict(self): + self.CompareToExpectedDefault(str(histogram.Histogram.FromCountDict( + {'Yes': 5, 'No': 1}))) + + def testFromKeyList(self): + self.CompareToExpectedDefault(str(histogram.Histogram.FromKeyList( + ['Yes', 'Yes', 'No', 'Yes', 'Yes', 'Yes']))) + + def testCustomScale(self): + expected_str = ( + 'Yes |#### | 5 (83.3%)\n' + 'No | | 1 (16.6%)' + ) + actual_str = str(histogram.Histogram([('Yes', 5), ('No', 1)], scale=5)) + self.assertEqual(actual_str, expected_str) + + def testCustomFormatter(self): + expected_str = ( + 'Yes |################ | 5000 (4.8 KiB) (83.3%)\n' + 'No |### | 1000 (16.6%)' + ) + actual_str = str(histogram.Histogram( + [('Yes', 5000), ('No', 1000)], formatter=self.AddHumanReadableSize)) + self.assertEqual(actual_str, expected_str) + + +if __name__ == '__main__': + unittest.main() diff --git a/update_payload/payload-test-key.pem b/update_payload/payload-test-key.pem new file mode 100644 index 0000000..342e923 --- /dev/null +++ b/update_payload/payload-test-key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAvtGHtqO21Uhy2wGz9fluIpIUR8G7dZoCZhZukGkm4mlfgL71 +xPSArjx02/w/FhYxOusV6/XQeKgL3i8cni3HCkCOurZLpi2L5Ver6qrxKFh6WBVZ +0Dj7N6P/Mf5jZdhfvVyweLlsNK8Ypeb+RazfrsXhd4cy3dBMxouGwH7R7QQXTFCo +Cc8kgJBTxILl3jfvY8OrNKgYiCETa7tQdFkP0bfPwH9cAXuMjHXiZatim0tF+ivp +kM2v/6LTxtD6Rq1wks/N6CHi8efrRaviFp7c0mNmBNFaV54cHEUW2SlNIiRun7L0 +1nAz/D8kuoHfx4E3Mtj0DbvngZJMX/X+rJQ5cQIDAQABAoIBADmE2X7hbJxwAUcp +BUExFdTP6dMTf9lcOjrhqiRXvgPjtYkOhvD+rsdWq/cf2zhiKibTdEEzUMr+BM3N +r7eyntvlR+DaUIVgF1pjigvryVPbD837aZ5NftRv194PC5FInttq1Dsf0ZEz8p8X +uS/xg1+ggG1SUK/yOSJkLpNZ5xelbclQJ9bnJST8PR8XbEieA83xt5M2DcooPzq0 +/99m/daA5hmSWs6n8sFrIZDQxDhLyyW4J72jjoNTE87eCpwK855yXMelpEPDZNQi +nB3x5Y/bGbl81PInqL2q14lekrVYdYZ7bOBVlsmyvz6f1e4OOE1aaAM+w6ArA4az +6elZQE0CgYEA4GOU6BBu9jLqFdqV9jIkWsgz5ZWINz8PLJPtZzk5I9KO1m+GAUy2 +h/1IGGR6qRQR49hMtq4C0lUifxquq0xivzJ87U9oxKC9yEeTxkmDe5csVHsnAtqT +xRgVM7Ysrut5NLU1zm0q3jBmkDu7d99LvscM/3n7eJ6RiYpnA54O6I8CgYEA2bNA +34PTvxBS2deRoxKQNlVU14FtirE+q0+k0wcE85wr7wIMpR13al8T1TpE8J1yvvZM +92HMGFGfYNDB46b8VfJ5AxEUFwdruec6sTVVfkMZMOqM/A08yiaLzQ1exDxNwaja +fLuG5FAVRD/2g7fLBcsmosyNgcgNr1XA8Q/nvf8CgYEAwaSOg7py19rWcqehlMZu +4z00tCNYWzz7LmA2l0clzYlPJTU3MvXt6+ujhRFpXXJpgfRPN7Nx0ewQihoPtNqF +uTSr5OwLoOyK+0Tx/UPByS2L3xgscWUJ8yQ2X9sOMqIZhmf/mDZTsU2ZpU03GlrE +dk43JF4zq0NEm6qp/dAwU3cCgYEAvECl+KKmmLIk8vvWlI2Y52Mi2rixYR2kc7+L +aHDJd1+1HhlHlgDFItbU765Trz5322phZArN0rnCeJYNFC9yRWBIBL7gAIoKPdgW +iOb15xlez04EXHGV/7kVa1wEdu0u0CiTxwjivMwDl+E36u8kQP5LirwYIgI800H0 +doCqhUECgYEAjvA38OS7hy56Q4LQtmHFBuRIn4E5SrIGMwNIH6TGbEKQix3ajTCQ +0fSoLDGTkU6dH+T4v0WheveN2a2Kofqm0UQx5V2rfnY/Ut1fAAWgL/lsHLDnzPUZ +bvTOANl8TbT49xAfNXTaGWe7F7nYz+bK0UDif1tJNDLQw7USD5I8lbQ= +-----END RSA PRIVATE KEY----- diff --git a/update_payload/payload-test-key.pub b/update_payload/payload-test-key.pub new file mode 100644 index 0000000..fdae963 --- /dev/null +++ b/update_payload/payload-test-key.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvtGHtqO21Uhy2wGz9flu +IpIUR8G7dZoCZhZukGkm4mlfgL71xPSArjx02/w/FhYxOusV6/XQeKgL3i8cni3H +CkCOurZLpi2L5Ver6qrxKFh6WBVZ0Dj7N6P/Mf5jZdhfvVyweLlsNK8Ypeb+Razf +rsXhd4cy3dBMxouGwH7R7QQXTFCoCc8kgJBTxILl3jfvY8OrNKgYiCETa7tQdFkP +0bfPwH9cAXuMjHXiZatim0tF+ivpkM2v/6LTxtD6Rq1wks/N6CHi8efrRaviFp7c +0mNmBNFaV54cHEUW2SlNIiRun7L01nAz/D8kuoHfx4E3Mtj0DbvngZJMX/X+rJQ5 +cQIDAQAB +-----END PUBLIC KEY----- diff --git a/update_payload/payload.py b/update_payload/payload.py new file mode 100644 index 0000000..380d6d0 --- /dev/null +++ b/update_payload/payload.py @@ -0,0 +1,336 @@ +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Tools for reading, verifying and applying Chrome OS update payloads.""" + +from __future__ import print_function + +import hashlib +import struct + +from update_payload import applier +from update_payload import checker +from update_payload import common +from update_payload import update_metadata_pb2 +from update_payload.error import PayloadError + + +# +# Helper functions. +# +def _ReadInt(file_obj, size, is_unsigned, hasher=None): + """Reads a binary-encoded integer from a file. + + It will do the correct conversion based on the reported size and whether or + not a signed number is expected. Assumes a network (big-endian) byte + ordering. + + Args: + file_obj: a file object + size: the integer size in bytes (2, 4 or 8) + is_unsigned: whether it is signed or not + hasher: an optional hasher to pass the value through + + Returns: + An "unpacked" (Python) integer value. + + Raises: + PayloadError if an read error occurred. + """ + return struct.unpack(common.IntPackingFmtStr(size, is_unsigned), + common.Read(file_obj, size, hasher=hasher))[0] + + +# +# Update payload. +# +class Payload(object): + """Chrome OS update payload processor.""" + + class _PayloadHeader(object): + """Update payload header struct.""" + + # Header constants; sizes are in bytes. + _MAGIC = 'CrAU' + _VERSION_SIZE = 8 + _MANIFEST_LEN_SIZE = 8 + _METADATA_SIGNATURE_LEN_SIZE = 4 + + def __init__(self): + self.version = None + self.manifest_len = None + self.metadata_signature_len = None + self.size = None + + def ReadFromPayload(self, payload_file, hasher=None): + """Reads the payload header from a file. + + Reads the payload header from the |payload_file| and updates the |hasher| + if one is passed. The parsed header is stored in the _PayloadHeader + instance attributes. + + Args: + payload_file: a file object + hasher: an optional hasher to pass the value through + + Returns: + None. + + Raises: + PayloadError if a read error occurred or the header is invalid. + """ + # Verify magic + magic = common.Read(payload_file, len(self._MAGIC), hasher=hasher) + if magic != self._MAGIC: + raise PayloadError('invalid payload magic: %s' % magic) + + self.version = _ReadInt(payload_file, self._VERSION_SIZE, True, + hasher=hasher) + self.manifest_len = _ReadInt(payload_file, self._MANIFEST_LEN_SIZE, True, + hasher=hasher) + self.size = (len(self._MAGIC) + self._VERSION_SIZE + + self._MANIFEST_LEN_SIZE) + self.metadata_signature_len = 0 + + if self.version == common.BRILLO_MAJOR_PAYLOAD_VERSION: + self.size += self._METADATA_SIGNATURE_LEN_SIZE + self.metadata_signature_len = _ReadInt( + payload_file, self._METADATA_SIGNATURE_LEN_SIZE, True, + hasher=hasher) + + + def __init__(self, payload_file, payload_file_offset=0): + """Initialize the payload object. + + Args: + payload_file: update payload file object open for reading + payload_file_offset: the offset of the actual payload + """ + self.payload_file = payload_file + self.payload_file_offset = payload_file_offset + self.manifest_hasher = None + self.is_init = False + self.header = None + self.manifest = None + self.data_offset = None + self.metadata_signature = None + self.metadata_size = None + + def _ReadHeader(self): + """Reads and returns the payload header. + + Returns: + A payload header object. + + Raises: + PayloadError if a read error occurred. + """ + header = self._PayloadHeader() + header.ReadFromPayload(self.payload_file, self.manifest_hasher) + return header + + def _ReadManifest(self): + """Reads and returns the payload manifest. + + Returns: + A string containing the payload manifest in binary form. + + Raises: + PayloadError if a read error occurred. + """ + if not self.header: + raise PayloadError('payload header not present') + + return common.Read(self.payload_file, self.header.manifest_len, + hasher=self.manifest_hasher) + + def _ReadMetadataSignature(self): + """Reads and returns the metadata signatures. + + Returns: + A string containing the metadata signatures protobuf in binary form or + an empty string if no metadata signature found in the payload. + + Raises: + PayloadError if a read error occurred. + """ + if not self.header: + raise PayloadError('payload header not present') + + return common.Read( + self.payload_file, self.header.metadata_signature_len, + offset=self.payload_file_offset + self.header.size + + self.header.manifest_len) + + def ReadDataBlob(self, offset, length): + """Reads and returns a single data blob from the update payload. + + Args: + offset: offset to the beginning of the blob from the end of the manifest + length: the blob's length + + Returns: + A string containing the raw blob data. + + Raises: + PayloadError if a read error occurred. + """ + return common.Read(self.payload_file, length, + offset=self.payload_file_offset + self.data_offset + + offset) + + def Init(self): + """Initializes the payload object. + + This is a prerequisite for any other public API call. + + Raises: + PayloadError if object already initialized or fails to initialize + correctly. + """ + if self.is_init: + raise PayloadError('payload object already initialized') + + self.manifest_hasher = hashlib.sha256() + + # Read the file header. + self.payload_file.seek(self.payload_file_offset) + self.header = self._ReadHeader() + + # Read the manifest. + manifest_raw = self._ReadManifest() + self.manifest = update_metadata_pb2.DeltaArchiveManifest() + self.manifest.ParseFromString(manifest_raw) + + # Read the metadata signature (if any). + metadata_signature_raw = self._ReadMetadataSignature() + if metadata_signature_raw: + self.metadata_signature = update_metadata_pb2.Signatures() + self.metadata_signature.ParseFromString(metadata_signature_raw) + + self.metadata_size = self.header.size + self.header.manifest_len + self.data_offset = self.metadata_size + self.header.metadata_signature_len + + self.is_init = True + + def Describe(self): + """Emits the payload embedded description data to standard output.""" + def _DescribeImageInfo(description, image_info): + """Display info about the image.""" + def _DisplayIndentedValue(name, value): + print(' {:<14} {}'.format(name+':', value)) + + print('%s:' % description) + _DisplayIndentedValue('Channel', image_info.channel) + _DisplayIndentedValue('Board', image_info.board) + _DisplayIndentedValue('Version', image_info.version) + _DisplayIndentedValue('Key', image_info.key) + + if image_info.build_channel != image_info.channel: + _DisplayIndentedValue('Build channel', image_info.build_channel) + + if image_info.build_version != image_info.version: + _DisplayIndentedValue('Build version', image_info.build_version) + + if self.manifest.HasField('old_image_info'): + _DescribeImageInfo('Old Image', self.manifest.old_image_info) + + if self.manifest.HasField('new_image_info'): + _DescribeImageInfo('New Image', self.manifest.new_image_info) + + def _AssertInit(self): + """Raises an exception if the object was not initialized.""" + if not self.is_init: + raise PayloadError('payload object not initialized') + + def ResetFile(self): + """Resets the offset of the payload file to right past the manifest.""" + self.payload_file.seek(self.payload_file_offset + self.data_offset) + + def IsDelta(self): + """Returns True iff the payload appears to be a delta.""" + self._AssertInit() + return (self.manifest.HasField('old_kernel_info') or + self.manifest.HasField('old_rootfs_info') or + any(partition.HasField('old_partition_info') + for partition in self.manifest.partitions)) + + def IsFull(self): + """Returns True iff the payload appears to be a full.""" + return not self.IsDelta() + + def Check(self, pubkey_file_name=None, metadata_sig_file=None, + report_out_file=None, assert_type=None, block_size=0, + rootfs_part_size=0, kernel_part_size=0, allow_unhashed=False, + disabled_tests=()): + """Checks the payload integrity. + + Args: + pubkey_file_name: public key used for signature verification + metadata_sig_file: metadata signature, if verification is desired + report_out_file: file object to dump the report to + assert_type: assert that payload is either 'full' or 'delta' + block_size: expected filesystem / payload block size + rootfs_part_size: the size of (physical) rootfs partitions in bytes + kernel_part_size: the size of (physical) kernel partitions in bytes + allow_unhashed: allow unhashed operation blobs + disabled_tests: list of tests to disable + + Raises: + PayloadError if payload verification failed. + """ + self._AssertInit() + + # Create a short-lived payload checker object and run it. + helper = checker.PayloadChecker( + self, assert_type=assert_type, block_size=block_size, + allow_unhashed=allow_unhashed, disabled_tests=disabled_tests) + helper.Run(pubkey_file_name=pubkey_file_name, + metadata_sig_file=metadata_sig_file, + rootfs_part_size=rootfs_part_size, + kernel_part_size=kernel_part_size, + report_out_file=report_out_file) + + def Apply(self, new_kernel_part, new_rootfs_part, old_kernel_part=None, + old_rootfs_part=None, bsdiff_in_place=True, bspatch_path=None, + puffpatch_path=None, truncate_to_expected_size=True): + """Applies the update payload. + + Args: + new_kernel_part: name of dest kernel partition file + new_rootfs_part: name of dest rootfs partition file + old_kernel_part: name of source kernel partition file (optional) + old_rootfs_part: name of source rootfs partition file (optional) + bsdiff_in_place: whether to perform BSDIFF operations in-place (optional) + bspatch_path: path to the bspatch binary (optional) + puffpatch_path: path to the puffpatch binary (optional) + truncate_to_expected_size: whether to truncate the resulting partitions + to their expected sizes, as specified in the + payload (optional) + + Raises: + PayloadError if payload application failed. + """ + self._AssertInit() + + # Create a short-lived payload applier object and run it. + helper = applier.PayloadApplier( + self, bsdiff_in_place=bsdiff_in_place, bspatch_path=bspatch_path, + puffpatch_path=puffpatch_path, + truncate_to_expected_size=truncate_to_expected_size) + helper.Run(new_kernel_part, new_rootfs_part, + old_kernel_part=old_kernel_part, + old_rootfs_part=old_rootfs_part) diff --git a/update_payload/test_utils.py b/update_payload/test_utils.py new file mode 100644 index 0000000..1e2259d --- /dev/null +++ b/update_payload/test_utils.py @@ -0,0 +1,369 @@ +# +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Utilities for unit testing.""" + +from __future__ import print_function + +import cStringIO +import hashlib +import os +import struct +import subprocess + +from update_payload import common +from update_payload import payload +from update_payload import update_metadata_pb2 + + +class TestError(Exception): + """An error during testing of update payload code.""" + + +# Private/public RSA keys used for testing. +_PRIVKEY_FILE_NAME = os.path.join(os.path.dirname(__file__), + 'payload-test-key.pem') +_PUBKEY_FILE_NAME = os.path.join(os.path.dirname(__file__), + 'payload-test-key.pub') + + +def KiB(count): + return count << 10 + + +def MiB(count): + return count << 20 + + +def GiB(count): + return count << 30 + + +def _WriteInt(file_obj, size, is_unsigned, val): + """Writes a binary-encoded integer to a file. + + It will do the correct conversion based on the reported size and whether or + not a signed number is expected. Assumes a network (big-endian) byte + ordering. + + Args: + file_obj: a file object + size: the integer size in bytes (2, 4 or 8) + is_unsigned: whether it is signed or not + val: integer value to encode + + Raises: + PayloadError if a write error occurred. + """ + try: + file_obj.write(struct.pack(common.IntPackingFmtStr(size, is_unsigned), val)) + except IOError, e: + raise payload.PayloadError('error writing to file (%s): %s' % + (file_obj.name, e)) + + +def _SetMsgField(msg, field_name, val): + """Sets or clears a field in a protobuf message.""" + if val is None: + msg.ClearField(field_name) + else: + setattr(msg, field_name, val) + + +def SignSha256(data, privkey_file_name): + """Signs the data's SHA256 hash with an RSA private key. + + Args: + data: the data whose SHA256 hash we want to sign + privkey_file_name: private key used for signing data + + Returns: + The signature string, prepended with an ASN1 header. + + Raises: + TestError if something goes wrong. + """ + data_sha256_hash = common.SIG_ASN1_HEADER + hashlib.sha256(data).digest() + sign_cmd = ['openssl', 'rsautl', '-sign', '-inkey', privkey_file_name] + try: + sign_process = subprocess.Popen(sign_cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + sig, _ = sign_process.communicate(input=data_sha256_hash) + except Exception as e: + raise TestError('signing subprocess failed: %s' % e) + + return sig + + +class SignaturesGenerator(object): + """Generates a payload signatures data block.""" + + def __init__(self): + self.sigs = update_metadata_pb2.Signatures() + + def AddSig(self, version, data): + """Adds a signature to the signature sequence. + + Args: + version: signature version (None means do not assign) + data: signature binary data (None means do not assign) + """ + sig = self.sigs.signatures.add() + if version is not None: + sig.version = version + if data is not None: + sig.data = data + + def ToBinary(self): + """Returns the binary representation of the signature block.""" + return self.sigs.SerializeToString() + + +class PayloadGenerator(object): + """Generates an update payload allowing low-level control. + + Attributes: + manifest: the protobuf containing the payload manifest + version: the payload version identifier + block_size: the block size pertaining to update operations + + """ + + def __init__(self, version=1): + self.manifest = update_metadata_pb2.DeltaArchiveManifest() + self.version = version + self.block_size = 0 + + @staticmethod + def _WriteExtent(ex, val): + """Returns an Extent message.""" + start_block, num_blocks = val + _SetMsgField(ex, 'start_block', start_block) + _SetMsgField(ex, 'num_blocks', num_blocks) + + @staticmethod + def _AddValuesToRepeatedField(repeated_field, values, write_func): + """Adds values to a repeated message field.""" + if values: + for val in values: + new_item = repeated_field.add() + write_func(new_item, val) + + @staticmethod + def _AddExtents(extents_field, values): + """Adds extents to an extents field.""" + PayloadGenerator._AddValuesToRepeatedField( + extents_field, values, PayloadGenerator._WriteExtent) + + def SetBlockSize(self, block_size): + """Sets the payload's block size.""" + self.block_size = block_size + _SetMsgField(self.manifest, 'block_size', block_size) + + def SetPartInfo(self, is_kernel, is_new, part_size, part_hash): + """Set the partition info entry. + + Args: + is_kernel: whether this is kernel partition info + is_new: whether to set old (False) or new (True) info + part_size: the partition size (in fact, filesystem size) + part_hash: the partition hash + """ + if is_kernel: + part_info = (self.manifest.new_kernel_info if is_new + else self.manifest.old_kernel_info) + else: + part_info = (self.manifest.new_rootfs_info if is_new + else self.manifest.old_rootfs_info) + _SetMsgField(part_info, 'size', part_size) + _SetMsgField(part_info, 'hash', part_hash) + + def AddOperation(self, is_kernel, op_type, data_offset=None, + data_length=None, src_extents=None, src_length=None, + dst_extents=None, dst_length=None, data_sha256_hash=None): + """Adds an InstallOperation entry.""" + operations = (self.manifest.kernel_install_operations if is_kernel + else self.manifest.install_operations) + + op = operations.add() + op.type = op_type + + _SetMsgField(op, 'data_offset', data_offset) + _SetMsgField(op, 'data_length', data_length) + + self._AddExtents(op.src_extents, src_extents) + _SetMsgField(op, 'src_length', src_length) + + self._AddExtents(op.dst_extents, dst_extents) + _SetMsgField(op, 'dst_length', dst_length) + + _SetMsgField(op, 'data_sha256_hash', data_sha256_hash) + + def SetSignatures(self, sigs_offset, sigs_size): + """Set the payload's signature block descriptors.""" + _SetMsgField(self.manifest, 'signatures_offset', sigs_offset) + _SetMsgField(self.manifest, 'signatures_size', sigs_size) + + def SetMinorVersion(self, minor_version): + """Set the payload's minor version field.""" + _SetMsgField(self.manifest, 'minor_version', minor_version) + + def _WriteHeaderToFile(self, file_obj, manifest_len): + """Writes a payload heaer to a file.""" + # We need to access protected members in Payload for writing the header. + # pylint: disable=W0212 + file_obj.write(payload.Payload._PayloadHeader._MAGIC) + _WriteInt(file_obj, payload.Payload._PayloadHeader._VERSION_SIZE, True, + self.version) + _WriteInt(file_obj, payload.Payload._PayloadHeader._MANIFEST_LEN_SIZE, True, + manifest_len) + + def WriteToFile(self, file_obj, manifest_len=-1, data_blobs=None, + sigs_data=None, padding=None): + """Writes the payload content to a file. + + Args: + file_obj: a file object open for writing + manifest_len: manifest len to dump (otherwise computed automatically) + data_blobs: a list of data blobs to be concatenated to the payload + sigs_data: a binary Signatures message to be concatenated to the payload + padding: stuff to dump past the normal data blobs provided (optional) + """ + manifest = self.manifest.SerializeToString() + if manifest_len < 0: + manifest_len = len(manifest) + self._WriteHeaderToFile(file_obj, manifest_len) + file_obj.write(manifest) + if data_blobs: + for data_blob in data_blobs: + file_obj.write(data_blob) + if sigs_data: + file_obj.write(sigs_data) + if padding: + file_obj.write(padding) + + +class EnhancedPayloadGenerator(PayloadGenerator): + """Payload generator with automatic handling of data blobs. + + Attributes: + data_blobs: a list of blobs, in the order they were added + curr_offset: the currently consumed offset of blobs added to the payload + """ + + def __init__(self): + super(EnhancedPayloadGenerator, self).__init__() + self.data_blobs = [] + self.curr_offset = 0 + + def AddData(self, data_blob): + """Adds a (possibly orphan) data blob.""" + data_length = len(data_blob) + data_offset = self.curr_offset + self.curr_offset += data_length + self.data_blobs.append(data_blob) + return data_length, data_offset + + def AddOperationWithData(self, is_kernel, op_type, src_extents=None, + src_length=None, dst_extents=None, dst_length=None, + data_blob=None, do_hash_data_blob=True): + """Adds an install operation and associated data blob. + + This takes care of obtaining a hash of the data blob (if so instructed) + and appending it to the internally maintained list of blobs, including the + necessary offset/length accounting. + + Args: + is_kernel: whether this is a kernel (True) or rootfs (False) operation + op_type: one of REPLACE, REPLACE_BZ, REPLACE_XZ, MOVE or BSDIFF + src_extents: list of (start, length) pairs indicating src block ranges + src_length: size of the src data in bytes (needed for BSDIFF) + dst_extents: list of (start, length) pairs indicating dst block ranges + dst_length: size of the dst data in bytes (needed for BSDIFF) + data_blob: a data blob associated with this operation + do_hash_data_blob: whether or not to compute and add a data blob hash + """ + data_offset = data_length = data_sha256_hash = None + if data_blob is not None: + if do_hash_data_blob: + data_sha256_hash = hashlib.sha256(data_blob).digest() + data_length, data_offset = self.AddData(data_blob) + + self.AddOperation(is_kernel, op_type, data_offset=data_offset, + data_length=data_length, src_extents=src_extents, + src_length=src_length, dst_extents=dst_extents, + dst_length=dst_length, data_sha256_hash=data_sha256_hash) + + def WriteToFileWithData(self, file_obj, sigs_data=None, + privkey_file_name=None, + do_add_pseudo_operation=False, + is_pseudo_in_kernel=False, padding=None): + """Writes the payload content to a file, optionally signing the content. + + Args: + file_obj: a file object open for writing + sigs_data: signatures blob to be appended to the payload (optional; + payload signature fields assumed to be preset by the caller) + privkey_file_name: key used for signing the payload (optional; used only + if explicit signatures blob not provided) + do_add_pseudo_operation: whether a pseudo-operation should be added to + account for the signature blob + is_pseudo_in_kernel: whether the pseudo-operation should be added to + kernel (True) or rootfs (False) operations + padding: stuff to dump past the normal data blobs provided (optional) + + Raises: + TestError: if arguments are inconsistent or something goes wrong. + """ + sigs_len = len(sigs_data) if sigs_data else 0 + + # Do we need to generate a genuine signatures blob? + do_generate_sigs_data = sigs_data is None and privkey_file_name + + if do_generate_sigs_data: + # First, sign some arbitrary data to obtain the size of a signature blob. + fake_sig = SignSha256('fake-payload-data', privkey_file_name) + fake_sigs_gen = SignaturesGenerator() + fake_sigs_gen.AddSig(1, fake_sig) + sigs_len = len(fake_sigs_gen.ToBinary()) + + # Update the payload with proper signature attributes. + self.SetSignatures(self.curr_offset, sigs_len) + + # Add a pseudo-operation to account for the signature blob, if requested. + if do_add_pseudo_operation: + if not self.block_size: + raise TestError('cannot add pseudo-operation without knowing the ' + 'payload block size') + self.AddOperation( + is_pseudo_in_kernel, common.OpType.REPLACE, + data_offset=self.curr_offset, data_length=sigs_len, + dst_extents=[(common.PSEUDO_EXTENT_MARKER, + (sigs_len + self.block_size - 1) / self.block_size)]) + + if do_generate_sigs_data: + # Once all payload fields are updated, dump and sign it. + temp_payload_file = cStringIO.StringIO() + self.WriteToFile(temp_payload_file, data_blobs=self.data_blobs) + sig = SignSha256(temp_payload_file.getvalue(), privkey_file_name) + sigs_gen = SignaturesGenerator() + sigs_gen.AddSig(1, sig) + sigs_data = sigs_gen.ToBinary() + assert len(sigs_data) == sigs_len, 'signature blob lengths mismatch' + + # Dump the whole thing, complete with data and signature blob, to a file. + self.WriteToFile(file_obj, data_blobs=self.data_blobs, sigs_data=sigs_data, + padding=padding) diff --git a/update_payload/update-payload-key.pub.pem b/update_payload/update-payload-key.pub.pem new file mode 100644 index 0000000..7ac369f --- /dev/null +++ b/update_payload/update-payload-key.pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Bg9BnjWhX3jJyECeXqF +O28nkYTF1NHWLlFHgzAGg+ysva22BL3S5LlsNejnYVg/xzx3izvAQyOF3I1TJVOy +2fH1DoZOWyKuckMyUrFQbO6OV1VIvPUPKckHadWcXSsHj2lBdDPH9xRDEBsXeztf +nAGBD8GlAyTU7iH+Bf+xzyK9k4BmITf4Nx4xWhRZ6gm2Fc2SEP3x5N5fohkLv5ZP +kFr0fj5wUK+0XF95rkGFBLIq2XACS3dmxMFToFl1HMM1HonUg9TAH+3dVH93zue1 +y81mkTuGnNX+zYya5ov2kD8zW1V10iTOSJfOlho5T8FpKbG37o3yYcUiyMHKO1Iv +PQIDAQAB +-----END PUBLIC KEY----- diff --git a/update_payload/update_metadata_pb2.py b/update_payload/update_metadata_pb2.py new file mode 100644 index 0000000..595f2f6 --- /dev/null +++ b/update_payload/update_metadata_pb2.py @@ -0,0 +1,631 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: update_metadata.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='update_metadata.proto', + package='chromeos_update_engine', + serialized_pb='\n\x15update_metadata.proto\x12\x16\x63hromeos_update_engine\"1\n\x06\x45xtent\x12\x13\n\x0bstart_block\x18\x01 \x01(\x04\x12\x12\n\nnum_blocks\x18\x02 \x01(\x04\"z\n\nSignatures\x12@\n\nsignatures\x18\x01 \x03(\x0b\x32,.chromeos_update_engine.Signatures.Signature\x1a*\n\tSignature\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"+\n\rPartitionInfo\x12\x0c\n\x04size\x18\x01 \x01(\x04\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\"w\n\tImageInfo\x12\r\n\x05\x62oard\x18\x01 \x01(\t\x12\x0b\n\x03key\x18\x02 \x01(\t\x12\x0f\n\x07\x63hannel\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\x12\x15\n\rbuild_channel\x18\x05 \x01(\t\x12\x15\n\rbuild_version\x18\x06 \x01(\t\"\xe6\x03\n\x10InstallOperation\x12;\n\x04type\x18\x01 \x02(\x0e\x32-.chromeos_update_engine.InstallOperation.Type\x12\x13\n\x0b\x64\x61ta_offset\x18\x02 \x01(\r\x12\x13\n\x0b\x64\x61ta_length\x18\x03 \x01(\r\x12\x33\n\x0bsrc_extents\x18\x04 \x03(\x0b\x32\x1e.chromeos_update_engine.Extent\x12\x12\n\nsrc_length\x18\x05 \x01(\x04\x12\x33\n\x0b\x64st_extents\x18\x06 \x03(\x0b\x32\x1e.chromeos_update_engine.Extent\x12\x12\n\ndst_length\x18\x07 \x01(\x04\x12\x18\n\x10\x64\x61ta_sha256_hash\x18\x08 \x01(\x0c\x12\x17\n\x0fsrc_sha256_hash\x18\t \x01(\x0c\"\xa5\x01\n\x04Type\x12\x0b\n\x07REPLACE\x10\x00\x12\x0e\n\nREPLACE_BZ\x10\x01\x12\x08\n\x04MOVE\x10\x02\x12\n\n\x06\x42SDIFF\x10\x03\x12\x0f\n\x0bSOURCE_COPY\x10\x04\x12\x11\n\rSOURCE_BSDIFF\x10\x05\x12\x08\n\x04ZERO\x10\x06\x12\x0b\n\x07\x44ISCARD\x10\x07\x12\x0e\n\nREPLACE_XZ\x10\x08\x12\x0c\n\x08PUFFDIFF\x10\t\x12\x11\n\rBROTLI_BSDIFF\x10\n\"\xa6\x03\n\x0fPartitionUpdate\x12\x16\n\x0epartition_name\x18\x01 \x02(\t\x12\x17\n\x0frun_postinstall\x18\x02 \x01(\x08\x12\x18\n\x10postinstall_path\x18\x03 \x01(\t\x12\x17\n\x0f\x66ilesystem_type\x18\x04 \x01(\t\x12M\n\x17new_partition_signature\x18\x05 \x03(\x0b\x32,.chromeos_update_engine.Signatures.Signature\x12\x41\n\x12old_partition_info\x18\x06 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12\x41\n\x12new_partition_info\x18\x07 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12<\n\noperations\x18\x08 \x03(\x0b\x32(.chromeos_update_engine.InstallOperation\x12\x1c\n\x14postinstall_optional\x18\t \x01(\x08\"\xc4\x05\n\x14\x44\x65ltaArchiveManifest\x12\x44\n\x12install_operations\x18\x01 \x03(\x0b\x32(.chromeos_update_engine.InstallOperation\x12K\n\x19kernel_install_operations\x18\x02 \x03(\x0b\x32(.chromeos_update_engine.InstallOperation\x12\x18\n\nblock_size\x18\x03 \x01(\r:\x04\x34\x30\x39\x36\x12\x19\n\x11signatures_offset\x18\x04 \x01(\x04\x12\x17\n\x0fsignatures_size\x18\x05 \x01(\x04\x12>\n\x0fold_kernel_info\x18\x06 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12>\n\x0fnew_kernel_info\x18\x07 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12>\n\x0fold_rootfs_info\x18\x08 \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12>\n\x0fnew_rootfs_info\x18\t \x01(\x0b\x32%.chromeos_update_engine.PartitionInfo\x12\x39\n\x0eold_image_info\x18\n \x01(\x0b\x32!.chromeos_update_engine.ImageInfo\x12\x39\n\x0enew_image_info\x18\x0b \x01(\x0b\x32!.chromeos_update_engine.ImageInfo\x12\x18\n\rminor_version\x18\x0c \x01(\r:\x01\x30\x12;\n\npartitions\x18\r \x03(\x0b\x32\'.chromeos_update_engine.PartitionUpdateB\x02H\x03') + + + +_INSTALLOPERATION_TYPE = _descriptor.EnumDescriptor( + name='Type', + full_name='chromeos_update_engine.InstallOperation.Type', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='REPLACE', index=0, number=0, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='REPLACE_BZ', index=1, number=1, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='MOVE', index=2, number=2, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='BSDIFF', index=3, number=3, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOURCE_COPY', index=4, number=4, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOURCE_BSDIFF', index=5, number=5, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='ZERO', index=6, number=6, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='DISCARD', index=7, number=7, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='REPLACE_XZ', index=8, number=8, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='PUFFDIFF', index=9, number=9, + options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='BROTLI_BSDIFF', index=10, number=10, + options=None, + type=None), + ], + containing_type=None, + options=None, + serialized_start=712, + serialized_end=877, +) + + +_EXTENT = _descriptor.Descriptor( + name='Extent', + full_name='chromeos_update_engine.Extent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='start_block', full_name='chromeos_update_engine.Extent.start_block', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='num_blocks', full_name='chromeos_update_engine.Extent.num_blocks', index=1, + number=2, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=49, + serialized_end=98, +) + + +_SIGNATURES_SIGNATURE = _descriptor.Descriptor( + name='Signature', + full_name='chromeos_update_engine.Signatures.Signature', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='version', full_name='chromeos_update_engine.Signatures.Signature.version', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='data', full_name='chromeos_update_engine.Signatures.Signature.data', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=180, + serialized_end=222, +) + +_SIGNATURES = _descriptor.Descriptor( + name='Signatures', + full_name='chromeos_update_engine.Signatures', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='signatures', full_name='chromeos_update_engine.Signatures.signatures', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[_SIGNATURES_SIGNATURE, ], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=100, + serialized_end=222, +) + + +_PARTITIONINFO = _descriptor.Descriptor( + name='PartitionInfo', + full_name='chromeos_update_engine.PartitionInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='size', full_name='chromeos_update_engine.PartitionInfo.size', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='hash', full_name='chromeos_update_engine.PartitionInfo.hash', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=224, + serialized_end=267, +) + + +_IMAGEINFO = _descriptor.Descriptor( + name='ImageInfo', + full_name='chromeos_update_engine.ImageInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='board', full_name='chromeos_update_engine.ImageInfo.board', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='key', full_name='chromeos_update_engine.ImageInfo.key', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='channel', full_name='chromeos_update_engine.ImageInfo.channel', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='version', full_name='chromeos_update_engine.ImageInfo.version', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='build_channel', full_name='chromeos_update_engine.ImageInfo.build_channel', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='build_version', full_name='chromeos_update_engine.ImageInfo.build_version', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=269, + serialized_end=388, +) + + +_INSTALLOPERATION = _descriptor.Descriptor( + name='InstallOperation', + full_name='chromeos_update_engine.InstallOperation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='type', full_name='chromeos_update_engine.InstallOperation.type', index=0, + number=1, type=14, cpp_type=8, label=2, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='data_offset', full_name='chromeos_update_engine.InstallOperation.data_offset', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='data_length', full_name='chromeos_update_engine.InstallOperation.data_length', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='src_extents', full_name='chromeos_update_engine.InstallOperation.src_extents', index=3, + number=4, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='src_length', full_name='chromeos_update_engine.InstallOperation.src_length', index=4, + number=5, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='dst_extents', full_name='chromeos_update_engine.InstallOperation.dst_extents', index=5, + number=6, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='dst_length', full_name='chromeos_update_engine.InstallOperation.dst_length', index=6, + number=7, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='data_sha256_hash', full_name='chromeos_update_engine.InstallOperation.data_sha256_hash', index=7, + number=8, type=12, cpp_type=9, label=1, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='src_sha256_hash', full_name='chromeos_update_engine.InstallOperation.src_sha256_hash', index=8, + number=9, type=12, cpp_type=9, label=1, + has_default_value=False, default_value="", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _INSTALLOPERATION_TYPE, + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=391, + serialized_end=877, +) + + +_PARTITIONUPDATE = _descriptor.Descriptor( + name='PartitionUpdate', + full_name='chromeos_update_engine.PartitionUpdate', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='partition_name', full_name='chromeos_update_engine.PartitionUpdate.partition_name', index=0, + number=1, type=9, cpp_type=9, label=2, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='run_postinstall', full_name='chromeos_update_engine.PartitionUpdate.run_postinstall', index=1, + number=2, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='postinstall_path', full_name='chromeos_update_engine.PartitionUpdate.postinstall_path', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='filesystem_type', full_name='chromeos_update_engine.PartitionUpdate.filesystem_type', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=unicode("", "utf-8"), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='new_partition_signature', full_name='chromeos_update_engine.PartitionUpdate.new_partition_signature', index=4, + number=5, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='old_partition_info', full_name='chromeos_update_engine.PartitionUpdate.old_partition_info', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='new_partition_info', full_name='chromeos_update_engine.PartitionUpdate.new_partition_info', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='operations', full_name='chromeos_update_engine.PartitionUpdate.operations', index=7, + number=8, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='postinstall_optional', full_name='chromeos_update_engine.PartitionUpdate.postinstall_optional', index=8, + number=9, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=880, + serialized_end=1302, +) + + +_DELTAARCHIVEMANIFEST = _descriptor.Descriptor( + name='DeltaArchiveManifest', + full_name='chromeos_update_engine.DeltaArchiveManifest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='install_operations', full_name='chromeos_update_engine.DeltaArchiveManifest.install_operations', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='kernel_install_operations', full_name='chromeos_update_engine.DeltaArchiveManifest.kernel_install_operations', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='block_size', full_name='chromeos_update_engine.DeltaArchiveManifest.block_size', index=2, + number=3, type=13, cpp_type=3, label=1, + has_default_value=True, default_value=4096, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='signatures_offset', full_name='chromeos_update_engine.DeltaArchiveManifest.signatures_offset', index=3, + number=4, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='signatures_size', full_name='chromeos_update_engine.DeltaArchiveManifest.signatures_size', index=4, + number=5, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='old_kernel_info', full_name='chromeos_update_engine.DeltaArchiveManifest.old_kernel_info', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='new_kernel_info', full_name='chromeos_update_engine.DeltaArchiveManifest.new_kernel_info', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='old_rootfs_info', full_name='chromeos_update_engine.DeltaArchiveManifest.old_rootfs_info', index=7, + number=8, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='new_rootfs_info', full_name='chromeos_update_engine.DeltaArchiveManifest.new_rootfs_info', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='old_image_info', full_name='chromeos_update_engine.DeltaArchiveManifest.old_image_info', index=9, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='new_image_info', full_name='chromeos_update_engine.DeltaArchiveManifest.new_image_info', index=10, + number=11, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='minor_version', full_name='chromeos_update_engine.DeltaArchiveManifest.minor_version', index=11, + number=12, type=13, cpp_type=3, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + _descriptor.FieldDescriptor( + name='partitions', full_name='chromeos_update_engine.DeltaArchiveManifest.partitions', index=12, + number=13, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + extension_ranges=[], + serialized_start=1305, + serialized_end=2013, +) + +_SIGNATURES_SIGNATURE.containing_type = _SIGNATURES; +_SIGNATURES.fields_by_name['signatures'].message_type = _SIGNATURES_SIGNATURE +_INSTALLOPERATION.fields_by_name['type'].enum_type = _INSTALLOPERATION_TYPE +_INSTALLOPERATION.fields_by_name['src_extents'].message_type = _EXTENT +_INSTALLOPERATION.fields_by_name['dst_extents'].message_type = _EXTENT +_INSTALLOPERATION_TYPE.containing_type = _INSTALLOPERATION; +_PARTITIONUPDATE.fields_by_name['new_partition_signature'].message_type = _SIGNATURES_SIGNATURE +_PARTITIONUPDATE.fields_by_name['old_partition_info'].message_type = _PARTITIONINFO +_PARTITIONUPDATE.fields_by_name['new_partition_info'].message_type = _PARTITIONINFO +_PARTITIONUPDATE.fields_by_name['operations'].message_type = _INSTALLOPERATION +_DELTAARCHIVEMANIFEST.fields_by_name['install_operations'].message_type = _INSTALLOPERATION +_DELTAARCHIVEMANIFEST.fields_by_name['kernel_install_operations'].message_type = _INSTALLOPERATION +_DELTAARCHIVEMANIFEST.fields_by_name['old_kernel_info'].message_type = _PARTITIONINFO +_DELTAARCHIVEMANIFEST.fields_by_name['new_kernel_info'].message_type = _PARTITIONINFO +_DELTAARCHIVEMANIFEST.fields_by_name['old_rootfs_info'].message_type = _PARTITIONINFO +_DELTAARCHIVEMANIFEST.fields_by_name['new_rootfs_info'].message_type = _PARTITIONINFO +_DELTAARCHIVEMANIFEST.fields_by_name['old_image_info'].message_type = _IMAGEINFO +_DELTAARCHIVEMANIFEST.fields_by_name['new_image_info'].message_type = _IMAGEINFO +_DELTAARCHIVEMANIFEST.fields_by_name['partitions'].message_type = _PARTITIONUPDATE +DESCRIPTOR.message_types_by_name['Extent'] = _EXTENT +DESCRIPTOR.message_types_by_name['Signatures'] = _SIGNATURES +DESCRIPTOR.message_types_by_name['PartitionInfo'] = _PARTITIONINFO +DESCRIPTOR.message_types_by_name['ImageInfo'] = _IMAGEINFO +DESCRIPTOR.message_types_by_name['InstallOperation'] = _INSTALLOPERATION +DESCRIPTOR.message_types_by_name['PartitionUpdate'] = _PARTITIONUPDATE +DESCRIPTOR.message_types_by_name['DeltaArchiveManifest'] = _DELTAARCHIVEMANIFEST + +class Extent(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _EXTENT + + # @@protoc_insertion_point(class_scope:chromeos_update_engine.Extent) + +class Signatures(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + + class Signature(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _SIGNATURES_SIGNATURE + + # @@protoc_insertion_point(class_scope:chromeos_update_engine.Signatures.Signature) + DESCRIPTOR = _SIGNATURES + + # @@protoc_insertion_point(class_scope:chromeos_update_engine.Signatures) + +class PartitionInfo(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _PARTITIONINFO + + # @@protoc_insertion_point(class_scope:chromeos_update_engine.PartitionInfo) + +class ImageInfo(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _IMAGEINFO + + # @@protoc_insertion_point(class_scope:chromeos_update_engine.ImageInfo) + +class InstallOperation(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _INSTALLOPERATION + + # @@protoc_insertion_point(class_scope:chromeos_update_engine.InstallOperation) + +class PartitionUpdate(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _PARTITIONUPDATE + + # @@protoc_insertion_point(class_scope:chromeos_update_engine.PartitionUpdate) + +class DeltaArchiveManifest(_message.Message): + __metaclass__ = _reflection.GeneratedProtocolMessageType + DESCRIPTOR = _DELTAARCHIVEMANIFEST + + # @@protoc_insertion_point(class_scope:chromeos_update_engine.DeltaArchiveManifest) + + +DESCRIPTOR.has_options = True +DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), 'H\003') +# @@protoc_insertion_point(module_scope) |