blob: e153669e28820805bb198f2061aac0a3bb9de793 [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
Andrew Lassalle165843c2019-11-05 13:30:34 -080019from __future__ import absolute_import
Gilad Arnold25c18212015-07-14 09:55:07 -070020from __future__ import print_function
21
Andrew Lassalle165843c2019-11-05 13:30:34 -080022import io
Gilad Arnold5502b562013-03-08 13:22:31 -080023import hashlib
Alex Deymo28466772015-09-11 17:16:44 -070024import os
Gilad Arnold5502b562013-03-08 13:22:31 -080025import struct
26import subprocess
27
Amin Hassanib05a65a2017-12-18 15:15:32 -080028from update_payload import common
29from update_payload import payload
30from update_payload import update_metadata_pb2
Gilad Arnold5502b562013-03-08 13:22:31 -080031
32
33class TestError(Exception):
34 """An error during testing of update payload code."""
35
36
Gilad Arnold18f4f9f2013-04-02 16:24:41 -070037# Private/public RSA keys used for testing.
Alex Deymo28466772015-09-11 17:16:44 -070038_PRIVKEY_FILE_NAME = os.path.join(os.path.dirname(__file__),
39 'payload-test-key.pem')
40_PUBKEY_FILE_NAME = os.path.join(os.path.dirname(__file__),
41 'payload-test-key.pub')
Gilad Arnold18f4f9f2013-04-02 16:24:41 -070042
43
44def KiB(count):
45 return count << 10
46
47
48def MiB(count):
49 return count << 20
50
51
52def GiB(count):
53 return count << 30
54
55
Gilad Arnold5502b562013-03-08 13:22:31 -080056def _WriteInt(file_obj, size, is_unsigned, val):
57 """Writes a binary-encoded integer to a file.
58
59 It will do the correct conversion based on the reported size and whether or
60 not a signed number is expected. Assumes a network (big-endian) byte
61 ordering.
62
63 Args:
64 file_obj: a file object
65 size: the integer size in bytes (2, 4 or 8)
66 is_unsigned: whether it is signed or not
67 val: integer value to encode
Gilad Arnold25c18212015-07-14 09:55:07 -070068
Gilad Arnold5502b562013-03-08 13:22:31 -080069 Raises:
70 PayloadError if a write error occurred.
Gilad Arnold5502b562013-03-08 13:22:31 -080071 """
72 try:
73 file_obj.write(struct.pack(common.IntPackingFmtStr(size, is_unsigned), val))
Andrew Lassalle165843c2019-11-05 13:30:34 -080074 except IOError as e:
Gilad Arnold5502b562013-03-08 13:22:31 -080075 raise payload.PayloadError('error writing to file (%s): %s' %
76 (file_obj.name, e))
77
78
79def _SetMsgField(msg, field_name, val):
80 """Sets or clears a field in a protobuf message."""
81 if val is None:
82 msg.ClearField(field_name)
83 else:
84 setattr(msg, field_name, val)
85
86
87def SignSha256(data, privkey_file_name):
88 """Signs the data's SHA256 hash with an RSA private key.
89
90 Args:
91 data: the data whose SHA256 hash we want to sign
92 privkey_file_name: private key used for signing data
Gilad Arnold25c18212015-07-14 09:55:07 -070093
Gilad Arnold5502b562013-03-08 13:22:31 -080094 Returns:
95 The signature string, prepended with an ASN1 header.
Gilad Arnold25c18212015-07-14 09:55:07 -070096
Gilad Arnold5502b562013-03-08 13:22:31 -080097 Raises:
98 TestError if something goes wrong.
Gilad Arnold5502b562013-03-08 13:22:31 -080099 """
Gilad Arnold5502b562013-03-08 13:22:31 -0800100 data_sha256_hash = common.SIG_ASN1_HEADER + hashlib.sha256(data).digest()
101 sign_cmd = ['openssl', 'rsautl', '-sign', '-inkey', privkey_file_name]
102 try:
103 sign_process = subprocess.Popen(sign_cmd, stdin=subprocess.PIPE,
104 stdout=subprocess.PIPE)
105 sig, _ = sign_process.communicate(input=data_sha256_hash)
106 except Exception as e:
107 raise TestError('signing subprocess failed: %s' % e)
108
109 return sig
110
111
112class SignaturesGenerator(object):
113 """Generates a payload signatures data block."""
114
115 def __init__(self):
116 self.sigs = update_metadata_pb2.Signatures()
117
118 def AddSig(self, version, data):
119 """Adds a signature to the signature sequence.
120
121 Args:
122 version: signature version (None means do not assign)
123 data: signature binary data (None means do not assign)
Gilad Arnold5502b562013-03-08 13:22:31 -0800124 """
Gilad Arnold5502b562013-03-08 13:22:31 -0800125 sig = self.sigs.signatures.add()
126 if version is not None:
127 sig.version = version
128 if data is not None:
129 sig.data = data
130
131 def ToBinary(self):
132 """Returns the binary representation of the signature block."""
133 return self.sigs.SerializeToString()
134
135
136class PayloadGenerator(object):
137 """Generates an update payload allowing low-level control.
138
139 Attributes:
140 manifest: the protobuf containing the payload manifest
141 version: the payload version identifier
142 block_size: the block size pertaining to update operations
143
144 """
145
146 def __init__(self, version=1):
147 self.manifest = update_metadata_pb2.DeltaArchiveManifest()
148 self.version = version
149 self.block_size = 0
150
151 @staticmethod
152 def _WriteExtent(ex, val):
153 """Returns an Extent message."""
154 start_block, num_blocks = val
155 _SetMsgField(ex, 'start_block', start_block)
156 _SetMsgField(ex, 'num_blocks', num_blocks)
157
158 @staticmethod
159 def _AddValuesToRepeatedField(repeated_field, values, write_func):
160 """Adds values to a repeated message field."""
161 if values:
162 for val in values:
163 new_item = repeated_field.add()
164 write_func(new_item, val)
165
166 @staticmethod
167 def _AddExtents(extents_field, values):
168 """Adds extents to an extents field."""
169 PayloadGenerator._AddValuesToRepeatedField(
170 extents_field, values, PayloadGenerator._WriteExtent)
171
172 def SetBlockSize(self, block_size):
173 """Sets the payload's block size."""
174 self.block_size = block_size
175 _SetMsgField(self.manifest, 'block_size', block_size)
176
Amin Hassani55c75412019-10-07 11:20:39 -0700177 def SetPartInfo(self, part_name, is_new, part_size, part_hash):
Gilad Arnold5502b562013-03-08 13:22:31 -0800178 """Set the partition info entry.
179
180 Args:
Amin Hassani55c75412019-10-07 11:20:39 -0700181 part_name: The name of the partition.
182 is_new: Whether to set old (False) or new (True) info.
183 part_size: The partition size (in fact, filesystem size).
184 part_hash: The partition hash.
Gilad Arnold5502b562013-03-08 13:22:31 -0800185 """
Amin Hassani55c75412019-10-07 11:20:39 -0700186 partition = next((x for x in self.manifest.partitions
187 if x.partition_name == part_name), None)
188 if partition is None:
189 partition = self.manifest.partitions.add()
190 partition.partition_name = part_name
191
192 part_info = (partition.new_partition_info if is_new
193 else partition.old_partition_info)
Gilad Arnold5502b562013-03-08 13:22:31 -0800194 _SetMsgField(part_info, 'size', part_size)
195 _SetMsgField(part_info, 'hash', part_hash)
196
Amin Hassani55c75412019-10-07 11:20:39 -0700197 def AddOperation(self, part_name, op_type, data_offset=None,
Gilad Arnold5502b562013-03-08 13:22:31 -0800198 data_length=None, src_extents=None, src_length=None,
199 dst_extents=None, dst_length=None, data_sha256_hash=None):
200 """Adds an InstallOperation entry."""
Amin Hassani55c75412019-10-07 11:20:39 -0700201 partition = next((x for x in self.manifest.partitions
202 if x.partition_name == part_name), None)
203 if partition is None:
204 partition = self.manifest.partitions.add()
205 partition.partition_name = part_name
Gilad Arnold5502b562013-03-08 13:22:31 -0800206
Amin Hassani55c75412019-10-07 11:20:39 -0700207 operations = partition.operations
Gilad Arnold5502b562013-03-08 13:22:31 -0800208 op = operations.add()
209 op.type = op_type
210
211 _SetMsgField(op, 'data_offset', data_offset)
212 _SetMsgField(op, 'data_length', data_length)
213
214 self._AddExtents(op.src_extents, src_extents)
215 _SetMsgField(op, 'src_length', src_length)
216
217 self._AddExtents(op.dst_extents, dst_extents)
218 _SetMsgField(op, 'dst_length', dst_length)
219
220 _SetMsgField(op, 'data_sha256_hash', data_sha256_hash)
221
222 def SetSignatures(self, sigs_offset, sigs_size):
223 """Set the payload's signature block descriptors."""
224 _SetMsgField(self.manifest, 'signatures_offset', sigs_offset)
225 _SetMsgField(self.manifest, 'signatures_size', sigs_size)
226
Gilad Arnold0d575cd2015-07-13 17:29:21 -0700227 def SetMinorVersion(self, minor_version):
228 """Set the payload's minor version field."""
229 _SetMsgField(self.manifest, 'minor_version', minor_version)
230
Gilad Arnold5502b562013-03-08 13:22:31 -0800231 def _WriteHeaderToFile(self, file_obj, manifest_len):
232 """Writes a payload heaer to a file."""
233 # We need to access protected members in Payload for writing the header.
234 # pylint: disable=W0212
Sen Jiang912c4df2015-12-10 12:17:13 -0800235 file_obj.write(payload.Payload._PayloadHeader._MAGIC)
236 _WriteInt(file_obj, payload.Payload._PayloadHeader._VERSION_SIZE, True,
237 self.version)
238 _WriteInt(file_obj, payload.Payload._PayloadHeader._MANIFEST_LEN_SIZE, True,
239 manifest_len)
Gilad Arnold5502b562013-03-08 13:22:31 -0800240
241 def WriteToFile(self, file_obj, manifest_len=-1, data_blobs=None,
242 sigs_data=None, padding=None):
243 """Writes the payload content to a file.
244
245 Args:
246 file_obj: a file object open for writing
247 manifest_len: manifest len to dump (otherwise computed automatically)
248 data_blobs: a list of data blobs to be concatenated to the payload
249 sigs_data: a binary Signatures message to be concatenated to the payload
250 padding: stuff to dump past the normal data blobs provided (optional)
Gilad Arnold5502b562013-03-08 13:22:31 -0800251 """
252 manifest = self.manifest.SerializeToString()
253 if manifest_len < 0:
254 manifest_len = len(manifest)
255 self._WriteHeaderToFile(file_obj, manifest_len)
256 file_obj.write(manifest)
257 if data_blobs:
258 for data_blob in data_blobs:
259 file_obj.write(data_blob)
260 if sigs_data:
261 file_obj.write(sigs_data)
262 if padding:
263 file_obj.write(padding)
264
265
266class EnhancedPayloadGenerator(PayloadGenerator):
267 """Payload generator with automatic handling of data blobs.
268
269 Attributes:
270 data_blobs: a list of blobs, in the order they were added
271 curr_offset: the currently consumed offset of blobs added to the payload
Gilad Arnold5502b562013-03-08 13:22:31 -0800272 """
273
274 def __init__(self):
275 super(EnhancedPayloadGenerator, self).__init__()
276 self.data_blobs = []
277 self.curr_offset = 0
278
279 def AddData(self, data_blob):
280 """Adds a (possibly orphan) data blob."""
281 data_length = len(data_blob)
282 data_offset = self.curr_offset
283 self.curr_offset += data_length
284 self.data_blobs.append(data_blob)
285 return data_length, data_offset
286
Amin Hassani55c75412019-10-07 11:20:39 -0700287 def AddOperationWithData(self, part_name, op_type, src_extents=None,
Gilad Arnold5502b562013-03-08 13:22:31 -0800288 src_length=None, dst_extents=None, dst_length=None,
289 data_blob=None, do_hash_data_blob=True):
290 """Adds an install operation and associated data blob.
291
292 This takes care of obtaining a hash of the data blob (if so instructed)
293 and appending it to the internally maintained list of blobs, including the
294 necessary offset/length accounting.
295
296 Args:
Amin Hassani55c75412019-10-07 11:20:39 -0700297 part_name: The name of the partition (e.g. kernel or root).
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700298 op_type: one of REPLACE, REPLACE_BZ, REPLACE_XZ.
Gilad Arnold5502b562013-03-08 13:22:31 -0800299 src_extents: list of (start, length) pairs indicating src block ranges
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700300 src_length: size of the src data in bytes (needed for diff operations)
Gilad Arnold5502b562013-03-08 13:22:31 -0800301 dst_extents: list of (start, length) pairs indicating dst block ranges
Amin Hassani0f59a9a2019-09-27 10:24:31 -0700302 dst_length: size of the dst data in bytes (needed for diff operations)
Gilad Arnold5502b562013-03-08 13:22:31 -0800303 data_blob: a data blob associated with this operation
304 do_hash_data_blob: whether or not to compute and add a data blob hash
Gilad Arnold5502b562013-03-08 13:22:31 -0800305 """
306 data_offset = data_length = data_sha256_hash = None
307 if data_blob is not None:
308 if do_hash_data_blob:
Gilad Arnold5502b562013-03-08 13:22:31 -0800309 data_sha256_hash = hashlib.sha256(data_blob).digest()
310 data_length, data_offset = self.AddData(data_blob)
311
Amin Hassani55c75412019-10-07 11:20:39 -0700312 self.AddOperation(part_name, op_type, data_offset=data_offset,
Gilad Arnold5502b562013-03-08 13:22:31 -0800313 data_length=data_length, src_extents=src_extents,
314 src_length=src_length, dst_extents=dst_extents,
315 dst_length=dst_length, data_sha256_hash=data_sha256_hash)
316
317 def WriteToFileWithData(self, file_obj, sigs_data=None,
Amin Hassani55c75412019-10-07 11:20:39 -0700318 privkey_file_name=None, padding=None):
Gilad Arnold5502b562013-03-08 13:22:31 -0800319 """Writes the payload content to a file, optionally signing the content.
320
321 Args:
322 file_obj: a file object open for writing
323 sigs_data: signatures blob to be appended to the payload (optional;
324 payload signature fields assumed to be preset by the caller)
325 privkey_file_name: key used for signing the payload (optional; used only
326 if explicit signatures blob not provided)
Gilad Arnold5502b562013-03-08 13:22:31 -0800327 padding: stuff to dump past the normal data blobs provided (optional)
Gilad Arnold25c18212015-07-14 09:55:07 -0700328
Gilad Arnold5502b562013-03-08 13:22:31 -0800329 Raises:
330 TestError: if arguments are inconsistent or something goes wrong.
Gilad Arnold5502b562013-03-08 13:22:31 -0800331 """
332 sigs_len = len(sigs_data) if sigs_data else 0
333
334 # Do we need to generate a genuine signatures blob?
335 do_generate_sigs_data = sigs_data is None and privkey_file_name
336
337 if do_generate_sigs_data:
338 # First, sign some arbitrary data to obtain the size of a signature blob.
Andrew Lassalle165843c2019-11-05 13:30:34 -0800339 fake_sig = SignSha256(b'fake-payload-data', privkey_file_name)
Gilad Arnold5502b562013-03-08 13:22:31 -0800340 fake_sigs_gen = SignaturesGenerator()
341 fake_sigs_gen.AddSig(1, fake_sig)
342 sigs_len = len(fake_sigs_gen.ToBinary())
343
344 # Update the payload with proper signature attributes.
345 self.SetSignatures(self.curr_offset, sigs_len)
346
Gilad Arnold5502b562013-03-08 13:22:31 -0800347 if do_generate_sigs_data:
348 # Once all payload fields are updated, dump and sign it.
Andrew Lassalle165843c2019-11-05 13:30:34 -0800349 temp_payload_file = io.BytesIO()
Gilad Arnold5502b562013-03-08 13:22:31 -0800350 self.WriteToFile(temp_payload_file, data_blobs=self.data_blobs)
351 sig = SignSha256(temp_payload_file.getvalue(), privkey_file_name)
352 sigs_gen = SignaturesGenerator()
353 sigs_gen.AddSig(1, sig)
354 sigs_data = sigs_gen.ToBinary()
355 assert len(sigs_data) == sigs_len, 'signature blob lengths mismatch'
356
357 # Dump the whole thing, complete with data and signature blob, to a file.
358 self.WriteToFile(file_obj, data_blobs=self.data_blobs, sigs_data=sigs_data,
359 padding=padding)