blob: 703b1663c573d3cf392086ed124468d1fc4861c5 [file] [log] [blame]
Gilad Arnold553b0ec2013-01-26 01:00:39 -08001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Verifying the integrity of a Chrome OS update payload.
6
7This module is used internally by the main Payload class for verifying the
8integrity of an update payload. The interface for invoking the checks is as
9follows:
10
11 checker = PayloadChecker(payload)
12 checker.Run(...)
13
14"""
15
16import array
17import base64
18import hashlib
19import subprocess
20
21import common
22from error import PayloadError
23import format_utils
24import histogram
25import update_metadata_pb2
26
27
28#
29# Constants / helper functions.
30#
Gilad Arnoldeaed0d12013-04-30 15:38:22 -070031_CHECK_DST_PSEUDO_EXTENTS = 'dst-pseudo-extents'
32_CHECK_MOVE_SAME_SRC_DST_BLOCK = 'move-same-src-dst-block'
33_CHECK_PAYLOAD_SIG = 'payload-sig'
34CHECKS_TO_DISABLE = (
35 _CHECK_DST_PSEUDO_EXTENTS,
36 _CHECK_MOVE_SAME_SRC_DST_BLOCK,
37 _CHECK_PAYLOAD_SIG,
38)
39
Gilad Arnold553b0ec2013-01-26 01:00:39 -080040_TYPE_FULL = 'full'
41_TYPE_DELTA = 'delta'
42
43_DEFAULT_BLOCK_SIZE = 4096
44
45
46#
47# Helper functions.
48#
49def _IsPowerOfTwo(val):
50 """Returns True iff val is a power of two."""
51 return val > 0 and (val & (val - 1)) == 0
52
53
54def _AddFormat(format_func, value):
55 """Adds a custom formatted representation to ordinary string representation.
56
57 Args:
58 format_func: a value formatter
59 value: value to be formatted and returned
60 Returns:
61 A string 'x (y)' where x = str(value) and y = format_func(value).
62
63 """
64 return '%s (%s)' % (value, format_func(value))
65
66
67def _AddHumanReadableSize(size):
68 """Adds a human readable representation to a byte size value."""
69 return _AddFormat(format_utils.BytesToHumanReadable, size)
70
71
72#
73# Payload report generator.
74#
75class _PayloadReport(object):
76 """A payload report generator.
77
78 A report is essentially a sequence of nodes, which represent data points. It
79 is initialized to have a "global", untitled section. A node may be a
80 sub-report itself.
81
82 """
83
84 # Report nodes: field, sub-report, section.
85 class Node(object):
86 """A report node interface."""
87
88 @staticmethod
89 def _Indent(indent, line):
90 """Indents a line by a given indentation amount.
91
92 Args:
93 indent: the indentation amount
94 line: the line content (string)
95 Returns:
96 The properly indented line (string).
97
98 """
99 return '%*s%s' % (indent, '', line)
100
101 def GenerateLines(self, base_indent, sub_indent, curr_section):
102 """Generates the report lines for this node.
103
104 Args:
105 base_indent: base indentation for each line
106 sub_indent: additional indentation for sub-nodes
107 curr_section: the current report section object
108 Returns:
109 A pair consisting of a list of properly indented report lines and a new
110 current section object.
111
112 """
113 raise NotImplementedError()
114
115 class FieldNode(Node):
116 """A field report node, representing a (name, value) pair."""
117
118 def __init__(self, name, value, linebreak, indent):
119 super(_PayloadReport.FieldNode, self).__init__()
120 self.name = name
121 self.value = value
122 self.linebreak = linebreak
123 self.indent = indent
124
125 def GenerateLines(self, base_indent, sub_indent, curr_section):
126 """Generates a properly formatted 'name : value' entry."""
127 report_output = ''
128 if self.name:
129 report_output += self.name.ljust(curr_section.max_field_name_len) + ' :'
130 value_lines = str(self.value).splitlines()
131 if self.linebreak and self.name:
132 report_output += '\n' + '\n'.join(
133 ['%*s%s' % (self.indent, '', line) for line in value_lines])
134 else:
135 if self.name:
136 report_output += ' '
137 report_output += '%*s' % (self.indent, '')
138 cont_line_indent = len(report_output)
139 indented_value_lines = [value_lines[0]]
140 indented_value_lines.extend(['%*s%s' % (cont_line_indent, '', line)
141 for line in value_lines[1:]])
142 report_output += '\n'.join(indented_value_lines)
143
144 report_lines = [self._Indent(base_indent, line + '\n')
145 for line in report_output.split('\n')]
146 return report_lines, curr_section
147
148 class SubReportNode(Node):
149 """A sub-report node, representing a nested report."""
150
151 def __init__(self, title, report):
152 super(_PayloadReport.SubReportNode, self).__init__()
153 self.title = title
154 self.report = report
155
156 def GenerateLines(self, base_indent, sub_indent, curr_section):
157 """Recurse with indentation."""
158 report_lines = [self._Indent(base_indent, self.title + ' =>\n')]
159 report_lines.extend(self.report.GenerateLines(base_indent + sub_indent,
160 sub_indent))
161 return report_lines, curr_section
162
163 class SectionNode(Node):
164 """A section header node."""
165
166 def __init__(self, title=None):
167 super(_PayloadReport.SectionNode, self).__init__()
168 self.title = title
169 self.max_field_name_len = 0
170
171 def GenerateLines(self, base_indent, sub_indent, curr_section):
172 """Dump a title line, return self as the (new) current section."""
173 report_lines = []
174 if self.title:
175 report_lines.append(self._Indent(base_indent,
176 '=== %s ===\n' % self.title))
177 return report_lines, self
178
179 def __init__(self):
180 self.report = []
181 self.last_section = self.global_section = self.SectionNode()
182 self.is_finalized = False
183
184 def GenerateLines(self, base_indent, sub_indent):
185 """Generates the lines in the report, properly indented.
186
187 Args:
188 base_indent: the indentation used for root-level report lines
189 sub_indent: the indentation offset used for sub-reports
190 Returns:
191 A list of indented report lines.
192
193 """
194 report_lines = []
195 curr_section = self.global_section
196 for node in self.report:
197 node_report_lines, curr_section = node.GenerateLines(
198 base_indent, sub_indent, curr_section)
199 report_lines.extend(node_report_lines)
200
201 return report_lines
202
203 def Dump(self, out_file, base_indent=0, sub_indent=2):
204 """Dumps the report to a file.
205
206 Args:
207 out_file: file object to output the content to
208 base_indent: base indentation for report lines
209 sub_indent: added indentation for sub-reports
210
211 """
212
213 report_lines = self.GenerateLines(base_indent, sub_indent)
214 if report_lines and not self.is_finalized:
215 report_lines.append('(incomplete report)\n')
216
217 for line in report_lines:
218 out_file.write(line)
219
220 def AddField(self, name, value, linebreak=False, indent=0):
221 """Adds a field/value pair to the payload report.
222
223 Args:
224 name: the field's name
225 value: the field's value
226 linebreak: whether the value should be printed on a new line
227 indent: amount of extra indent for each line of the value
228
229 """
230 assert not self.is_finalized
231 if name and self.last_section.max_field_name_len < len(name):
232 self.last_section.max_field_name_len = len(name)
233 self.report.append(self.FieldNode(name, value, linebreak, indent))
234
235 def AddSubReport(self, title):
236 """Adds and returns a sub-report with a title."""
237 assert not self.is_finalized
238 sub_report = self.SubReportNode(title, type(self)())
239 self.report.append(sub_report)
240 return sub_report.report
241
242 def AddSection(self, title):
243 """Adds a new section title."""
244 assert not self.is_finalized
245 self.last_section = self.SectionNode(title)
246 self.report.append(self.last_section)
247
248 def Finalize(self):
249 """Seals the report, marking it as complete."""
250 self.is_finalized = True
251
252
253#
254# Payload verification.
255#
256class PayloadChecker(object):
257 """Checking the integrity of an update payload.
258
259 This is a short-lived object whose purpose is to isolate the logic used for
260 verifying the integrity of an update payload.
261
262 """
263
Gilad Arnoldeaed0d12013-04-30 15:38:22 -0700264 def __init__(self, payload, assert_type=None, block_size=0,
265 allow_unhashed=False, disabled_tests=()):
266 """Initialize the checker object.
267
268 Args:
269 payload: the payload object to check
270 assert_type: assert that payload is either 'full' or 'delta' (optional)
271 block_size: expected filesystem / payload block size (optional)
272 allow_unhashed: allow operations with unhashed data blobs
273 disabled_tests: list of tests to disable
274
275 """
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800276 assert payload.is_init, 'uninitialized update payload'
Gilad Arnoldeaed0d12013-04-30 15:38:22 -0700277
278 # Set checker configuration.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800279 self.payload = payload
Gilad Arnoldeaed0d12013-04-30 15:38:22 -0700280 self.block_size = block_size if block_size else _DEFAULT_BLOCK_SIZE
281 if not _IsPowerOfTwo(self.block_size):
282 raise PayloadError('expected block (%d) size is not a power of two' %
283 self.block_size)
284 if assert_type not in (None, _TYPE_FULL, _TYPE_DELTA):
285 raise PayloadError("invalid assert_type value (`%s')" % assert_type)
286 self.payload_type = assert_type
287 self.allow_unhashed = allow_unhashed
288
289 # Disable specific tests.
290 self.check_dst_pseudo_extents = (
291 _CHECK_DST_PSEUDO_EXTENTS not in disabled_tests)
292 self.check_move_same_src_dst_block = (
293 _CHECK_MOVE_SAME_SRC_DST_BLOCK not in disabled_tests)
294 self.check_payload_sig = _CHECK_PAYLOAD_SIG not in disabled_tests
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800295
296 # Reset state; these will be assigned when the manifest is checked.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800297 self.sigs_offset = 0
298 self.sigs_size = 0
299 self.old_rootfs_size = 0
300 self.old_kernel_size = 0
301 self.new_rootfs_size = 0
302 self.new_kernel_size = 0
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800303
304 @staticmethod
305 def _CheckElem(msg, name, report, is_mandatory, is_submsg, convert=str,
306 msg_name=None, linebreak=False, indent=0):
307 """Adds an element from a protobuf message to the payload report.
308
309 Checks to see whether a message contains a given element, and if so adds
310 the element value to the provided report. A missing mandatory element
311 causes an exception to be raised.
312
313 Args:
314 msg: the message containing the element
315 name: the name of the element
316 report: a report object to add the element name/value to
317 is_mandatory: whether or not this element must be present
318 is_submsg: whether this element is itself a message
319 convert: a function for converting the element value for reporting
320 msg_name: the name of the message object (for error reporting)
321 linebreak: whether the value report should induce a line break
322 indent: amount of indent used for reporting the value
323 Returns:
324 A pair consisting of the element value and the generated sub-report for
325 it (if the element is a sub-message, None otherwise). If the element is
326 missing, returns (None, None).
327 Raises:
328 PayloadError if a mandatory element is missing.
329
330 """
331 if not msg.HasField(name):
332 if is_mandatory:
333 raise PayloadError("%smissing mandatory %s '%s'" %
334 (msg_name + ' ' if msg_name else '',
335 'sub-message' if is_submsg else 'field',
336 name))
337 return (None, None)
338
339 value = getattr(msg, name)
340 if is_submsg:
341 return (value, report and report.AddSubReport(name))
342 else:
343 if report:
344 report.AddField(name, convert(value), linebreak=linebreak,
345 indent=indent)
346 return (value, None)
347
348 @staticmethod
349 def _CheckMandatoryField(msg, field_name, report, msg_name, convert=str,
350 linebreak=False, indent=0):
351 """Adds a mandatory field; returning first component from _CheckElem."""
352 return PayloadChecker._CheckElem(msg, field_name, report, True, False,
353 convert=convert, msg_name=msg_name,
354 linebreak=linebreak, indent=indent)[0]
355
356 @staticmethod
357 def _CheckOptionalField(msg, field_name, report, convert=str,
358 linebreak=False, indent=0):
359 """Adds an optional field; returning first component from _CheckElem."""
360 return PayloadChecker._CheckElem(msg, field_name, report, False, False,
361 convert=convert, linebreak=linebreak,
362 indent=indent)[0]
363
364 @staticmethod
365 def _CheckMandatorySubMsg(msg, submsg_name, report, msg_name):
366 """Adds a mandatory sub-message; wrapper for _CheckElem."""
367 return PayloadChecker._CheckElem(msg, submsg_name, report, True, True,
368 msg_name)
369
370 @staticmethod
371 def _CheckOptionalSubMsg(msg, submsg_name, report):
372 """Adds an optional sub-message; wrapper for _CheckElem."""
373 return PayloadChecker._CheckElem(msg, submsg_name, report, False, True)
374
375 @staticmethod
376 def _CheckPresentIff(val1, val2, name1, name2, obj_name):
377 """Checks that val1 is None iff val2 is None.
378
379 Args:
380 val1: first value to be compared
381 val2: second value to be compared
382 name1: name of object holding the first value
383 name2: name of object holding the second value
384 obj_name: name of the object containing these values
385 Raises:
386 PayloadError if assertion does not hold.
387
388 """
389 if None in (val1, val2) and val1 is not val2:
390 present, missing = (name1, name2) if val2 is None else (name2, name1)
391 raise PayloadError("'%s' present without '%s'%s" %
392 (present, missing,
393 ' in ' + obj_name if obj_name else ''))
394
395 @staticmethod
396 def _Run(cmd, send_data=None):
397 """Runs a subprocess, returns its output.
398
399 Args:
400 cmd: list of command-line argument for invoking the subprocess
401 send_data: data to feed to the process via its stdin
402 Returns:
403 A tuple containing the stdout and stderr output of the process.
404
405 """
406 run_process = subprocess.Popen(cmd, stdin=subprocess.PIPE,
407 stdout=subprocess.PIPE)
408 return run_process.communicate(input=send_data)
409
410 @staticmethod
411 def _CheckSha256Signature(sig_data, pubkey_file_name, actual_hash, sig_name):
412 """Verifies an actual hash against a signed one.
413
414 Args:
415 sig_data: the raw signature data
416 pubkey_file_name: public key used for verifying signature
417 actual_hash: the actual hash digest
418 sig_name: signature name for error reporting
419 Raises:
420 PayloadError if signature could not be verified.
421
422 """
423 if len(sig_data) != 256:
424 raise PayloadError('%s: signature size (%d) not as expected (256)' %
425 (sig_name, len(sig_data)))
426 signed_data, _ = PayloadChecker._Run(
427 ['openssl', 'rsautl', '-verify', '-pubin', '-inkey', pubkey_file_name],
428 send_data=sig_data)
429
Gilad Arnold5502b562013-03-08 13:22:31 -0800430 if len(signed_data) != len(common.SIG_ASN1_HEADER) + 32:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800431 raise PayloadError('%s: unexpected signed data length (%d)' %
432 (sig_name, len(signed_data)))
433
Gilad Arnold5502b562013-03-08 13:22:31 -0800434 if not signed_data.startswith(common.SIG_ASN1_HEADER):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800435 raise PayloadError('%s: not containing standard ASN.1 prefix' % sig_name)
436
Gilad Arnold5502b562013-03-08 13:22:31 -0800437 signed_hash = signed_data[len(common.SIG_ASN1_HEADER):]
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800438 if signed_hash != actual_hash:
439 raise PayloadError('%s: signed hash (%s) different from actual (%s)' %
440 (sig_name, signed_hash.encode('hex'),
441 actual_hash.encode('hex')))
442
443 @staticmethod
444 def _CheckBlocksFitLength(length, num_blocks, block_size, length_name,
445 block_name=None):
446 """Checks that a given length fits given block space.
447
448 This ensures that the number of blocks allocated is appropriate for the
449 length of the data residing in these blocks.
450
451 Args:
452 length: the actual length of the data
453 num_blocks: the number of blocks allocated for it
454 block_size: the size of each block in bytes
455 length_name: name of length (used for error reporting)
456 block_name: name of block (used for error reporting)
457 Raises:
458 PayloadError if the aforementioned invariant is not satisfied.
459
460 """
461 # Check: length <= num_blocks * block_size.
462 if not length <= num_blocks * block_size:
463 raise PayloadError(
464 '%s (%d) > num %sblocks (%d) * block_size (%d)' %
465 (length_name, length, block_name or '', num_blocks, block_size))
466
467 # Check: length > (num_blocks - 1) * block_size.
468 if not length > (num_blocks - 1) * block_size:
469 raise PayloadError(
470 '%s (%d) <= (num %sblocks - 1 (%d)) * block_size (%d)' %
471 (length_name, length, block_name or '', num_blocks - 1, block_size))
472
473 def _CheckManifest(self, report):
474 """Checks the payload manifest.
475
476 Args:
477 report: a report object to add to
478 Returns:
479 A tuple consisting of the partition block size used during the update
480 (integer), the signatures block offset and size.
481 Raises:
482 PayloadError if any of the checks fail.
483
484 """
485 manifest = self.payload.manifest
486 report.AddSection('manifest')
487
488 # Check: block_size must exist and match the expected value.
489 actual_block_size = self._CheckMandatoryField(manifest, 'block_size',
490 report, 'manifest')
491 if actual_block_size != self.block_size:
492 raise PayloadError('block_size (%d) not as expected (%d)' %
493 (actual_block_size, self.block_size))
494
495 # Check: signatures_offset <==> signatures_size.
496 self.sigs_offset = self._CheckOptionalField(manifest, 'signatures_offset',
497 report)
498 self.sigs_size = self._CheckOptionalField(manifest, 'signatures_size',
499 report)
500 self._CheckPresentIff(self.sigs_offset, self.sigs_size,
501 'signatures_offset', 'signatures_size', 'manifest')
502
503 # Check: old_kernel_info <==> old_rootfs_info.
504 oki_msg, oki_report = self._CheckOptionalSubMsg(manifest,
505 'old_kernel_info', report)
506 ori_msg, ori_report = self._CheckOptionalSubMsg(manifest,
507 'old_rootfs_info', report)
508 self._CheckPresentIff(oki_msg, ori_msg, 'old_kernel_info',
509 'old_rootfs_info', 'manifest')
510 if oki_msg: # equivalently, ori_msg
511 # Assert/mark delta payload.
512 if self.payload_type == _TYPE_FULL:
513 raise PayloadError(
514 'apparent full payload contains old_{kernel,rootfs}_info')
515 self.payload_type = _TYPE_DELTA
516
517 # Check: {size, hash} present in old_{kernel,rootfs}_info.
518 self.old_kernel_size = self._CheckMandatoryField(
519 oki_msg, 'size', oki_report, 'old_kernel_info')
520 self._CheckMandatoryField(oki_msg, 'hash', oki_report, 'old_kernel_info',
521 convert=common.FormatSha256)
522 self.old_rootfs_size = self._CheckMandatoryField(
523 ori_msg, 'size', ori_report, 'old_rootfs_info')
524 self._CheckMandatoryField(ori_msg, 'hash', ori_report, 'old_rootfs_info',
525 convert=common.FormatSha256)
526 else:
527 # Assert/mark full payload.
528 if self.payload_type == _TYPE_DELTA:
529 raise PayloadError(
530 'apparent delta payload missing old_{kernel,rootfs}_info')
531 self.payload_type = _TYPE_FULL
532
533 # Check: new_kernel_info present; contains {size, hash}.
534 nki_msg, nki_report = self._CheckMandatorySubMsg(
535 manifest, 'new_kernel_info', report, 'manifest')
536 self.new_kernel_size = self._CheckMandatoryField(
537 nki_msg, 'size', nki_report, 'new_kernel_info')
538 self._CheckMandatoryField(nki_msg, 'hash', nki_report, 'new_kernel_info',
539 convert=common.FormatSha256)
540
541 # Check: new_rootfs_info present; contains {size, hash}.
542 nri_msg, nri_report = self._CheckMandatorySubMsg(
543 manifest, 'new_rootfs_info', report, 'manifest')
544 self.new_rootfs_size = self._CheckMandatoryField(
545 nri_msg, 'size', nri_report, 'new_rootfs_info')
546 self._CheckMandatoryField(nri_msg, 'hash', nri_report, 'new_rootfs_info',
547 convert=common.FormatSha256)
548
549 # Check: payload must contain at least one operation.
550 if not(len(manifest.install_operations) or
551 len(manifest.kernel_install_operations)):
552 raise PayloadError('payload contains no operations')
553
554 def _CheckLength(self, length, total_blocks, op_name, length_name):
555 """Checks whether a length matches the space designated in extents.
556
557 Args:
558 length: the total length of the data
559 total_blocks: the total number of blocks in extents
560 op_name: operation name (for error reporting)
561 length_name: length name (for error reporting)
562 Raises:
563 PayloadError is there a problem with the length.
564
565 """
566 # Check: length is non-zero.
567 if length == 0:
568 raise PayloadError('%s: %s is zero' % (op_name, length_name))
569
570 # Check that length matches number of blocks.
571 self._CheckBlocksFitLength(length, total_blocks, self.block_size,
572 '%s: %s' % (op_name, length_name))
573
574 def _CheckExtents(self, extents, part_size, block_counters, name,
575 allow_pseudo=False, allow_signature=False):
576 """Checks a sequence of extents.
577
578 Args:
579 extents: the sequence of extents to check
580 part_size: the total size of the partition to which the extents apply
581 block_counters: an array of counters corresponding to the number of blocks
582 name: the name of the extent block
583 allow_pseudo: whether or not pseudo block numbers are allowed
584 allow_signature: whether or not the extents are used for a signature
585 Returns:
586 The total number of blocks in the extents.
587 Raises:
588 PayloadError if any of the entailed checks fails.
589
590 """
591 total_num_blocks = 0
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800592 for ex, ex_name in common.ExtentIter(extents, name):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800593 # Check: mandatory fields.
594 start_block = PayloadChecker._CheckMandatoryField(ex, 'start_block',
595 None, ex_name)
596 num_blocks = PayloadChecker._CheckMandatoryField(ex, 'num_blocks', None,
597 ex_name)
598 end_block = start_block + num_blocks
599
600 # Check: num_blocks > 0.
601 if num_blocks == 0:
602 raise PayloadError('%s: extent length is zero' % ex_name)
603
604 if start_block != common.PSEUDO_EXTENT_MARKER:
605 # Check: make sure we're within the partition limit.
Gilad Arnoldaa55d1a2013-03-08 12:05:59 -0800606 if part_size and end_block * self.block_size > part_size:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800607 raise PayloadError(
608 '%s: extent (%s) exceeds partition size (%d)' %
609 (ex_name, common.FormatExtent(ex, self.block_size), part_size))
610
611 # Record block usage.
612 for i in range(start_block, end_block):
613 block_counters[i] += 1
Gilad Arnold5502b562013-03-08 13:22:31 -0800614 elif not (allow_pseudo or (allow_signature and len(extents) == 1)):
615 # Pseudo-extents must be allowed explicitly, or otherwise be part of a
616 # signature operation (in which case there has to be exactly one).
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800617 raise PayloadError('%s: unexpected pseudo-extent' % ex_name)
618
619 total_num_blocks += num_blocks
620
621 return total_num_blocks
622
623 def _CheckReplaceOperation(self, op, data_length, total_dst_blocks, op_name):
624 """Specific checks for REPLACE/REPLACE_BZ operations.
625
626 Args:
627 op: the operation object from the manifest
628 data_length: the length of the data blob associated with the operation
629 total_dst_blocks: total number of blocks in dst_extents
630 op_name: operation name for error reporting
631 Raises:
632 PayloadError if any check fails.
633
634 """
Gilad Arnold5502b562013-03-08 13:22:31 -0800635 # Check: does not contain src extents.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800636 if op.src_extents:
637 raise PayloadError('%s: contains src_extents' % op_name)
638
Gilad Arnold5502b562013-03-08 13:22:31 -0800639 # Check: contains data.
640 if data_length is None:
641 raise PayloadError('%s: missing data_{offset,length}' % op_name)
642
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800643 if op.type == common.OpType.REPLACE:
644 PayloadChecker._CheckBlocksFitLength(data_length, total_dst_blocks,
645 self.block_size,
646 op_name + '.data_length', 'dst')
647 else:
648 # Check: data_length must be smaller than the alotted dst blocks.
649 if data_length >= total_dst_blocks * self.block_size:
650 raise PayloadError(
651 '%s: data_length (%d) must be less than allotted dst block '
652 'space (%d * %d)' %
653 (op_name, data_length, total_dst_blocks, self.block_size))
654
655 def _CheckMoveOperation(self, op, data_offset, total_src_blocks,
656 total_dst_blocks, op_name):
657 """Specific checks for MOVE operations.
658
659 Args:
660 op: the operation object from the manifest
661 data_offset: the offset of a data blob for the operation
662 total_src_blocks: total number of blocks in src_extents
663 total_dst_blocks: total number of blocks in dst_extents
664 op_name: operation name for error reporting
665 Raises:
666 PayloadError if any check fails.
667
668 """
669 # Check: no data_{offset,length}.
670 if data_offset is not None:
671 raise PayloadError('%s: contains data_{offset,length}' % op_name)
672
673 # Check: total src blocks == total dst blocks.
674 if total_src_blocks != total_dst_blocks:
675 raise PayloadError(
676 '%s: total src blocks (%d) != total dst blocks (%d)' %
677 (op_name, total_src_blocks, total_dst_blocks))
678
679 # Check: for all i, i-th src block index != i-th dst block index.
680 i = 0
681 src_extent_iter = iter(op.src_extents)
682 dst_extent_iter = iter(op.dst_extents)
683 src_extent = dst_extent = None
684 src_idx = src_num = dst_idx = dst_num = 0
685 while i < total_src_blocks:
686 # Get the next source extent, if needed.
687 if not src_extent:
688 try:
689 src_extent = src_extent_iter.next()
690 except StopIteration:
691 raise PayloadError('%s: ran out of src extents (%d/%d)' %
692 (op_name, i, total_src_blocks))
693 src_idx = src_extent.start_block
694 src_num = src_extent.num_blocks
695
696 # Get the next dest extent, if needed.
697 if not dst_extent:
698 try:
699 dst_extent = dst_extent_iter.next()
700 except StopIteration:
701 raise PayloadError('%s: ran out of dst extents (%d/%d)' %
702 (op_name, i, total_dst_blocks))
703 dst_idx = dst_extent.start_block
704 dst_num = dst_extent.num_blocks
705
Gilad Arnoldeaed0d12013-04-30 15:38:22 -0700706 if self.check_move_same_src_dst_block and src_idx == dst_idx:
Gilad Arnold5502b562013-03-08 13:22:31 -0800707 raise PayloadError('%s: src/dst block number %d is the same (%d)' %
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800708 (op_name, i, src_idx))
709
710 advance = min(src_num, dst_num)
711 i += advance
712
713 src_idx += advance
714 src_num -= advance
715 if src_num == 0:
716 src_extent = None
717
718 dst_idx += advance
719 dst_num -= advance
720 if dst_num == 0:
721 dst_extent = None
722
Gilad Arnold5502b562013-03-08 13:22:31 -0800723 # Make sure we've exhausted all src/dst extents.
724 if src_extent:
725 raise PayloadError('%s: excess src blocks' % op_name)
726 if dst_extent:
727 raise PayloadError('%s: excess dst blocks' % op_name)
728
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800729 def _CheckBsdiffOperation(self, data_length, total_dst_blocks, op_name):
730 """Specific checks for BSDIFF operations.
731
732 Args:
733 data_length: the length of the data blob associated with the operation
734 total_dst_blocks: total number of blocks in dst_extents
735 op_name: operation name for error reporting
736 Raises:
737 PayloadError if any check fails.
738
739 """
Gilad Arnold5502b562013-03-08 13:22:31 -0800740 # Check: data_{offset,length} present.
741 if data_length is None:
742 raise PayloadError('%s: missing data_{offset,length}' % op_name)
743
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800744 # Check: data_length is strictly smaller than the alotted dst blocks.
745 if data_length >= total_dst_blocks * self.block_size:
746 raise PayloadError(
Gilad Arnold5502b562013-03-08 13:22:31 -0800747 '%s: data_length (%d) must be smaller than allotted dst space '
748 '(%d * %d = %d)' %
749 (op_name, data_length, total_dst_blocks, self.block_size,
750 total_dst_blocks * self.block_size))
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800751
752 def _CheckOperation(self, op, op_name, is_last, old_block_counters,
753 new_block_counters, old_part_size, new_part_size,
Gilad Arnoldeaed0d12013-04-30 15:38:22 -0700754 prev_data_offset, allow_signature, blob_hash_counts):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800755 """Checks a single update operation.
756
757 Args:
758 op: the operation object
759 op_name: operation name string for error reporting
760 is_last: whether this is the last operation in the sequence
761 old_block_counters: arrays of block read counters
762 new_block_counters: arrays of block write counters
763 old_part_size: the source partition size in bytes
764 new_part_size: the target partition size in bytes
765 prev_data_offset: offset of last used data bytes
766 allow_signature: whether this may be a signature operation
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800767 blob_hash_counts: counters for hashed/unhashed blobs
768 Returns:
769 The amount of data blob associated with the operation.
770 Raises:
771 PayloadError if any check has failed.
772
773 """
774 # Check extents.
775 total_src_blocks = self._CheckExtents(
776 op.src_extents, old_part_size, old_block_counters,
777 op_name + '.src_extents', allow_pseudo=True)
778 allow_signature_in_extents = (allow_signature and is_last and
779 op.type == common.OpType.REPLACE)
780 total_dst_blocks = self._CheckExtents(
781 op.dst_extents, new_part_size, new_block_counters,
Gilad Arnoldeaed0d12013-04-30 15:38:22 -0700782 op_name + '.dst_extents',
783 allow_pseudo=(not self.check_dst_pseudo_extents),
784 allow_signature=allow_signature_in_extents)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800785
786 # Check: data_offset present <==> data_length present.
787 data_offset = self._CheckOptionalField(op, 'data_offset', None)
788 data_length = self._CheckOptionalField(op, 'data_length', None)
789 self._CheckPresentIff(data_offset, data_length, 'data_offset',
790 'data_length', op_name)
791
792 # Check: at least one dst_extent.
793 if not op.dst_extents:
794 raise PayloadError('%s: dst_extents is empty' % op_name)
795
796 # Check {src,dst}_length, if present.
797 if op.HasField('src_length'):
798 self._CheckLength(op.src_length, total_src_blocks, op_name, 'src_length')
799 if op.HasField('dst_length'):
800 self._CheckLength(op.dst_length, total_dst_blocks, op_name, 'dst_length')
801
802 if op.HasField('data_sha256_hash'):
803 blob_hash_counts['hashed'] += 1
804
805 # Check: operation carries data.
806 if data_offset is None:
807 raise PayloadError(
808 '%s: data_sha256_hash present but no data_{offset,length}' %
809 op_name)
810
811 # Check: hash verifies correctly.
812 # pylint: disable=E1101
813 actual_hash = hashlib.sha256(self.payload.ReadDataBlob(data_offset,
814 data_length))
815 if op.data_sha256_hash != actual_hash.digest():
816 raise PayloadError(
817 '%s: data_sha256_hash (%s) does not match actual hash (%s)' %
818 (op_name, op.data_sha256_hash.encode('hex'),
819 actual_hash.hexdigest()))
820 elif data_offset is not None:
821 if allow_signature_in_extents:
822 blob_hash_counts['signature'] += 1
Gilad Arnoldeaed0d12013-04-30 15:38:22 -0700823 elif self.allow_unhashed:
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800824 blob_hash_counts['unhashed'] += 1
825 else:
826 raise PayloadError('%s: unhashed operation not allowed' % op_name)
827
828 if data_offset is not None:
829 # Check: contiguous use of data section.
830 if data_offset != prev_data_offset:
831 raise PayloadError(
832 '%s: data offset (%d) not matching amount used so far (%d)' %
833 (op_name, data_offset, prev_data_offset))
834
835 # Type-specific checks.
836 if op.type in (common.OpType.REPLACE, common.OpType.REPLACE_BZ):
837 self._CheckReplaceOperation(op, data_length, total_dst_blocks, op_name)
838 elif self.payload_type == _TYPE_FULL:
839 raise PayloadError('%s: non-REPLACE operation in a full payload' %
840 op_name)
841 elif op.type == common.OpType.MOVE:
842 self._CheckMoveOperation(op, data_offset, total_src_blocks,
843 total_dst_blocks, op_name)
844 elif op.type == common.OpType.BSDIFF:
845 self._CheckBsdiffOperation(data_length, total_dst_blocks, op_name)
846 else:
847 assert False, 'cannot get here'
848
849 return data_length if data_length is not None else 0
850
Gilad Arnold5502b562013-03-08 13:22:31 -0800851 def _AllocBlockCounters(self, part_size):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800852 """Returns a freshly initialized array of block counters.
853
854 Args:
855 part_size: the size of the partition
856 Returns:
857 An array of unsigned char elements initialized to zero, one for each of
858 the blocks necessary for containing the partition.
859
860 """
861 num_blocks = (part_size + self.block_size - 1) / self.block_size
862 return array.array('B', [0] * num_blocks)
863
864 def _CheckOperations(self, operations, report, base_name, old_part_size,
Gilad Arnoldeaed0d12013-04-30 15:38:22 -0700865 new_part_size, prev_data_offset, allow_signature):
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800866 """Checks a sequence of update operations.
867
868 Args:
869 operations: the sequence of operations to check
870 report: the report object to add to
871 base_name: the name of the operation block
872 old_part_size: the old partition size in bytes
873 new_part_size: the new partition size in bytes
874 prev_data_offset: offset of last used data bytes
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800875 allow_signature: whether this sequence may contain signature operations
876 Returns:
Gilad Arnold5502b562013-03-08 13:22:31 -0800877 The total data blob size used.
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800878 Raises:
879 PayloadError if any of the checks fails.
880
881 """
882 # The total size of data blobs used by operations scanned thus far.
883 total_data_used = 0
884 # Counts of specific operation types.
885 op_counts = {
886 common.OpType.REPLACE: 0,
887 common.OpType.REPLACE_BZ: 0,
888 common.OpType.MOVE: 0,
889 common.OpType.BSDIFF: 0,
890 }
891 # Total blob sizes for each operation type.
892 op_blob_totals = {
893 common.OpType.REPLACE: 0,
894 common.OpType.REPLACE_BZ: 0,
895 # MOVE operations don't have blobs
896 common.OpType.BSDIFF: 0,
897 }
898 # Counts of hashed vs unhashed operations.
899 blob_hash_counts = {
900 'hashed': 0,
901 'unhashed': 0,
902 }
903 if allow_signature:
904 blob_hash_counts['signature'] = 0
905
906 # Allocate old and new block counters.
Gilad Arnold5502b562013-03-08 13:22:31 -0800907 old_block_counters = (self._AllocBlockCounters(old_part_size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800908 if old_part_size else None)
Gilad Arnold5502b562013-03-08 13:22:31 -0800909 new_block_counters = self._AllocBlockCounters(new_part_size)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800910
911 # Process and verify each operation.
912 op_num = 0
913 for op, op_name in common.OperationIter(operations, base_name):
914 op_num += 1
915
916 # Check: type is valid.
917 if op.type not in op_counts.keys():
918 raise PayloadError('%s: invalid type (%d)' % (op_name, op.type))
919 op_counts[op.type] += 1
920
921 is_last = op_num == len(operations)
922 curr_data_used = self._CheckOperation(
923 op, op_name, is_last, old_block_counters, new_block_counters,
924 old_part_size, new_part_size, prev_data_offset + total_data_used,
Gilad Arnoldeaed0d12013-04-30 15:38:22 -0700925 allow_signature, blob_hash_counts)
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800926 if curr_data_used:
927 op_blob_totals[op.type] += curr_data_used
928 total_data_used += curr_data_used
929
930 # Report totals and breakdown statistics.
931 report.AddField('total operations', op_num)
932 report.AddField(
933 None,
934 histogram.Histogram.FromCountDict(op_counts,
935 key_names=common.OpType.NAMES),
936 indent=1)
937 report.AddField('total blobs', sum(blob_hash_counts.values()))
938 report.AddField(None,
939 histogram.Histogram.FromCountDict(blob_hash_counts),
940 indent=1)
941 report.AddField('total blob size', _AddHumanReadableSize(total_data_used))
942 report.AddField(
943 None,
944 histogram.Histogram.FromCountDict(op_blob_totals,
945 formatter=_AddHumanReadableSize,
946 key_names=common.OpType.NAMES),
947 indent=1)
948
949 # Report read/write histograms.
950 if old_block_counters:
951 report.AddField('block read hist',
952 histogram.Histogram.FromKeyList(old_block_counters),
953 linebreak=True, indent=1)
954
955 new_write_hist = histogram.Histogram.FromKeyList(new_block_counters)
956 # Check: full update must write each dst block once.
957 if self.payload_type == _TYPE_FULL and new_write_hist.GetKeys() != [1]:
958 raise PayloadError(
959 '%s: not all blocks written exactly once during full update' %
960 base_name)
961
962 report.AddField('block write hist', new_write_hist, linebreak=True,
963 indent=1)
964
965 return total_data_used
966
967 def _CheckSignatures(self, report, pubkey_file_name):
968 """Checks a payload's signature block."""
969 sigs_raw = self.payload.ReadDataBlob(self.sigs_offset, self.sigs_size)
970 sigs = update_metadata_pb2.Signatures()
971 sigs.ParseFromString(sigs_raw)
972 report.AddSection('signatures')
973
974 # Check: at least one signature present.
975 # pylint: disable=E1101
976 if not sigs.signatures:
977 raise PayloadError('signature block is empty')
978
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800979 last_ops_section = (self.payload.manifest.kernel_install_operations or
980 self.payload.manifest.install_operations)
981 fake_sig_op = last_ops_section[-1]
Gilad Arnold5502b562013-03-08 13:22:31 -0800982 # Check: signatures_{offset,size} must match the last (fake) operation.
983 if not (fake_sig_op.type == common.OpType.REPLACE and
984 self.sigs_offset == fake_sig_op.data_offset and
Gilad Arnold553b0ec2013-01-26 01:00:39 -0800985 self.sigs_size == fake_sig_op.data_length):
986 raise PayloadError(
987 'signatures_{offset,size} (%d+%d) does not match last operation '
988 '(%d+%d)' %
989 (self.sigs_offset, self.sigs_size, fake_sig_op.data_offset,
990 fake_sig_op.data_length))
991
992 # Compute the checksum of all data up to signature blob.
993 # TODO(garnold) we're re-reading the whole data section into a string
994 # just to compute the checksum; instead, we could do it incrementally as
995 # we read the blobs one-by-one, under the assumption that we're reading
996 # them in order (which currently holds). This should be reconsidered.
997 payload_hasher = self.payload.manifest_hasher.copy()
998 common.Read(self.payload.payload_file, self.sigs_offset,
999 offset=self.payload.data_offset, hasher=payload_hasher)
1000
1001 for sig, sig_name in common.SignatureIter(sigs.signatures, 'signatures'):
1002 sig_report = report.AddSubReport(sig_name)
1003
1004 # Check: signature contains mandatory fields.
1005 self._CheckMandatoryField(sig, 'version', sig_report, sig_name)
1006 self._CheckMandatoryField(sig, 'data', None, sig_name)
1007 sig_report.AddField('data len', len(sig.data))
1008
1009 # Check: signatures pertains to actual payload hash.
1010 if sig.version == 1:
1011 self._CheckSha256Signature(sig.data, pubkey_file_name,
1012 payload_hasher.digest(), sig_name)
1013 else:
1014 raise PayloadError('unknown signature version (%d)' % sig.version)
1015
1016 def Run(self, pubkey_file_name=None, metadata_sig_file=None,
Gilad Arnoldeaed0d12013-04-30 15:38:22 -07001017 report_out_file=None):
Gilad Arnold553b0ec2013-01-26 01:00:39 -08001018 """Checker entry point, invoking all checks.
1019
1020 Args:
1021 pubkey_file_name: public key used for signature verification
1022 metadata_sig_file: metadata signature, if verification is desired
1023 report_out_file: file object to dump the report to
Gilad Arnold553b0ec2013-01-26 01:00:39 -08001024 Raises:
1025 PayloadError if payload verification failed.
1026
1027 """
1028 report = _PayloadReport()
1029
Gilad Arnold553b0ec2013-01-26 01:00:39 -08001030 # Get payload file size.
1031 self.payload.payload_file.seek(0, 2)
1032 payload_file_size = self.payload.payload_file.tell()
1033 self.payload.ResetFile()
1034
1035 try:
1036 # Check metadata signature (if provided).
1037 if metadata_sig_file:
1038 if not pubkey_file_name:
1039 raise PayloadError(
1040 'no public key provided, cannot verify metadata signature')
1041 metadata_sig = base64.b64decode(metadata_sig_file.read())
1042 self._CheckSha256Signature(metadata_sig, pubkey_file_name,
1043 self.payload.manifest_hasher.digest(),
1044 'metadata signature')
1045
1046 # Part 1: check the file header.
1047 report.AddSection('header')
1048 # Check: payload version is valid.
1049 if self.payload.header.version != 1:
1050 raise PayloadError('unknown payload version (%d)' %
1051 self.payload.header.version)
1052 report.AddField('version', self.payload.header.version)
1053 report.AddField('manifest len', self.payload.header.manifest_len)
1054
1055 # Part 2: check the manifest.
1056 self._CheckManifest(report)
1057 assert self.payload_type, 'payload type should be known by now'
1058
1059 # Part 3: examine rootfs operations.
1060 report.AddSection('rootfs operations')
1061 total_blob_size = self._CheckOperations(
1062 self.payload.manifest.install_operations, report,
Gilad Arnoldeaed0d12013-04-30 15:38:22 -07001063 'install_operations', self.old_rootfs_size, self.new_rootfs_size, 0,
1064 False)
Gilad Arnold553b0ec2013-01-26 01:00:39 -08001065
1066 # Part 4: examine kernel operations.
1067 report.AddSection('kernel operations')
1068 total_blob_size += self._CheckOperations(
1069 self.payload.manifest.kernel_install_operations, report,
1070 'kernel_install_operations', self.old_kernel_size,
Gilad Arnoldeaed0d12013-04-30 15:38:22 -07001071 self.new_kernel_size, total_blob_size, True)
Gilad Arnold553b0ec2013-01-26 01:00:39 -08001072
1073 # Check: operations data reach the end of the payload file.
1074 used_payload_size = self.payload.data_offset + total_blob_size
1075 if used_payload_size != payload_file_size:
1076 raise PayloadError(
1077 'used payload size (%d) different from actual file size (%d)' %
1078 (used_payload_size, payload_file_size))
1079
1080 # Part 5: handle payload signatures message.
Gilad Arnoldeaed0d12013-04-30 15:38:22 -07001081 if self.check_payload_sig and self.sigs_size:
Gilad Arnold553b0ec2013-01-26 01:00:39 -08001082 if not pubkey_file_name:
1083 raise PayloadError(
1084 'no public key provided, cannot verify payload signature')
1085 self._CheckSignatures(report, pubkey_file_name)
1086
1087 # Part 6: summary.
1088 report.AddSection('summary')
1089 report.AddField('update type', self.payload_type)
1090
1091 report.Finalize()
1092 finally:
1093 if report_out_file:
1094 report.Dump(report_out_file)