blob: 4f5fed0363f382ffab964888448141787c9378e6 [file] [log] [blame]
Amin Hassanif94b6432018-01-26 17:39:47 -08001#
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#
Gilad Arnold5502b562013-03-08 13:22:31 -080016
17"""Utilities for unit testing."""
18
Gilad Arnold25c18212015-07-14 09:55:07 -070019from __future__ import print_function
20
Gilad Arnold5502b562013-03-08 13:22:31 -080021import cStringIO
22import hashlib
Alex Deymo28466772015-09-11 17:16:44 -070023import os
Gilad Arnold5502b562013-03-08 13:22:31 -080024import struct
25import subprocess
26
Amin Hassanib05a65a2017-12-18 15:15:32 -080027from update_payload import common
28from update_payload import payload
29from update_payload import update_metadata_pb2
Gilad Arnold5502b562013-03-08 13:22:31 -080030
31
32class TestError(Exception):
33 """An error during testing of update payload code."""
34
35
Gilad Arnold18f4f9f2013-04-02 16:24:41 -070036# Private/public RSA keys used for testing.
Alex Deymo28466772015-09-11 17:16:44 -070037_PRIVKEY_FILE_NAME = os.path.join(os.path.dirname(__file__),
38 'payload-test-key.pem')
39_PUBKEY_FILE_NAME = os.path.join(os.path.dirname(__file__),
40 'payload-test-key.pub')
Gilad Arnold18f4f9f2013-04-02 16:24:41 -070041
42
43def KiB(count):
44 return count << 10
45
46
47def MiB(count):
48 return count << 20
49
50
51def GiB(count):
52 return count << 30
53
54
Gilad Arnold5502b562013-03-08 13:22:31 -080055def _WriteInt(file_obj, size, is_unsigned, val):
56 """Writes a binary-encoded integer to a file.
57
58 It will do the correct conversion based on the reported size and whether or
59 not a signed number is expected. Assumes a network (big-endian) byte
60 ordering.
61
62 Args:
63 file_obj: a file object
64 size: the integer size in bytes (2, 4 or 8)
65 is_unsigned: whether it is signed or not
66 val: integer value to encode
Gilad Arnold25c18212015-07-14 09:55:07 -070067
Gilad Arnold5502b562013-03-08 13:22:31 -080068 Raises:
69 PayloadError if a write error occurred.
Gilad Arnold5502b562013-03-08 13:22:31 -080070 """
71 try:
72 file_obj.write(struct.pack(common.IntPackingFmtStr(size, is_unsigned), val))
73 except IOError, e:
74 raise payload.PayloadError('error writing to file (%s): %s' %
75 (file_obj.name, e))
76
77
78def _SetMsgField(msg, field_name, val):
79 """Sets or clears a field in a protobuf message."""
80 if val is None:
81 msg.ClearField(field_name)
82 else:
83 setattr(msg, field_name, val)
84
85
86def SignSha256(data, privkey_file_name):
87 """Signs the data's SHA256 hash with an RSA private key.
88
89 Args:
90 data: the data whose SHA256 hash we want to sign
91 privkey_file_name: private key used for signing data
Gilad Arnold25c18212015-07-14 09:55:07 -070092
Gilad Arnold5502b562013-03-08 13:22:31 -080093 Returns:
94 The signature string, prepended with an ASN1 header.
Gilad Arnold25c18212015-07-14 09:55:07 -070095
Gilad Arnold5502b562013-03-08 13:22:31 -080096 Raises:
97 TestError if something goes wrong.
Gilad Arnold5502b562013-03-08 13:22:31 -080098 """
Gilad Arnold5502b562013-03-08 13:22:31 -080099 data_sha256_hash = common.SIG_ASN1_HEADER + hashlib.sha256(data).digest()
100 sign_cmd = ['openssl', 'rsautl', '-sign', '-inkey', privkey_file_name]
101 try:
102 sign_process = subprocess.Popen(sign_cmd, stdin=subprocess.PIPE,
103 stdout=subprocess.PIPE)
104 sig, _ = sign_process.communicate(input=data_sha256_hash)
105 except Exception as e:
106 raise TestError('signing subprocess failed: %s' % e)
107
108 return sig
109
110
111class SignaturesGenerator(object):
112 """Generates a payload signatures data block."""
113
114 def __init__(self):
115 self.sigs = update_metadata_pb2.Signatures()
116
117 def AddSig(self, version, data):
118 """Adds a signature to the signature sequence.
119
120 Args:
121 version: signature version (None means do not assign)
122 data: signature binary data (None means do not assign)
Gilad Arnold5502b562013-03-08 13:22:31 -0800123 """
Gilad Arnold5502b562013-03-08 13:22:31 -0800124 sig = self.sigs.signatures.add()
125 if version is not None:
126 sig.version = version
127 if data is not None:
128 sig.data = data
129
130 def ToBinary(self):
131 """Returns the binary representation of the signature block."""
132 return self.sigs.SerializeToString()
133
134
135class PayloadGenerator(object):
136 """Generates an update payload allowing low-level control.
137
138 Attributes:
139 manifest: the protobuf containing the payload manifest
140 version: the payload version identifier
141 block_size: the block size pertaining to update operations
142
143 """
144
145 def __init__(self, version=1):
146 self.manifest = update_metadata_pb2.DeltaArchiveManifest()
147 self.version = version
148 self.block_size = 0
149
150 @staticmethod
151 def _WriteExtent(ex, val):
152 """Returns an Extent message."""
153 start_block, num_blocks = val
154 _SetMsgField(ex, 'start_block', start_block)
155 _SetMsgField(ex, 'num_blocks', num_blocks)
156
157 @staticmethod
158 def _AddValuesToRepeatedField(repeated_field, values, write_func):
159 """Adds values to a repeated message field."""
160 if values:
161 for val in values:
162 new_item = repeated_field.add()
163 write_func(new_item, val)
164
165 @staticmethod
166 def _AddExtents(extents_field, values):
167 """Adds extents to an extents field."""
168 PayloadGenerator._AddValuesToRepeatedField(
169 extents_field, values, PayloadGenerator._WriteExtent)
170
171 def SetBlockSize(self, block_size):
172 """Sets the payload's block size."""
173 self.block_size = block_size
174 _SetMsgField(self.manifest, 'block_size', block_size)
175
Amin Hassani55c75412019-10-07 11:20:39 -0700176 def SetPartInfo(self, part_name, is_new, part_size, part_hash):
Gilad Arnold5502b562013-03-08 13:22:31 -0800177 """Set the partition info entry.
178
179 Args:
Amin Hassani55c75412019-10-07 11:20:39 -0700180 part_name: The name of the partition.
181 is_new: Whether to set old (False) or new (True) info.
182 part_size: The partition size (in fact, filesystem size).
183 part_hash: The partition hash.
Gilad Arnold5502b562013-03-08 13:22:31 -0800184 """
Amin Hassani55c75412019-10-07 11:20:39 -0700185 partition = next((x for x in self.manifest.partitions
186 if x.partition_name == part_name), None)
187 if partition is None:
188 partition = self.manifest.partitions.add()
189 partition.partition_name = part_name
190
191 part_info = (partition.new_partition_info if is_new
192 else partition.old_partition_info)
Gilad Arnold5502b562013-03-08 13:22:31 -0800193 _SetMsgField(part_info, 'size', part_size)
194 _SetMsgField(part_info, 'hash', part_hash)
195
Amin Hassani55c75412019-10-07 11:20:39 -0700196 def AddOperation(self, part_name, op_type, data_offset=None,
Gilad Arnold5502b562013-03-08 13:22:31 -0800197 data_length=None, src_extents=None, src_length=None,
198 dst_extents=None, dst_length=None, data_sha256_hash=None):
199 """Adds an InstallOperation entry."""
Amin Hassani55c75412019-10-07 11:20:39 -0700200 partition = next((x for x in self.manifest.partitions
201 if x.partition_name == part_name), None)
202 if partition is None:
203 partition = self.manifest.partitions.add()
204 partition.partition_name = part_name
Gilad Arnold5502b562013-03-08 13:22:31 -0800205
Amin Hassani55c75412019-10-07 11:20:39 -0700206 operations = partition.operations
Gilad Arnold5502b562013-03-08 13:22:31 -0800207 op = operations.add()
208 op.type = op_type
209
210 _SetMsgField(op, 'data_offset', data_offset)
211 _SetMsgField(op, 'data_length', data_length)
212
213 self._AddExtents(op.src_extents, src_extents)
214 _SetMsgField(op, 'src_length', src_length)
215
216 self._AddExtents(op.dst_extents, dst_extents)
217 _SetMsgField(op, 'dst_length', dst_length)
218
219 _SetMsgField(op, 'data_sha256_hash', data_sha256_hash)
220
221 def SetSignatures(self, sigs_offset, sigs_size):
222 """Set the payload's signature block descriptors."""
223 _SetMsgField(self.manifest, 'signatures_offset', sigs_offset)
224 _SetMsgField(self.manifest, 'signatures_size', sigs_size)
225
Gilad Arnold0d575cd2015-07-13 17:29:21 -0700226 def SetMinorVersion(self, minor_version):
227 """Set the payload's minor version field."""
228 _SetMsgField(self.manifest, 'minor_version', minor_version)
229
Gilad Arnold5502b562013-03-08 13:22:31 -0800230 def _WriteHeaderToFile(self, file_obj, manifest_len):
231 """Writes a payload heaer to a file."""
232 # We need to access protected members in Payload for writing the header.
233 # pylint: disable=W0212
Sen Jiang912c4df2015-12-10 12:17:13 -0800234 file_obj.write(payload.Payload._PayloadHeader._MAGIC)
235 _WriteInt(file_obj, payload.Payload._PayloadHeader._VERSION_SIZE, True,
236 self.version)
237 _WriteInt(file_obj, payload.Payload._PayloadHeader._MANIFEST_LEN_SIZE, True,
238 manifest_len)
Gilad Arnold5502b562013-03-08 13:22:31 -0800239
240 def WriteToFile(self, file_obj, manifest_len=-1, data_blobs=None,
241 sigs_data=None, padding=None):
242 """Writes the payload content to a file.
243
244 Args:
245 file_obj: a file object open for writing
246 manifest_len: manifest len to dump (otherwise computed automatically)
247 data_blobs: a list of data blobs to be concatenated to the payload
248 sigs_data: a binary Signatures message to be concatenated to the payload
249 padding: stuff to dump past the normal data blobs provided (optional)
Gilad Arnold5502b562013-03-08 13:22:31 -0800250 """
251 manifest = self.manifest.SerializeToString()
252 if manifest_len < 0:
253 manifest_len = len(manifest)
254 self._WriteHeaderToFile(file_obj, manifest_len)
255 file_obj.write(manifest)
256 if data_blobs:
257 for data_blob in data_blobs:
258 file_obj.write(data_blob)
259 if sigs_data:
260 file_obj.write(sigs_data)
261 if padding:
262 file_obj.write(padding)
263
264
265class EnhancedPayloadGenerator(PayloadGenerator):
266 """Payload generator with automatic handling of data blobs.
267
268 Attributes:
269 data_blobs: a list of blobs, in the order they were added
270 curr_offset: the currently consumed offset of blobs added to the payload
Gilad Arnold5502b562013-03-08 13:22:31 -0800271 """
272
273 def __init__(self):
274 super(EnhancedPayloadGenerator, self).__init__()
275 self.data_blobs = []
276 self.curr_offset = 0
277
278 def AddData(self, data_blob):
279 """Adds a (possibly orphan) data blob."""
280 data_length = len(data_blob)
281 data_offset = self.curr_offset
282 self.curr_offset += data_length
283 self.data_blobs.append(data_blob)
284 return data_length, data_offset
285
Amin Hassani55c75412019-10-07 11:20:39 -0700286 def AddOperationWithData(self, part_name, op_type, src_extents=None,
Gilad Arnold5502b562013-03-08 13:22:31 -0800287 src_length=None, dst_extents=None, dst_length=None,
288 data_blob=None, do_hash_data_blob=True):
289 """Adds an install operation and associated data blob.
290
291 This takes care of obtaining a hash of the data blob (if so instructed)
292 and appending it to the internally maintained list of blobs, including the
293 necessary offset/length accounting.
294
295 Args:
Amin Hassani55c75412019-10-07 11:20:39 -0700296 part_name: The name of the partition (e.g. kernel or root).
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700297 op_type: one of REPLACE, REPLACE_BZ, REPLACE_XZ.
Gilad Arnold5502b562013-03-08 13:22:31 -0800298 src_extents: list of (start, length) pairs indicating src block ranges
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700299 src_length: size of the src data in bytes (needed for diff operations)
Gilad Arnold5502b562013-03-08 13:22:31 -0800300 dst_extents: list of (start, length) pairs indicating dst block ranges
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700301 dst_length: size of the dst data in bytes (needed for diff operations)
Gilad Arnold5502b562013-03-08 13:22:31 -0800302 data_blob: a data blob associated with this operation
303 do_hash_data_blob: whether or not to compute and add a data blob hash
Gilad Arnold5502b562013-03-08 13:22:31 -0800304 """
305 data_offset = data_length = data_sha256_hash = None
306 if data_blob is not None:
307 if do_hash_data_blob:
Gilad Arnold5502b562013-03-08 13:22:31 -0800308 data_sha256_hash = hashlib.sha256(data_blob).digest()
309 data_length, data_offset = self.AddData(data_blob)
310
Amin Hassani55c75412019-10-07 11:20:39 -0700311 self.AddOperation(part_name, op_type, data_offset=data_offset,
Gilad Arnold5502b562013-03-08 13:22:31 -0800312 data_length=data_length, src_extents=src_extents,
313 src_length=src_length, dst_extents=dst_extents,
314 dst_length=dst_length, data_sha256_hash=data_sha256_hash)
315
316 def WriteToFileWithData(self, file_obj, sigs_data=None,
Amin Hassani55c75412019-10-07 11:20:39 -0700317 privkey_file_name=None, padding=None):
Gilad Arnold5502b562013-03-08 13:22:31 -0800318 """Writes the payload content to a file, optionally signing the content.
319
320 Args:
321 file_obj: a file object open for writing
322 sigs_data: signatures blob to be appended to the payload (optional;
323 payload signature fields assumed to be preset by the caller)
324 privkey_file_name: key used for signing the payload (optional; used only
325 if explicit signatures blob not provided)
Gilad Arnold5502b562013-03-08 13:22:31 -0800326 padding: stuff to dump past the normal data blobs provided (optional)
Gilad Arnold25c18212015-07-14 09:55:07 -0700327
Gilad Arnold5502b562013-03-08 13:22:31 -0800328 Raises:
329 TestError: if arguments are inconsistent or something goes wrong.
Gilad Arnold5502b562013-03-08 13:22:31 -0800330 """
331 sigs_len = len(sigs_data) if sigs_data else 0
332
333 # Do we need to generate a genuine signatures blob?
334 do_generate_sigs_data = sigs_data is None and privkey_file_name
335
336 if do_generate_sigs_data:
337 # First, sign some arbitrary data to obtain the size of a signature blob.
338 fake_sig = SignSha256('fake-payload-data', privkey_file_name)
339 fake_sigs_gen = SignaturesGenerator()
340 fake_sigs_gen.AddSig(1, fake_sig)
341 sigs_len = len(fake_sigs_gen.ToBinary())
342
343 # Update the payload with proper signature attributes.
344 self.SetSignatures(self.curr_offset, sigs_len)
345
Gilad Arnold5502b562013-03-08 13:22:31 -0800346 if do_generate_sigs_data:
347 # Once all payload fields are updated, dump and sign it.
348 temp_payload_file = cStringIO.StringIO()
349 self.WriteToFile(temp_payload_file, data_blobs=self.data_blobs)
350 sig = SignSha256(temp_payload_file.getvalue(), privkey_file_name)
351 sigs_gen = SignaturesGenerator()
352 sigs_gen.AddSig(1, sig)
353 sigs_data = sigs_gen.ToBinary()
354 assert len(sigs_data) == sigs_len, 'signature blob lengths mismatch'
355
356 # Dump the whole thing, complete with data and signature blob, to a file.
357 self.WriteToFile(file_obj, data_blobs=self.data_blobs, sigs_data=sigs_data,
358 padding=padding)