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"""Tools for reading, verifying and applying Chrome OS update payloads.""" 18 19from __future__ import absolute_import 20from __future__ import print_function 21import binascii 22 23import hashlib 24import io 25import mmap 26import struct 27import zipfile 28 29import update_metadata_pb2 30 31from update_payload import checker 32from update_payload import common 33from update_payload.error import PayloadError 34 35 36# 37# Helper functions. 38# 39def _ReadInt(file_obj, size, is_unsigned, hasher=None): 40 """Reads a binary-encoded integer from a file. 41 42 It will do the correct conversion based on the reported size and whether or 43 not a signed number is expected. Assumes a network (big-endian) byte 44 ordering. 45 46 Args: 47 file_obj: a file object 48 size: the integer size in bytes (2, 4 or 8) 49 is_unsigned: whether it is signed or not 50 hasher: an optional hasher to pass the value through 51 52 Returns: 53 An "unpacked" (Python) integer value. 54 55 Raises: 56 PayloadError if an read error occurred. 57 """ 58 return struct.unpack(common.IntPackingFmtStr(size, is_unsigned), 59 common.Read(file_obj, size, hasher=hasher))[0] 60 61 62# 63# Update payload. 64# 65class Payload(object): 66 """Chrome OS update payload processor.""" 67 68 class _PayloadHeader(object): 69 """Update payload header struct.""" 70 71 # Header constants; sizes are in bytes. 72 _MAGIC = b'CrAU' 73 _VERSION_SIZE = 8 74 _MANIFEST_LEN_SIZE = 8 75 _METADATA_SIGNATURE_LEN_SIZE = 4 76 77 def __init__(self): 78 self.version = None 79 self.manifest_len = None 80 self.metadata_signature_len = None 81 self.size = None 82 83 def ReadFromPayload(self, payload_file, hasher=None): 84 """Reads the payload header from a file. 85 86 Reads the payload header from the |payload_file| and updates the |hasher| 87 if one is passed. The parsed header is stored in the _PayloadHeader 88 instance attributes. 89 90 Args: 91 payload_file: a file object 92 hasher: an optional hasher to pass the value through 93 94 Returns: 95 None. 96 97 Raises: 98 PayloadError if a read error occurred or the header is invalid. 99 """ 100 # Verify magic 101 magic = common.Read(payload_file, len(self._MAGIC), hasher=hasher) 102 if magic != self._MAGIC: 103 raise PayloadError('invalid payload magic: %s' % magic) 104 105 self.version = _ReadInt(payload_file, self._VERSION_SIZE, True, 106 hasher=hasher) 107 self.manifest_len = _ReadInt(payload_file, self._MANIFEST_LEN_SIZE, True, 108 hasher=hasher) 109 self.size = (len(self._MAGIC) + self._VERSION_SIZE + 110 self._MANIFEST_LEN_SIZE) 111 self.metadata_signature_len = 0 112 113 if self.version == common.BRILLO_MAJOR_PAYLOAD_VERSION: 114 self.size += self._METADATA_SIGNATURE_LEN_SIZE 115 self.metadata_signature_len = _ReadInt( 116 payload_file, self._METADATA_SIGNATURE_LEN_SIZE, True, 117 hasher=hasher) 118 119 def __init__(self, payload_file, payload_file_offset=0): 120 """Initialize the payload object. 121 122 Args: 123 payload_file: update payload file object open for reading 124 payload_file_offset: the offset of the actual payload 125 """ 126 if zipfile.is_zipfile(payload_file): 127 self.name = payload_file 128 with zipfile.ZipFile(payload_file) as zfp: 129 if "payload.bin" not in zfp.namelist(): 130 raise ValueError(f"payload.bin missing in archive {payload_file}") 131 self.payload_file = zfp.open("payload.bin", "r") 132 elif isinstance(payload_file, str): 133 self.name = payload_file 134 payload_fp = open(payload_file, "rb") 135 payload_bytes = mmap.mmap( 136 payload_fp.fileno(), 0, access=mmap.ACCESS_READ) 137 self.payload_file = io.BytesIO(payload_bytes) 138 else: 139 self.name = payload_file.name 140 self.payload_file = payload_file 141 self.payload_file_size = self.payload_file.seek(0, io.SEEK_END) 142 self.payload_file.seek(0, io.SEEK_SET) 143 self.payload_file_offset = payload_file_offset 144 self.manifest_hasher = None 145 self.is_init = False 146 self.header = None 147 self.manifest = None 148 self.data_offset = None 149 self.metadata_signature = None 150 self.payload_signature = None 151 self.metadata_size = None 152 self.Init() 153 154 @property 155 def is_incremental(self): 156 return any([part.HasField("old_partition_info") for part in self.manifest.partitions]) 157 158 @property 159 def is_partial(self): 160 return self.manifest.partial_update 161 162 @property 163 def total_data_length(self): 164 """Return the total data length of this payload, excluding payload 165 signature at the very end. 166 """ 167 # Operations are sorted in ascending data_offset order, so iterating 168 # backwards and find the first one with non zero data_offset will tell 169 # us total data length 170 for partition in reversed(self.manifest.partitions): 171 for op in reversed(partition.operations): 172 if op.data_length > 0: 173 return op.data_offset + op.data_length 174 return 0 175 176 def _ReadHeader(self): 177 """Reads and returns the payload header. 178 179 Returns: 180 A payload header object. 181 182 Raises: 183 PayloadError if a read error occurred. 184 """ 185 header = self._PayloadHeader() 186 header.ReadFromPayload(self.payload_file, self.manifest_hasher) 187 return header 188 189 def _ReadManifest(self): 190 """Reads and returns the payload manifest. 191 192 Returns: 193 A string containing the payload manifest in binary form. 194 195 Raises: 196 PayloadError if a read error occurred. 197 """ 198 if not self.header: 199 raise PayloadError('payload header not present') 200 201 return common.Read(self.payload_file, self.header.manifest_len, 202 hasher=self.manifest_hasher) 203 204 def _ReadMetadataSignature(self): 205 """Reads and returns the metadata signatures. 206 207 Returns: 208 A string containing the metadata signatures protobuf in binary form or 209 an empty string if no metadata signature found in the payload. 210 211 Raises: 212 PayloadError if a read error occurred. 213 """ 214 if not self.header: 215 raise PayloadError('payload header not present') 216 217 return common.Read( 218 self.payload_file, self.header.metadata_signature_len, 219 offset=self.payload_file_offset + self.header.size + 220 self.header.manifest_len) 221 222 def ReadDataBlob(self, offset, length): 223 """Reads and returns a single data blob from the update payload. 224 225 Args: 226 offset: offset to the beginning of the blob from the end of the manifest 227 length: the blob's length 228 229 Returns: 230 A string containing the raw blob data. 231 232 Raises: 233 PayloadError if a read error occurred. 234 """ 235 return common.Read(self.payload_file, length, 236 offset=self.payload_file_offset + self.data_offset + 237 offset) 238 239 def Init(self): 240 """Initializes the payload object. 241 242 This is a prerequisite for any other public API call. 243 244 Raises: 245 PayloadError if object already initialized or fails to initialize 246 correctly. 247 """ 248 if self.is_init: 249 return 250 251 self.manifest_hasher = hashlib.sha256() 252 253 # Read the file header. 254 self.payload_file.seek(self.payload_file_offset) 255 self.header = self._ReadHeader() 256 257 # Read the manifest. 258 manifest_raw = self._ReadManifest() 259 self.manifest = update_metadata_pb2.DeltaArchiveManifest() 260 self.manifest.ParseFromString(manifest_raw) 261 262 # Read the metadata signature (if any). 263 metadata_signature_raw = self._ReadMetadataSignature() 264 if metadata_signature_raw: 265 self.metadata_signature = update_metadata_pb2.Signatures() 266 self.metadata_signature.ParseFromString(metadata_signature_raw) 267 268 self.metadata_size = self.header.size + self.header.manifest_len 269 self.data_offset = self.metadata_size + self.header.metadata_signature_len 270 271 if self.manifest.signatures_offset and self.manifest.signatures_size and self.manifest.signatures_offset + self.manifest.signatures_size <= self.payload_file_size: 272 payload_signature_blob = self.ReadDataBlob( 273 self.manifest.signatures_offset, self.manifest.signatures_size) 274 payload_signature = update_metadata_pb2.Signatures() 275 payload_signature.ParseFromString(payload_signature_blob) 276 self.payload_signature = payload_signature 277 278 self.is_init = True 279 280 def _AssertInit(self): 281 """Raises an exception if the object was not initialized.""" 282 if not self.is_init: 283 raise PayloadError('payload object not initialized') 284 285 def ResetFile(self): 286 """Resets the offset of the payload file to right past the manifest.""" 287 self.payload_file.seek(self.payload_file_offset + self.data_offset) 288 289 def IsDelta(self): 290 """Returns True iff the payload appears to be a delta.""" 291 self._AssertInit() 292 return (any(partition.HasField('old_partition_info') 293 for partition in self.manifest.partitions)) 294 295 def IsFull(self): 296 """Returns True iff the payload appears to be a full.""" 297 return not self.IsDelta() 298 299 def Check(self, pubkey_file_name=None, metadata_sig_file=None, 300 metadata_size=0, report_out_file=None, assert_type=None, 301 block_size=0, part_sizes=None, allow_unhashed=False, 302 disabled_tests=()): 303 """Checks the payload integrity. 304 305 Args: 306 pubkey_file_name: public key used for signature verification 307 metadata_sig_file: metadata signature, if verification is desired 308 metadata_size: metadata size, if verification is desired 309 report_out_file: file object to dump the report to 310 assert_type: assert that payload is either 'full' or 'delta' 311 block_size: expected filesystem / payload block size 312 part_sizes: map of partition label to (physical) size in bytes 313 allow_unhashed: allow unhashed operation blobs 314 disabled_tests: list of tests to disable 315 316 Raises: 317 PayloadError if payload verification failed. 318 """ 319 self._AssertInit() 320 321 # Create a short-lived payload checker object and run it. 322 helper = checker.PayloadChecker( 323 self, assert_type=assert_type, block_size=block_size, 324 allow_unhashed=allow_unhashed, disabled_tests=disabled_tests) 325 helper.Run(pubkey_file_name=pubkey_file_name, 326 metadata_sig_file=metadata_sig_file, 327 metadata_size=metadata_size, 328 part_sizes=part_sizes, 329 report_out_file=report_out_file) 330 331 def CheckDataHash(self): 332 for part in self.manifest.partitions: 333 for op in part.operations: 334 if op.data_length == 0: 335 continue 336 if not op.data_sha256_hash: 337 raise PayloadError( 338 f"Operation {op} in partition {part.partition_name} missing data_sha256_hash") 339 blob = self.ReadDataBlob(op.data_offset, op.data_length) 340 blob_hash = hashlib.sha256(blob) 341 if blob_hash.digest() != op.data_sha256_hash: 342 raise PayloadError( 343 f"Operation {op} in partition {part.partition_name} has unexpected hash, expected: {binascii.hexlify(op.data_sha256_hash)}, actual: {blob_hash.hexdigest()}") 344