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 metadata_hash(self): 156 return self.manifest_hasher.digest() 157 158 @property 159 def payload_hash(self): 160 hasher = hashlib.sha256() 161 self.payload_file.seek(0) 162 hasher.update(self.payload_file.read(self.metadata_size)) 163 self.payload_file.seek(self.header.metadata_signature_len, io.SEEK_CUR) 164 hasher.update(self.payload_file.read(self.total_data_length)) 165 return hasher.digest() 166 167 @property 168 def is_incremental(self): 169 return any([part.HasField("old_partition_info") for part in self.manifest.partitions]) 170 171 @property 172 def is_partial(self): 173 return self.manifest.partial_update 174 175 @property 176 def total_data_length(self): 177 """Return the total data length of this payload, excluding payload 178 signature at the very end. 179 """ 180 # Operations are sorted in ascending data_offset order, so iterating 181 # backwards and find the first one with non zero data_offset will tell 182 # us total data length 183 for partition in reversed(self.manifest.partitions): 184 for op in reversed(partition.operations): 185 if op.data_length > 0: 186 return op.data_offset + op.data_length 187 return 0 188 189 def _ReadHeader(self): 190 """Reads and returns the payload header. 191 192 Returns: 193 A payload header object. 194 195 Raises: 196 PayloadError if a read error occurred. 197 """ 198 header = self._PayloadHeader() 199 header.ReadFromPayload(self.payload_file, self.manifest_hasher) 200 return header 201 202 def _ReadManifest(self): 203 """Reads and returns the payload manifest. 204 205 Returns: 206 A string containing the payload manifest in binary form. 207 208 Raises: 209 PayloadError if a read error occurred. 210 """ 211 if not self.header: 212 raise PayloadError('payload header not present') 213 214 return common.Read(self.payload_file, self.header.manifest_len, 215 hasher=self.manifest_hasher) 216 217 def _ReadMetadataSignature(self): 218 """Reads and returns the metadata signatures. 219 220 Returns: 221 A string containing the metadata signatures protobuf in binary form or 222 an empty string if no metadata signature found in the payload. 223 224 Raises: 225 PayloadError if a read error occurred. 226 """ 227 if not self.header: 228 raise PayloadError('payload header not present') 229 230 return common.Read( 231 self.payload_file, self.header.metadata_signature_len, 232 offset=self.payload_file_offset + self.header.size + 233 self.header.manifest_len) 234 235 def ReadDataBlob(self, offset, length): 236 """Reads and returns a single data blob from the update payload. 237 238 Args: 239 offset: offset to the beginning of the blob from the end of the manifest 240 length: the blob's length 241 242 Returns: 243 A string containing the raw blob data. 244 245 Raises: 246 PayloadError if a read error occurred. 247 """ 248 return common.Read(self.payload_file, length, 249 offset=self.payload_file_offset + self.data_offset + 250 offset) 251 252 def Init(self): 253 """Initializes the payload object. 254 255 This is a prerequisite for any other public API call. 256 257 Raises: 258 PayloadError if object already initialized or fails to initialize 259 correctly. 260 """ 261 if self.is_init: 262 return 263 264 self.manifest_hasher = hashlib.sha256() 265 266 # Read the file header. 267 self.payload_file.seek(self.payload_file_offset) 268 self.header = self._ReadHeader() 269 270 # Read the manifest. 271 manifest_raw = self._ReadManifest() 272 self.manifest = update_metadata_pb2.DeltaArchiveManifest() 273 self.manifest.ParseFromString(manifest_raw) 274 275 # Read the metadata signature (if any). 276 metadata_signature_raw = self._ReadMetadataSignature() 277 if metadata_signature_raw: 278 self.metadata_signature = update_metadata_pb2.Signatures() 279 self.metadata_signature.ParseFromString(metadata_signature_raw) 280 281 self.metadata_size = self.header.size + self.header.manifest_len 282 self.data_offset = self.metadata_size + self.header.metadata_signature_len 283 284 if self.manifest.signatures_offset and self.manifest.signatures_size and self.manifest.signatures_offset + self.manifest.signatures_size <= self.payload_file_size: 285 payload_signature_blob = self.ReadDataBlob( 286 self.manifest.signatures_offset, self.manifest.signatures_size) 287 payload_signature = update_metadata_pb2.Signatures() 288 payload_signature.ParseFromString(payload_signature_blob) 289 self.payload_signature = payload_signature 290 291 self.is_init = True 292 293 def _AssertInit(self): 294 """Raises an exception if the object was not initialized.""" 295 if not self.is_init: 296 raise PayloadError('payload object not initialized') 297 298 def ResetFile(self): 299 """Resets the offset of the payload file to right past the manifest.""" 300 self.payload_file.seek(self.payload_file_offset + self.data_offset) 301 302 def IsDelta(self): 303 """Returns True iff the payload appears to be a delta.""" 304 self._AssertInit() 305 return (any(partition.HasField('old_partition_info') 306 for partition in self.manifest.partitions)) 307 308 def IsFull(self): 309 """Returns True iff the payload appears to be a full.""" 310 return not self.IsDelta() 311 312 def Check(self, pubkey_file_name=None, metadata_sig_file=None, 313 metadata_size=0, report_out_file=None, assert_type=None, 314 block_size=0, part_sizes=None, allow_unhashed=False, 315 disabled_tests=()): 316 """Checks the payload integrity. 317 318 Args: 319 pubkey_file_name: public key used for signature verification 320 metadata_sig_file: metadata signature, if verification is desired 321 metadata_size: metadata size, if verification is desired 322 report_out_file: file object to dump the report to 323 assert_type: assert that payload is either 'full' or 'delta' 324 block_size: expected filesystem / payload block size 325 part_sizes: map of partition label to (physical) size in bytes 326 allow_unhashed: allow unhashed operation blobs 327 disabled_tests: list of tests to disable 328 329 Raises: 330 PayloadError if payload verification failed. 331 """ 332 self._AssertInit() 333 334 # Create a short-lived payload checker object and run it. 335 helper = checker.PayloadChecker( 336 self, assert_type=assert_type, block_size=block_size, 337 allow_unhashed=allow_unhashed, disabled_tests=disabled_tests) 338 helper.Run(pubkey_file_name=pubkey_file_name, 339 metadata_sig_file=metadata_sig_file, 340 metadata_size=metadata_size, 341 part_sizes=part_sizes, 342 report_out_file=report_out_file) 343 344 def CheckDataHash(self): 345 for part in self.manifest.partitions: 346 for op in part.operations: 347 if op.data_length == 0: 348 continue 349 if not op.data_sha256_hash: 350 raise PayloadError( 351 f"Operation {op} in partition {part.partition_name} missing data_sha256_hash") 352 blob = self.ReadDataBlob(op.data_offset, op.data_length) 353 blob_hash = hashlib.sha256(blob) 354 if blob_hash.digest() != op.data_sha256_hash: 355 raise PayloadError( 356 f"Operation {op} in partition {part.partition_name} has unexpected hash, expected: {binascii.hexlify(op.data_sha256_hash)}, actual: {blob_hash.hexdigest()}") 357