1# 2# Copyright (C) 2013 The Android Open Source Project 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16 17"""Verifying the integrity of a Chrome OS update payload. 18 19This module is used internally by the main Payload class for verifying the 20integrity of an update payload. The interface for invoking the checks is as 21follows: 22 23 checker = PayloadChecker(payload) 24 checker.Run(...) 25""" 26 27from __future__ import absolute_import 28from __future__ import print_function 29 30import array 31import base64 32import collections 33import hashlib 34import itertools 35import os 36import subprocess 37 38# pylint: disable=redefined-builtin 39from six.moves import range 40 41from update_payload import common 42from update_payload import error 43from update_payload import format_utils 44from update_payload import histogram 45from update_payload import update_metadata_pb2 46 47# 48# Constants. 49# 50 51_CHECK_MOVE_SAME_SRC_DST_BLOCK = 'move-same-src-dst-block' 52_CHECK_PAYLOAD_SIG = 'payload-sig' 53CHECKS_TO_DISABLE = ( 54 _CHECK_MOVE_SAME_SRC_DST_BLOCK, 55 _CHECK_PAYLOAD_SIG, 56) 57 58_TYPE_FULL = 'full' 59_TYPE_DELTA = 'delta' 60 61_DEFAULT_BLOCK_SIZE = 4096 62 63_DEFAULT_PUBKEY_BASE_NAME = 'update-payload-key.pub.pem' 64_DEFAULT_PUBKEY_FILE_NAME = os.path.join(os.path.dirname(__file__), 65 _DEFAULT_PUBKEY_BASE_NAME) 66 67# Supported minor version map to payload types allowed to be using them. 68_SUPPORTED_MINOR_VERSIONS = { 69 0: (_TYPE_FULL,), 70 2: (_TYPE_DELTA,), 71 3: (_TYPE_DELTA,), 72 4: (_TYPE_DELTA,), 73 5: (_TYPE_DELTA,), 74 6: (_TYPE_DELTA,), 75 7: (_TYPE_DELTA,), 76} 77 78 79# 80# Helper functions. 81# 82 83def _IsPowerOfTwo(val): 84 """Returns True iff val is a power of two.""" 85 return val > 0 and (val & (val - 1)) == 0 86 87 88def _AddFormat(format_func, value): 89 """Adds a custom formatted representation to ordinary string representation. 90 91 Args: 92 format_func: A value formatter. 93 value: Value to be formatted and returned. 94 95 Returns: 96 A string 'x (y)' where x = str(value) and y = format_func(value). 97 """ 98 ret = str(value) 99 formatted_str = format_func(value) 100 if formatted_str: 101 ret += ' (%s)' % formatted_str 102 return ret 103 104 105def _AddHumanReadableSize(size): 106 """Adds a human readable representation to a byte size value.""" 107 return _AddFormat(format_utils.BytesToHumanReadable, size) 108 109 110# 111# Payload report generator. 112# 113 114class _PayloadReport(object): 115 """A payload report generator. 116 117 A report is essentially a sequence of nodes, which represent data points. It 118 is initialized to have a "global", untitled section. A node may be a 119 sub-report itself. 120 """ 121 122 # Report nodes: Field, sub-report, section. 123 class Node(object): 124 """A report node interface.""" 125 126 @staticmethod 127 def _Indent(indent, line): 128 """Indents a line by a given indentation amount. 129 130 Args: 131 indent: The indentation amount. 132 line: The line content (string). 133 134 Returns: 135 The properly indented line (string). 136 """ 137 return '%*s%s' % (indent, '', line) 138 139 def GenerateLines(self, base_indent, sub_indent, curr_section): 140 """Generates the report lines for this node. 141 142 Args: 143 base_indent: Base indentation for each line. 144 sub_indent: Additional indentation for sub-nodes. 145 curr_section: The current report section object. 146 147 Returns: 148 A pair consisting of a list of properly indented report lines and a new 149 current section object. 150 """ 151 raise NotImplementedError 152 153 class FieldNode(Node): 154 """A field report node, representing a (name, value) pair.""" 155 156 def __init__(self, name, value, linebreak, indent): 157 super(_PayloadReport.FieldNode, self).__init__() 158 self.name = name 159 self.value = value 160 self.linebreak = linebreak 161 self.indent = indent 162 163 def GenerateLines(self, base_indent, sub_indent, curr_section): 164 """Generates a properly formatted 'name : value' entry.""" 165 report_output = '' 166 if self.name: 167 report_output += self.name.ljust(curr_section.max_field_name_len) + ' :' 168 value_lines = str(self.value).splitlines() 169 if self.linebreak and self.name: 170 report_output += '\n' + '\n'.join( 171 ['%*s%s' % (self.indent, '', line) for line in value_lines]) 172 else: 173 if self.name: 174 report_output += ' ' 175 report_output += '%*s' % (self.indent, '') 176 cont_line_indent = len(report_output) 177 indented_value_lines = [value_lines[0]] 178 indented_value_lines.extend(['%*s%s' % (cont_line_indent, '', line) 179 for line in value_lines[1:]]) 180 report_output += '\n'.join(indented_value_lines) 181 182 report_lines = [self._Indent(base_indent, line + '\n') 183 for line in report_output.split('\n')] 184 return report_lines, curr_section 185 186 class SubReportNode(Node): 187 """A sub-report node, representing a nested report.""" 188 189 def __init__(self, title, report): 190 super(_PayloadReport.SubReportNode, self).__init__() 191 self.title = title 192 self.report = report 193 194 def GenerateLines(self, base_indent, sub_indent, curr_section): 195 """Recurse with indentation.""" 196 report_lines = [self._Indent(base_indent, self.title + ' =>\n')] 197 report_lines.extend(self.report.GenerateLines(base_indent + sub_indent, 198 sub_indent)) 199 return report_lines, curr_section 200 201 class SectionNode(Node): 202 """A section header node.""" 203 204 def __init__(self, title=None): 205 super(_PayloadReport.SectionNode, self).__init__() 206 self.title = title 207 self.max_field_name_len = 0 208 209 def GenerateLines(self, base_indent, sub_indent, curr_section): 210 """Dump a title line, return self as the (new) current section.""" 211 report_lines = [] 212 if self.title: 213 report_lines.append(self._Indent(base_indent, 214 '=== %s ===\n' % self.title)) 215 return report_lines, self 216 217 def __init__(self): 218 self.report = [] 219 self.last_section = self.global_section = self.SectionNode() 220 self.is_finalized = False 221 222 def GenerateLines(self, base_indent, sub_indent): 223 """Generates the lines in the report, properly indented. 224 225 Args: 226 base_indent: The indentation used for root-level report lines. 227 sub_indent: The indentation offset used for sub-reports. 228 229 Returns: 230 A list of indented report lines. 231 """ 232 report_lines = [] 233 curr_section = self.global_section 234 for node in self.report: 235 node_report_lines, curr_section = node.GenerateLines( 236 base_indent, sub_indent, curr_section) 237 report_lines.extend(node_report_lines) 238 239 return report_lines 240 241 def Dump(self, out_file, base_indent=0, sub_indent=2): 242 """Dumps the report to a file. 243 244 Args: 245 out_file: File object to output the content to. 246 base_indent: Base indentation for report lines. 247 sub_indent: Added indentation for sub-reports. 248 """ 249 report_lines = self.GenerateLines(base_indent, sub_indent) 250 if report_lines and not self.is_finalized: 251 report_lines.append('(incomplete report)\n') 252 253 for line in report_lines: 254 out_file.write(line) 255 256 def AddField(self, name, value, linebreak=False, indent=0): 257 """Adds a field/value pair to the payload report. 258 259 Args: 260 name: The field's name. 261 value: The field's value. 262 linebreak: Whether the value should be printed on a new line. 263 indent: Amount of extra indent for each line of the value. 264 """ 265 assert not self.is_finalized 266 if name and self.last_section.max_field_name_len < len(name): 267 self.last_section.max_field_name_len = len(name) 268 self.report.append(self.FieldNode(name, value, linebreak, indent)) 269 270 def AddSubReport(self, title): 271 """Adds and returns a sub-report with a title.""" 272 assert not self.is_finalized 273 sub_report = self.SubReportNode(title, type(self)()) 274 self.report.append(sub_report) 275 return sub_report.report 276 277 def AddSection(self, title): 278 """Adds a new section title.""" 279 assert not self.is_finalized 280 self.last_section = self.SectionNode(title) 281 self.report.append(self.last_section) 282 283 def Finalize(self): 284 """Seals the report, marking it as complete.""" 285 self.is_finalized = True 286 287 288# 289# Payload verification. 290# 291 292class PayloadChecker(object): 293 """Checking the integrity of an update payload. 294 295 This is a short-lived object whose purpose is to isolate the logic used for 296 verifying the integrity of an update payload. 297 """ 298 299 def __init__(self, payload, assert_type=None, block_size=0, 300 allow_unhashed=False, disabled_tests=()): 301 """Initialize the checker. 302 303 Args: 304 payload: The payload object to check. 305 assert_type: Assert that payload is either 'full' or 'delta' (optional). 306 block_size: Expected filesystem / payload block size (optional). 307 allow_unhashed: Allow operations with unhashed data blobs. 308 disabled_tests: Sequence of tests to disable. 309 """ 310 if not payload.is_init: 311 raise ValueError('Uninitialized update payload.') 312 313 # Set checker configuration. 314 self.payload = payload 315 self.block_size = block_size if block_size else _DEFAULT_BLOCK_SIZE 316 if not _IsPowerOfTwo(self.block_size): 317 raise error.PayloadError( 318 'Expected block (%d) size is not a power of two.' % self.block_size) 319 if assert_type not in (None, _TYPE_FULL, _TYPE_DELTA): 320 raise error.PayloadError('Invalid assert_type value (%r).' % 321 assert_type) 322 self.payload_type = assert_type 323 self.allow_unhashed = allow_unhashed 324 325 # Disable specific tests. 326 self.check_move_same_src_dst_block = ( 327 _CHECK_MOVE_SAME_SRC_DST_BLOCK not in disabled_tests) 328 self.check_payload_sig = _CHECK_PAYLOAD_SIG not in disabled_tests 329 330 # Reset state; these will be assigned when the manifest is checked. 331 self.sigs_offset = 0 332 self.sigs_size = 0 333 self.old_part_info = {} 334 self.new_part_info = {} 335 self.new_fs_sizes = collections.defaultdict(int) 336 self.old_fs_sizes = collections.defaultdict(int) 337 self.minor_version = None 338 self.major_version = None 339 340 @staticmethod 341 def _CheckElem(msg, name, report, is_mandatory, is_submsg, convert=str, 342 msg_name=None, linebreak=False, indent=0): 343 """Adds an element from a protobuf message to the payload report. 344 345 Checks to see whether a message contains a given element, and if so adds 346 the element value to the provided report. A missing mandatory element 347 causes an exception to be raised. 348 349 Args: 350 msg: The message containing the element. 351 name: The name of the element. 352 report: A report object to add the element name/value to. 353 is_mandatory: Whether or not this element must be present. 354 is_submsg: Whether this element is itself a message. 355 convert: A function for converting the element value for reporting. 356 msg_name: The name of the message object (for error reporting). 357 linebreak: Whether the value report should induce a line break. 358 indent: Amount of indent used for reporting the value. 359 360 Returns: 361 A pair consisting of the element value and the generated sub-report for 362 it (if the element is a sub-message, None otherwise). If the element is 363 missing, returns (None, None). 364 365 Raises: 366 error.PayloadError if a mandatory element is missing. 367 """ 368 element_result = collections.namedtuple('element_result', ['msg', 'report']) 369 370 if not msg.HasField(name): 371 if is_mandatory: 372 raise error.PayloadError('%smissing mandatory %s %r.' % 373 (msg_name + ' ' if msg_name else '', 374 'sub-message' if is_submsg else 'field', 375 name)) 376 return element_result(None, None) 377 378 value = getattr(msg, name) 379 if is_submsg: 380 return element_result(value, report and report.AddSubReport(name)) 381 else: 382 if report: 383 report.AddField(name, convert(value), linebreak=linebreak, 384 indent=indent) 385 return element_result(value, None) 386 387 @staticmethod 388 def _CheckRepeatedElemNotPresent(msg, field_name, msg_name): 389 """Checks that a repeated element is not specified in the message. 390 391 Args: 392 msg: The message containing the element. 393 field_name: The name of the element. 394 msg_name: The name of the message object (for error reporting). 395 396 Raises: 397 error.PayloadError if the repeated element is present or non-empty. 398 """ 399 if getattr(msg, field_name, None): 400 raise error.PayloadError('%sfield %r not empty.' % 401 (msg_name + ' ' if msg_name else '', field_name)) 402 403 @staticmethod 404 def _CheckElemNotPresent(msg, field_name, msg_name): 405 """Checks that an element is not specified in the message. 406 407 Args: 408 msg: The message containing the element. 409 field_name: The name of the element. 410 msg_name: The name of the message object (for error reporting). 411 412 Raises: 413 error.PayloadError if the repeated element is present. 414 """ 415 if msg.HasField(field_name): 416 raise error.PayloadError('%sfield %r exists.' % 417 (msg_name + ' ' if msg_name else '', field_name)) 418 419 @staticmethod 420 def _CheckMandatoryField(msg, field_name, report, msg_name, convert=str, 421 linebreak=False, indent=0): 422 """Adds a mandatory field; returning first component from _CheckElem.""" 423 return PayloadChecker._CheckElem(msg, field_name, report, True, False, 424 convert=convert, msg_name=msg_name, 425 linebreak=linebreak, indent=indent)[0] 426 427 @staticmethod 428 def _CheckOptionalField(msg, field_name, report, convert=str, 429 linebreak=False, indent=0): 430 """Adds an optional field; returning first component from _CheckElem.""" 431 return PayloadChecker._CheckElem(msg, field_name, report, False, False, 432 convert=convert, linebreak=linebreak, 433 indent=indent)[0] 434 435 @staticmethod 436 def _CheckMandatorySubMsg(msg, submsg_name, report, msg_name): 437 """Adds a mandatory sub-message; wrapper for _CheckElem.""" 438 return PayloadChecker._CheckElem(msg, submsg_name, report, True, True, 439 msg_name) 440 441 @staticmethod 442 def _CheckOptionalSubMsg(msg, submsg_name, report): 443 """Adds an optional sub-message; wrapper for _CheckElem.""" 444 return PayloadChecker._CheckElem(msg, submsg_name, report, False, True) 445 446 @staticmethod 447 def _CheckPresentIff(val1, val2, name1, name2, obj_name): 448 """Checks that val1 is None iff val2 is None. 449 450 Args: 451 val1: first value to be compared. 452 val2: second value to be compared. 453 name1: name of object holding the first value. 454 name2: name of object holding the second value. 455 obj_name: Name of the object containing these values. 456 457 Raises: 458 error.PayloadError if assertion does not hold. 459 """ 460 if None in (val1, val2) and val1 is not val2: 461 present, missing = (name1, name2) if val2 is None else (name2, name1) 462 raise error.PayloadError('%r present without %r%s.' % 463 (present, missing, 464 ' in ' + obj_name if obj_name else '')) 465 466 @staticmethod 467 def _CheckPresentIffMany(vals, name, obj_name): 468 """Checks that a set of vals and names imply every other element. 469 470 Args: 471 vals: The set of values to be compared. 472 name: The name of the objects holding the corresponding value. 473 obj_name: Name of the object containing these values. 474 475 Raises: 476 error.PayloadError if assertion does not hold. 477 """ 478 if any(vals) and not all(vals): 479 raise error.PayloadError('%r is not present in all values%s.' % 480 (name, ' in ' + obj_name if obj_name else '')) 481 482 @staticmethod 483 def _Run(cmd, send_data=None): 484 """Runs a subprocess, returns its output. 485 486 Args: 487 cmd: Sequence of command-line argument for invoking the subprocess. 488 send_data: Data to feed to the process via its stdin. 489 490 Returns: 491 A tuple containing the stdout and stderr output of the process. 492 """ 493 run_process = subprocess.Popen(cmd, stdin=subprocess.PIPE, 494 stdout=subprocess.PIPE) 495 try: 496 result = run_process.communicate(input=send_data) 497 finally: 498 exit_code = run_process.wait() 499 500 if exit_code: 501 raise RuntimeError('Subprocess %r failed with code %r.' % 502 (cmd, exit_code)) 503 504 return result 505 506 @staticmethod 507 def _CheckSha256Signature(sig_data, pubkey_file_name, actual_hash, sig_name): 508 """Verifies an actual hash against a signed one. 509 510 Args: 511 sig_data: The raw signature data. 512 pubkey_file_name: Public key used for verifying signature. 513 actual_hash: The actual hash digest. 514 sig_name: Signature name for error reporting. 515 516 Raises: 517 error.PayloadError if signature could not be verified. 518 """ 519 if len(sig_data) != 256: 520 raise error.PayloadError( 521 '%s: signature size (%d) not as expected (256).' % 522 (sig_name, len(sig_data))) 523 signed_data, _ = PayloadChecker._Run( 524 ['openssl', 'rsautl', '-verify', '-pubin', '-inkey', pubkey_file_name], 525 send_data=sig_data) 526 527 if len(signed_data) != len(common.SIG_ASN1_HEADER) + 32: 528 raise error.PayloadError('%s: unexpected signed data length (%d).' % 529 (sig_name, len(signed_data))) 530 531 if not signed_data.startswith(common.SIG_ASN1_HEADER): 532 raise error.PayloadError('%s: not containing standard ASN.1 prefix.' % 533 sig_name) 534 535 signed_hash = signed_data[len(common.SIG_ASN1_HEADER):] 536 if signed_hash != actual_hash: 537 raise error.PayloadError( 538 '%s: signed hash (%s) different from actual (%s).' % 539 (sig_name, common.FormatSha256(signed_hash), 540 common.FormatSha256(actual_hash))) 541 542 @staticmethod 543 def _CheckBlocksFitLength(length, num_blocks, block_size, length_name, 544 block_name=None): 545 """Checks that a given length fits given block space. 546 547 This ensures that the number of blocks allocated is appropriate for the 548 length of the data residing in these blocks. 549 550 Args: 551 length: The actual length of the data. 552 num_blocks: The number of blocks allocated for it. 553 block_size: The size of each block in bytes. 554 length_name: Name of length (used for error reporting). 555 block_name: Name of block (used for error reporting). 556 557 Raises: 558 error.PayloadError if the aforementioned invariant is not satisfied. 559 """ 560 # Check: length <= num_blocks * block_size. 561 if length > num_blocks * block_size: 562 raise error.PayloadError( 563 '%s (%d) > num %sblocks (%d) * block_size (%d).' % 564 (length_name, length, block_name or '', num_blocks, block_size)) 565 566 # Check: length > (num_blocks - 1) * block_size. 567 if length <= (num_blocks - 1) * block_size: 568 raise error.PayloadError( 569 '%s (%d) <= (num %sblocks - 1 (%d)) * block_size (%d).' % 570 (length_name, length, block_name or '', num_blocks - 1, block_size)) 571 572 def _CheckManifestMinorVersion(self, report): 573 """Checks the payload manifest minor_version field. 574 575 Args: 576 report: The report object to add to. 577 578 Raises: 579 error.PayloadError if any of the checks fail. 580 """ 581 self.minor_version = self._CheckOptionalField(self.payload.manifest, 582 'minor_version', report) 583 if self.minor_version in _SUPPORTED_MINOR_VERSIONS: 584 if self.payload_type not in _SUPPORTED_MINOR_VERSIONS[self.minor_version]: 585 raise error.PayloadError( 586 'Minor version %d not compatible with payload type %s.' % 587 (self.minor_version, self.payload_type)) 588 elif self.minor_version is None: 589 raise error.PayloadError('Minor version is not set.') 590 else: 591 raise error.PayloadError('Unsupported minor version: %d' % 592 self.minor_version) 593 594 def _CheckManifest(self, report, part_sizes=None): 595 """Checks the payload manifest. 596 597 Args: 598 report: A report object to add to. 599 part_sizes: Map of partition label to partition size in bytes. 600 601 Returns: 602 A tuple consisting of the partition block size used during the update 603 (integer), the signatures block offset and size. 604 605 Raises: 606 error.PayloadError if any of the checks fail. 607 """ 608 self.major_version = self.payload.header.version 609 610 part_sizes = part_sizes or collections.defaultdict(int) 611 manifest = self.payload.manifest 612 report.AddSection('manifest') 613 614 # Check: block_size must exist and match the expected value. 615 actual_block_size = self._CheckMandatoryField(manifest, 'block_size', 616 report, 'manifest') 617 if actual_block_size != self.block_size: 618 raise error.PayloadError('Block_size (%d) not as expected (%d).' % 619 (actual_block_size, self.block_size)) 620 621 # Check: signatures_offset <==> signatures_size. 622 self.sigs_offset = self._CheckOptionalField(manifest, 'signatures_offset', 623 report) 624 self.sigs_size = self._CheckOptionalField(manifest, 'signatures_size', 625 report) 626 self._CheckPresentIff(self.sigs_offset, self.sigs_size, 627 'signatures_offset', 'signatures_size', 'manifest') 628 629 for part in manifest.partitions: 630 name = part.partition_name 631 self.old_part_info[name] = self._CheckOptionalSubMsg( 632 part, 'old_partition_info', report) 633 self.new_part_info[name] = self._CheckMandatorySubMsg( 634 part, 'new_partition_info', report, 'manifest.partitions') 635 636 # Check: Old-style partition infos should not be specified. 637 for _, part in common.CROS_PARTITIONS: 638 self._CheckElemNotPresent(manifest, 'old_%s_info' % part, 'manifest') 639 self._CheckElemNotPresent(manifest, 'new_%s_info' % part, 'manifest') 640 641 # Check: If old_partition_info is specified anywhere, it must be 642 # specified everywhere. 643 old_part_msgs = [part.msg for part in self.old_part_info.values() if part] 644 self._CheckPresentIffMany(old_part_msgs, 'old_partition_info', 645 'manifest.partitions') 646 647 is_delta = any(part and part.msg for part in self.old_part_info.values()) 648 if is_delta: 649 # Assert/mark delta payload. 650 if self.payload_type == _TYPE_FULL: 651 raise error.PayloadError( 652 'Apparent full payload contains old_{kernel,rootfs}_info.') 653 self.payload_type = _TYPE_DELTA 654 655 for part, (msg, part_report) in self.old_part_info.items(): 656 # Check: {size, hash} present in old_{kernel,rootfs}_info. 657 field = 'old_%s_info' % part 658 self.old_fs_sizes[part] = self._CheckMandatoryField(msg, 'size', 659 part_report, field) 660 self._CheckMandatoryField(msg, 'hash', part_report, field, 661 convert=common.FormatSha256) 662 663 # Check: old_{kernel,rootfs} size must fit in respective partition. 664 if self.old_fs_sizes[part] > part_sizes[part] > 0: 665 raise error.PayloadError( 666 'Old %s content (%d) exceed partition size (%d).' % 667 (part, self.old_fs_sizes[part], part_sizes[part])) 668 else: 669 # Assert/mark full payload. 670 if self.payload_type == _TYPE_DELTA: 671 raise error.PayloadError( 672 'Apparent delta payload missing old_{kernel,rootfs}_info.') 673 self.payload_type = _TYPE_FULL 674 675 # Check: new_{kernel,rootfs}_info present; contains {size, hash}. 676 for part, (msg, part_report) in self.new_part_info.items(): 677 field = 'new_%s_info' % part 678 self.new_fs_sizes[part] = self._CheckMandatoryField(msg, 'size', 679 part_report, field) 680 self._CheckMandatoryField(msg, 'hash', part_report, field, 681 convert=common.FormatSha256) 682 683 # Check: new_{kernel,rootfs} size must fit in respective partition. 684 if self.new_fs_sizes[part] > part_sizes[part] > 0: 685 raise error.PayloadError( 686 'New %s content (%d) exceed partition size (%d).' % 687 (part, self.new_fs_sizes[part], part_sizes[part])) 688 689 # Check: minor_version makes sense for the payload type. This check should 690 # run after the payload type has been set. 691 self._CheckManifestMinorVersion(report) 692 693 def _CheckLength(self, length, total_blocks, op_name, length_name): 694 """Checks whether a length matches the space designated in extents. 695 696 Args: 697 length: The total length of the data. 698 total_blocks: The total number of blocks in extents. 699 op_name: Operation name (for error reporting). 700 length_name: Length name (for error reporting). 701 702 Raises: 703 error.PayloadError is there a problem with the length. 704 """ 705 # Check: length is non-zero. 706 if length == 0: 707 raise error.PayloadError('%s: %s is zero.' % (op_name, length_name)) 708 709 # Check that length matches number of blocks. 710 self._CheckBlocksFitLength(length, total_blocks, self.block_size, 711 '%s: %s' % (op_name, length_name)) 712 713 def _CheckExtents(self, extents, usable_size, block_counters, name): 714 """Checks a sequence of extents. 715 716 Args: 717 extents: The sequence of extents to check. 718 usable_size: The usable size of the partition to which the extents apply. 719 block_counters: Array of counters corresponding to the number of blocks. 720 name: The name of the extent block. 721 722 Returns: 723 The total number of blocks in the extents. 724 725 Raises: 726 error.PayloadError if any of the entailed checks fails. 727 """ 728 total_num_blocks = 0 729 for ex, ex_name in common.ExtentIter(extents, name): 730 # Check: Mandatory fields. 731 start_block = PayloadChecker._CheckMandatoryField(ex, 'start_block', 732 None, ex_name) 733 num_blocks = PayloadChecker._CheckMandatoryField(ex, 'num_blocks', None, 734 ex_name) 735 end_block = start_block + num_blocks 736 737 # Check: num_blocks > 0. 738 if num_blocks == 0: 739 raise error.PayloadError('%s: extent length is zero.' % ex_name) 740 741 # Check: Make sure we're within the partition limit. 742 if usable_size and end_block * self.block_size > usable_size: 743 raise error.PayloadError( 744 '%s: extent (%s) exceeds usable partition size (%d).' % 745 (ex_name, common.FormatExtent(ex, self.block_size), usable_size)) 746 747 # Record block usage. 748 for i in range(start_block, end_block): 749 block_counters[i] += 1 750 751 total_num_blocks += num_blocks 752 753 return total_num_blocks 754 755 def _CheckReplaceOperation(self, op, data_length, total_dst_blocks, op_name): 756 """Specific checks for REPLACE/REPLACE_BZ/REPLACE_XZ operations. 757 758 Args: 759 op: The operation object from the manifest. 760 data_length: The length of the data blob associated with the operation. 761 total_dst_blocks: Total number of blocks in dst_extents. 762 op_name: Operation name for error reporting. 763 764 Raises: 765 error.PayloadError if any check fails. 766 """ 767 # Check: total_dst_blocks is not a floating point. 768 if isinstance(total_dst_blocks, float): 769 raise error.PayloadError('%s: contains invalid data type of ' 770 'total_dst_blocks.' % op_name) 771 772 # Check: Does not contain src extents. 773 if op.src_extents: 774 raise error.PayloadError('%s: contains src_extents.' % op_name) 775 776 # Check: Contains data. 777 if data_length is None: 778 raise error.PayloadError('%s: missing data_{offset,length}.' % op_name) 779 780 if op.type == common.OpType.REPLACE: 781 PayloadChecker._CheckBlocksFitLength(data_length, total_dst_blocks, 782 self.block_size, 783 op_name + '.data_length', 'dst') 784 else: 785 # Check: data_length must be smaller than the allotted dst blocks. 786 if data_length >= total_dst_blocks * self.block_size: 787 raise error.PayloadError( 788 '%s: data_length (%d) must be less than allotted dst block ' 789 'space (%d * %d).' % 790 (op_name, data_length, total_dst_blocks, self.block_size)) 791 792 def _CheckZeroOperation(self, op, op_name): 793 """Specific checks for ZERO operations. 794 795 Args: 796 op: The operation object from the manifest. 797 op_name: Operation name for error reporting. 798 799 Raises: 800 error.PayloadError if any check fails. 801 """ 802 # Check: Does not contain src extents, data_length and data_offset. 803 if op.src_extents: 804 raise error.PayloadError('%s: contains src_extents.' % op_name) 805 if op.data_length: 806 raise error.PayloadError('%s: contains data_length.' % op_name) 807 if op.data_offset: 808 raise error.PayloadError('%s: contains data_offset.' % op_name) 809 810 def _CheckAnyDiffOperation(self, op, data_length, total_dst_blocks, op_name): 811 """Specific checks for SOURCE_BSDIFF, PUFFDIFF and BROTLI_BSDIFF 812 operations. 813 814 Args: 815 op: The operation. 816 data_length: The length of the data blob associated with the operation. 817 total_dst_blocks: Total number of blocks in dst_extents. 818 op_name: Operation name for error reporting. 819 820 Raises: 821 error.PayloadError if any check fails. 822 """ 823 # Check: data_{offset,length} present. 824 if data_length is None: 825 raise error.PayloadError('%s: missing data_{offset,length}.' % op_name) 826 827 # Check: data_length is strictly smaller than the allotted dst blocks. 828 if data_length >= total_dst_blocks * self.block_size: 829 raise error.PayloadError( 830 '%s: data_length (%d) must be smaller than allotted dst space ' 831 '(%d * %d = %d).' % 832 (op_name, data_length, total_dst_blocks, self.block_size, 833 total_dst_blocks * self.block_size)) 834 835 # Check the existence of src_length and dst_length for legacy bsdiffs. 836 if op.type == common.OpType.SOURCE_BSDIFF and self.minor_version <= 3: 837 if not op.HasField('src_length') or not op.HasField('dst_length'): 838 raise error.PayloadError('%s: require {src,dst}_length.' % op_name) 839 else: 840 if op.HasField('src_length') or op.HasField('dst_length'): 841 raise error.PayloadError('%s: unneeded {src,dst}_length.' % op_name) 842 843 def _CheckSourceCopyOperation(self, data_offset, total_src_blocks, 844 total_dst_blocks, op_name): 845 """Specific checks for SOURCE_COPY. 846 847 Args: 848 data_offset: The offset of a data blob for the operation. 849 total_src_blocks: Total number of blocks in src_extents. 850 total_dst_blocks: Total number of blocks in dst_extents. 851 op_name: Operation name for error reporting. 852 853 Raises: 854 error.PayloadError if any check fails. 855 """ 856 # Check: No data_{offset,length}. 857 if data_offset is not None: 858 raise error.PayloadError('%s: contains data_{offset,length}.' % op_name) 859 860 # Check: total_src_blocks == total_dst_blocks. 861 if total_src_blocks != total_dst_blocks: 862 raise error.PayloadError( 863 '%s: total src blocks (%d) != total dst blocks (%d).' % 864 (op_name, total_src_blocks, total_dst_blocks)) 865 866 def _CheckAnySourceOperation(self, op, total_src_blocks, op_name): 867 """Specific checks for SOURCE_* operations. 868 869 Args: 870 op: The operation object from the manifest. 871 total_src_blocks: Total number of blocks in src_extents. 872 op_name: Operation name for error reporting. 873 874 Raises: 875 error.PayloadError if any check fails. 876 """ 877 # Check: total_src_blocks != 0. 878 if total_src_blocks == 0: 879 raise error.PayloadError('%s: no src blocks in a source op.' % op_name) 880 881 # Check: src_sha256_hash present in minor version >= 3. 882 if self.minor_version >= 3 and op.src_sha256_hash is None: 883 raise error.PayloadError('%s: source hash missing.' % op_name) 884 885 def _CheckOperation(self, op, op_name, old_block_counters, new_block_counters, 886 old_usable_size, new_usable_size, prev_data_offset, 887 blob_hash_counts): 888 """Checks a single update operation. 889 890 Args: 891 op: The operation object. 892 op_name: Operation name string for error reporting. 893 old_block_counters: Arrays of block read counters. 894 new_block_counters: Arrays of block write counters. 895 old_usable_size: The overall usable size for src data in bytes. 896 new_usable_size: The overall usable size for dst data in bytes. 897 prev_data_offset: Offset of last used data bytes. 898 blob_hash_counts: Counters for hashed/unhashed blobs. 899 900 Returns: 901 The amount of data blob associated with the operation. 902 903 Raises: 904 error.PayloadError if any check has failed. 905 """ 906 # Check extents. 907 total_src_blocks = self._CheckExtents( 908 op.src_extents, old_usable_size, old_block_counters, 909 op_name + '.src_extents') 910 total_dst_blocks = self._CheckExtents( 911 op.dst_extents, new_usable_size, new_block_counters, 912 op_name + '.dst_extents') 913 914 # Check: data_offset present <==> data_length present. 915 data_offset = self._CheckOptionalField(op, 'data_offset', None) 916 data_length = self._CheckOptionalField(op, 'data_length', None) 917 self._CheckPresentIff(data_offset, data_length, 'data_offset', 918 'data_length', op_name) 919 920 # Check: At least one dst_extent. 921 if not op.dst_extents: 922 raise error.PayloadError('%s: dst_extents is empty.' % op_name) 923 924 # Check {src,dst}_length, if present. 925 if op.HasField('src_length'): 926 self._CheckLength(op.src_length, total_src_blocks, op_name, 'src_length') 927 if op.HasField('dst_length'): 928 self._CheckLength(op.dst_length, total_dst_blocks, op_name, 'dst_length') 929 930 if op.HasField('data_sha256_hash'): 931 blob_hash_counts['hashed'] += 1 932 933 # Check: Operation carries data. 934 if data_offset is None: 935 raise error.PayloadError( 936 '%s: data_sha256_hash present but no data_{offset,length}.' % 937 op_name) 938 939 # Check: Hash verifies correctly. 940 actual_hash = hashlib.sha256(self.payload.ReadDataBlob(data_offset, 941 data_length)) 942 if op.data_sha256_hash != actual_hash.digest(): 943 raise error.PayloadError( 944 '%s: data_sha256_hash (%s) does not match actual hash (%s).' % 945 (op_name, common.FormatSha256(op.data_sha256_hash), 946 common.FormatSha256(actual_hash.digest()))) 947 elif data_offset is not None: 948 if self.allow_unhashed: 949 blob_hash_counts['unhashed'] += 1 950 else: 951 raise error.PayloadError('%s: unhashed operation not allowed.' % 952 op_name) 953 954 if data_offset is not None: 955 # Check: Contiguous use of data section. 956 if data_offset != prev_data_offset: 957 raise error.PayloadError( 958 '%s: data offset (%d) not matching amount used so far (%d).' % 959 (op_name, data_offset, prev_data_offset)) 960 961 # Type-specific checks. 962 if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ, 963 common.OpType.REPLACE_XZ): 964 self._CheckReplaceOperation(op, data_length, total_dst_blocks, op_name) 965 elif op.type == common.OpType.ZERO and self.minor_version >= 4: 966 self._CheckZeroOperation(op, op_name) 967 elif op.type == common.OpType.SOURCE_COPY and self.minor_version >= 2: 968 self._CheckSourceCopyOperation(data_offset, total_src_blocks, 969 total_dst_blocks, op_name) 970 self._CheckAnySourceOperation(op, total_src_blocks, op_name) 971 elif op.type == common.OpType.SOURCE_BSDIFF and self.minor_version >= 2: 972 self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name) 973 self._CheckAnySourceOperation(op, total_src_blocks, op_name) 974 elif op.type == common.OpType.BROTLI_BSDIFF and self.minor_version >= 4: 975 self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name) 976 self._CheckAnySourceOperation(op, total_src_blocks, op_name) 977 elif op.type == common.OpType.PUFFDIFF and self.minor_version >= 5: 978 self._CheckAnyDiffOperation(op, data_length, total_dst_blocks, op_name) 979 self._CheckAnySourceOperation(op, total_src_blocks, op_name) 980 else: 981 raise error.PayloadError( 982 'Operation %s (type %d) not allowed in minor version %d' % 983 (op_name, op.type, self.minor_version)) 984 return data_length if data_length is not None else 0 985 986 def _SizeToNumBlocks(self, size): 987 """Returns the number of blocks needed to contain a given byte size.""" 988 return (size + self.block_size - 1) // self.block_size 989 990 def _AllocBlockCounters(self, total_size): 991 """Returns a freshly initialized array of block counters. 992 993 Note that the generated array is not portable as is due to byte-ordering 994 issues, hence it should not be serialized. 995 996 Args: 997 total_size: The total block size in bytes. 998 999 Returns: 1000 An array of unsigned short elements initialized to zero, one for each of 1001 the blocks necessary for containing the partition. 1002 """ 1003 return array.array('H', 1004 itertools.repeat(0, self._SizeToNumBlocks(total_size))) 1005 1006 def _CheckOperations(self, operations, report, base_name, old_fs_size, 1007 new_fs_size, old_usable_size, new_usable_size, 1008 prev_data_offset): 1009 """Checks a sequence of update operations. 1010 1011 Args: 1012 operations: The sequence of operations to check. 1013 report: The report object to add to. 1014 base_name: The name of the operation block. 1015 old_fs_size: The old filesystem size in bytes. 1016 new_fs_size: The new filesystem size in bytes. 1017 old_usable_size: The overall usable size of the old partition in bytes. 1018 new_usable_size: The overall usable size of the new partition in bytes. 1019 prev_data_offset: Offset of last used data bytes. 1020 1021 Returns: 1022 The total data blob size used. 1023 1024 Raises: 1025 error.PayloadError if any of the checks fails. 1026 """ 1027 # The total size of data blobs used by operations scanned thus far. 1028 total_data_used = 0 1029 # Counts of specific operation types. 1030 op_counts = { 1031 common.OpType.REPLACE: 0, 1032 common.OpType.REPLACE_BZ: 0, 1033 common.OpType.REPLACE_XZ: 0, 1034 common.OpType.ZERO: 0, 1035 common.OpType.SOURCE_COPY: 0, 1036 common.OpType.SOURCE_BSDIFF: 0, 1037 common.OpType.PUFFDIFF: 0, 1038 common.OpType.BROTLI_BSDIFF: 0, 1039 } 1040 # Total blob sizes for each operation type. 1041 op_blob_totals = { 1042 common.OpType.REPLACE: 0, 1043 common.OpType.REPLACE_BZ: 0, 1044 common.OpType.REPLACE_XZ: 0, 1045 # SOURCE_COPY operations don't have blobs. 1046 common.OpType.SOURCE_BSDIFF: 0, 1047 common.OpType.PUFFDIFF: 0, 1048 common.OpType.BROTLI_BSDIFF: 0, 1049 } 1050 # Counts of hashed vs unhashed operations. 1051 blob_hash_counts = { 1052 'hashed': 0, 1053 'unhashed': 0, 1054 } 1055 1056 # Allocate old and new block counters. 1057 old_block_counters = (self._AllocBlockCounters(old_usable_size) 1058 if old_fs_size else None) 1059 new_block_counters = self._AllocBlockCounters(new_usable_size) 1060 1061 # Process and verify each operation. 1062 op_num = 0 1063 for op, op_name in common.OperationIter(operations, base_name): 1064 op_num += 1 1065 1066 # Check: Type is valid. 1067 if op.type not in op_counts: 1068 raise error.PayloadError('%s: invalid type (%d).' % (op_name, op.type)) 1069 op_counts[op.type] += 1 1070 1071 curr_data_used = self._CheckOperation( 1072 op, op_name, old_block_counters, new_block_counters, 1073 old_usable_size, new_usable_size, 1074 prev_data_offset + total_data_used, blob_hash_counts) 1075 if curr_data_used: 1076 op_blob_totals[op.type] += curr_data_used 1077 total_data_used += curr_data_used 1078 1079 # Report totals and breakdown statistics. 1080 report.AddField('total operations', op_num) 1081 report.AddField( 1082 None, 1083 histogram.Histogram.FromCountDict(op_counts, 1084 key_names=common.OpType.NAMES), 1085 indent=1) 1086 report.AddField('total blobs', sum(blob_hash_counts.values())) 1087 report.AddField(None, 1088 histogram.Histogram.FromCountDict(blob_hash_counts), 1089 indent=1) 1090 report.AddField('total blob size', _AddHumanReadableSize(total_data_used)) 1091 report.AddField( 1092 None, 1093 histogram.Histogram.FromCountDict(op_blob_totals, 1094 formatter=_AddHumanReadableSize, 1095 key_names=common.OpType.NAMES), 1096 indent=1) 1097 1098 # Report read/write histograms. 1099 if old_block_counters: 1100 report.AddField('block read hist', 1101 histogram.Histogram.FromKeyList(old_block_counters), 1102 linebreak=True, indent=1) 1103 1104 new_write_hist = histogram.Histogram.FromKeyList( 1105 new_block_counters[:self._SizeToNumBlocks(new_fs_size)]) 1106 report.AddField('block write hist', new_write_hist, linebreak=True, 1107 indent=1) 1108 1109 # Check: Full update must write each dst block once. 1110 if self.payload_type == _TYPE_FULL and new_write_hist.GetKeys() != [1]: 1111 raise error.PayloadError( 1112 '%s: not all blocks written exactly once during full update.' % 1113 base_name) 1114 1115 return total_data_used 1116 1117 def _CheckSignatures(self, report, pubkey_file_name): 1118 """Checks a payload's signature block.""" 1119 sigs_raw = self.payload.ReadDataBlob(self.sigs_offset, self.sigs_size) 1120 sigs = update_metadata_pb2.Signatures() 1121 sigs.ParseFromString(sigs_raw) 1122 report.AddSection('signatures') 1123 1124 # Check: At least one signature present. 1125 if not sigs.signatures: 1126 raise error.PayloadError('Signature block is empty.') 1127 1128 # Check that we don't have the signature operation blob at the end (used to 1129 # be for major version 1). 1130 last_partition = self.payload.manifest.partitions[-1] 1131 if last_partition.operations: 1132 last_op = last_partition.operations[-1] 1133 # Check: signatures_{offset,size} must match the last (fake) operation. 1134 if (last_op.type == common.OpType.REPLACE and 1135 last_op.data_offset == self.sigs_offset and 1136 last_op.data_length == self.sigs_size): 1137 raise error.PayloadError('It seems like the last operation is the ' 1138 'signature blob. This is an invalid payload.') 1139 1140 # Compute the checksum of all data up to signature blob. 1141 # TODO(garnold) we're re-reading the whole data section into a string 1142 # just to compute the checksum; instead, we could do it incrementally as 1143 # we read the blobs one-by-one, under the assumption that we're reading 1144 # them in order (which currently holds). This should be reconsidered. 1145 payload_hasher = self.payload.manifest_hasher.copy() 1146 common.Read(self.payload.payload_file, self.sigs_offset, 1147 offset=self.payload.data_offset, hasher=payload_hasher) 1148 1149 for sig, sig_name in common.SignatureIter(sigs.signatures, 'signatures'): 1150 sig_report = report.AddSubReport(sig_name) 1151 1152 # Check: Signature contains mandatory fields. 1153 self._CheckMandatoryField(sig, 'data', None, sig_name) 1154 sig_report.AddField('data len', len(sig.data)) 1155 1156 # Check: Signatures pertains to actual payload hash. 1157 if sig.data: 1158 self._CheckSha256Signature(sig.data, pubkey_file_name, 1159 payload_hasher.digest(), sig_name) 1160 1161 def Run(self, pubkey_file_name=None, metadata_sig_file=None, metadata_size=0, 1162 part_sizes=None, report_out_file=None): 1163 """Checker entry point, invoking all checks. 1164 1165 Args: 1166 pubkey_file_name: Public key used for signature verification. 1167 metadata_sig_file: Metadata signature, if verification is desired. 1168 metadata_size: Metadata size, if verification is desired. 1169 part_sizes: Mapping of partition label to size in bytes (default: infer 1170 based on payload type and version or filesystem). 1171 report_out_file: File object to dump the report to. 1172 1173 Raises: 1174 error.PayloadError if payload verification failed. 1175 """ 1176 if not pubkey_file_name: 1177 pubkey_file_name = _DEFAULT_PUBKEY_FILE_NAME 1178 1179 report = _PayloadReport() 1180 1181 # Get payload file size. 1182 self.payload.payload_file.seek(0, 2) 1183 payload_file_size = self.payload.payload_file.tell() 1184 self.payload.ResetFile() 1185 1186 try: 1187 # Check metadata_size (if provided). 1188 if metadata_size and self.payload.metadata_size != metadata_size: 1189 raise error.PayloadError('Invalid payload metadata size in payload(%d) ' 1190 'vs given(%d)' % (self.payload.metadata_size, 1191 metadata_size)) 1192 1193 # Check metadata signature (if provided). 1194 if metadata_sig_file: 1195 metadata_sig = base64.b64decode(metadata_sig_file.read()) 1196 self._CheckSha256Signature(metadata_sig, pubkey_file_name, 1197 self.payload.manifest_hasher.digest(), 1198 'metadata signature') 1199 1200 # Part 1: Check the file header. 1201 report.AddSection('header') 1202 # Check: Payload version is valid. 1203 if self.payload.header.version not in (1, 2): 1204 raise error.PayloadError('Unknown payload version (%d).' % 1205 self.payload.header.version) 1206 report.AddField('version', self.payload.header.version) 1207 report.AddField('manifest len', self.payload.header.manifest_len) 1208 1209 # Part 2: Check the manifest. 1210 self._CheckManifest(report, part_sizes) 1211 assert self.payload_type, 'payload type should be known by now' 1212 1213 # Make sure deprecated values are not present in the payload. 1214 for field in ('install_operations', 'kernel_install_operations'): 1215 self._CheckRepeatedElemNotPresent(self.payload.manifest, field, 1216 'manifest') 1217 for field in ('old_kernel_info', 'old_rootfs_info', 1218 'new_kernel_info', 'new_rootfs_info'): 1219 self._CheckElemNotPresent(self.payload.manifest, field, 'manifest') 1220 1221 total_blob_size = 0 1222 for part, operations in ((p.partition_name, p.operations) 1223 for p in self.payload.manifest.partitions): 1224 report.AddSection('%s operations' % part) 1225 1226 new_fs_usable_size = self.new_fs_sizes[part] 1227 old_fs_usable_size = self.old_fs_sizes[part] 1228 1229 if part_sizes is not None and part_sizes.get(part, None): 1230 new_fs_usable_size = old_fs_usable_size = part_sizes[part] 1231 1232 # TODO(chromium:243559) only default to the filesystem size if no 1233 # explicit size provided *and* the partition size is not embedded in the 1234 # payload; see issue for more details. 1235 total_blob_size += self._CheckOperations( 1236 operations, report, '%s_install_operations' % part, 1237 self.old_fs_sizes[part], self.new_fs_sizes[part], 1238 old_fs_usable_size, new_fs_usable_size, total_blob_size) 1239 1240 # Check: Operations data reach the end of the payload file. 1241 used_payload_size = self.payload.data_offset + total_blob_size 1242 # Major versions 2 and higher have a signature at the end, so it should be 1243 # considered in the total size of the image. 1244 if self.sigs_size: 1245 used_payload_size += self.sigs_size 1246 1247 if used_payload_size != payload_file_size: 1248 raise error.PayloadError( 1249 'Used payload size (%d) different from actual file size (%d).' % 1250 (used_payload_size, payload_file_size)) 1251 1252 # Part 4: Handle payload signatures message. 1253 if self.check_payload_sig and self.sigs_size: 1254 self._CheckSignatures(report, pubkey_file_name) 1255 1256 # Part 5: Summary. 1257 report.AddSection('summary') 1258 report.AddField('update type', self.payload_type) 1259 1260 report.Finalize() 1261 finally: 1262 if report_out_file: 1263 report.Dump(report_out_file) 1264