blob: 4e7881d32e62f0f22fbe2349e94b0811eb3854fb [file] [log] [blame]
Gilad Arnold5502b562013-03-08 13:22:31 -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"""Utilities for unit testing."""
6
Gilad Arnold25c18212015-07-14 09:55:07 -07007from __future__ import print_function
8
Gilad Arnold5502b562013-03-08 13:22:31 -08009import cStringIO
10import hashlib
11import struct
12import subprocess
13
14import common
15import payload
16import update_metadata_pb2
17
18
19class TestError(Exception):
20 """An error during testing of update payload code."""
21
22
Gilad Arnold18f4f9f2013-04-02 16:24:41 -070023# Private/public RSA keys used for testing.
24_PRIVKEY_FILE_NAME = 'payload-test-key.pem'
25_PUBKEY_FILE_NAME = 'payload-test-key.pub'
26
27
28def KiB(count):
29 return count << 10
30
31
32def MiB(count):
33 return count << 20
34
35
36def GiB(count):
37 return count << 30
38
39
Gilad Arnold5502b562013-03-08 13:22:31 -080040def _WriteInt(file_obj, size, is_unsigned, val):
41 """Writes a binary-encoded integer to a file.
42
43 It will do the correct conversion based on the reported size and whether or
44 not a signed number is expected. Assumes a network (big-endian) byte
45 ordering.
46
47 Args:
48 file_obj: a file object
49 size: the integer size in bytes (2, 4 or 8)
50 is_unsigned: whether it is signed or not
51 val: integer value to encode
Gilad Arnold25c18212015-07-14 09:55:07 -070052
Gilad Arnold5502b562013-03-08 13:22:31 -080053 Raises:
54 PayloadError if a write error occurred.
Gilad Arnold5502b562013-03-08 13:22:31 -080055 """
56 try:
57 file_obj.write(struct.pack(common.IntPackingFmtStr(size, is_unsigned), val))
58 except IOError, e:
59 raise payload.PayloadError('error writing to file (%s): %s' %
60 (file_obj.name, e))
61
62
63def _SetMsgField(msg, field_name, val):
64 """Sets or clears a field in a protobuf message."""
65 if val is None:
66 msg.ClearField(field_name)
67 else:
68 setattr(msg, field_name, val)
69
70
71def SignSha256(data, privkey_file_name):
72 """Signs the data's SHA256 hash with an RSA private key.
73
74 Args:
75 data: the data whose SHA256 hash we want to sign
76 privkey_file_name: private key used for signing data
Gilad Arnold25c18212015-07-14 09:55:07 -070077
Gilad Arnold5502b562013-03-08 13:22:31 -080078 Returns:
79 The signature string, prepended with an ASN1 header.
Gilad Arnold25c18212015-07-14 09:55:07 -070080
Gilad Arnold5502b562013-03-08 13:22:31 -080081 Raises:
82 TestError if something goes wrong.
Gilad Arnold5502b562013-03-08 13:22:31 -080083 """
84 # pylint: disable=E1101
85 data_sha256_hash = common.SIG_ASN1_HEADER + hashlib.sha256(data).digest()
86 sign_cmd = ['openssl', 'rsautl', '-sign', '-inkey', privkey_file_name]
87 try:
88 sign_process = subprocess.Popen(sign_cmd, stdin=subprocess.PIPE,
89 stdout=subprocess.PIPE)
90 sig, _ = sign_process.communicate(input=data_sha256_hash)
91 except Exception as e:
92 raise TestError('signing subprocess failed: %s' % e)
93
94 return sig
95
96
97class SignaturesGenerator(object):
98 """Generates a payload signatures data block."""
99
100 def __init__(self):
101 self.sigs = update_metadata_pb2.Signatures()
102
103 def AddSig(self, version, data):
104 """Adds a signature to the signature sequence.
105
106 Args:
107 version: signature version (None means do not assign)
108 data: signature binary data (None means do not assign)
Gilad Arnold5502b562013-03-08 13:22:31 -0800109 """
110 # Pylint fails to identify a member of the Signatures message.
111 # pylint: disable=E1101
112 sig = self.sigs.signatures.add()
113 if version is not None:
114 sig.version = version
115 if data is not None:
116 sig.data = data
117
118 def ToBinary(self):
119 """Returns the binary representation of the signature block."""
120 return self.sigs.SerializeToString()
121
122
123class PayloadGenerator(object):
124 """Generates an update payload allowing low-level control.
125
126 Attributes:
127 manifest: the protobuf containing the payload manifest
128 version: the payload version identifier
129 block_size: the block size pertaining to update operations
130
131 """
132
133 def __init__(self, version=1):
134 self.manifest = update_metadata_pb2.DeltaArchiveManifest()
135 self.version = version
136 self.block_size = 0
137
138 @staticmethod
139 def _WriteExtent(ex, val):
140 """Returns an Extent message."""
141 start_block, num_blocks = val
142 _SetMsgField(ex, 'start_block', start_block)
143 _SetMsgField(ex, 'num_blocks', num_blocks)
144
145 @staticmethod
146 def _AddValuesToRepeatedField(repeated_field, values, write_func):
147 """Adds values to a repeated message field."""
148 if values:
149 for val in values:
150 new_item = repeated_field.add()
151 write_func(new_item, val)
152
153 @staticmethod
154 def _AddExtents(extents_field, values):
155 """Adds extents to an extents field."""
156 PayloadGenerator._AddValuesToRepeatedField(
157 extents_field, values, PayloadGenerator._WriteExtent)
158
159 def SetBlockSize(self, block_size):
160 """Sets the payload's block size."""
161 self.block_size = block_size
162 _SetMsgField(self.manifest, 'block_size', block_size)
163
164 def SetPartInfo(self, is_kernel, is_new, part_size, part_hash):
165 """Set the partition info entry.
166
167 Args:
168 is_kernel: whether this is kernel partition info
169 is_new: whether to set old (False) or new (True) info
170 part_size: the partition size (in fact, filesystem size)
171 part_hash: the partition hash
Gilad Arnold5502b562013-03-08 13:22:31 -0800172 """
173 if is_kernel:
174 # pylint: disable=E1101
175 part_info = (self.manifest.new_kernel_info if is_new
176 else self.manifest.old_kernel_info)
177 else:
178 # pylint: disable=E1101
179 part_info = (self.manifest.new_rootfs_info if is_new
180 else self.manifest.old_rootfs_info)
181 _SetMsgField(part_info, 'size', part_size)
182 _SetMsgField(part_info, 'hash', part_hash)
183
184 def AddOperation(self, is_kernel, op_type, data_offset=None,
185 data_length=None, src_extents=None, src_length=None,
186 dst_extents=None, dst_length=None, data_sha256_hash=None):
187 """Adds an InstallOperation entry."""
188 # pylint: disable=E1101
189 operations = (self.manifest.kernel_install_operations if is_kernel
190 else self.manifest.install_operations)
191
192 op = operations.add()
193 op.type = op_type
194
195 _SetMsgField(op, 'data_offset', data_offset)
196 _SetMsgField(op, 'data_length', data_length)
197
198 self._AddExtents(op.src_extents, src_extents)
199 _SetMsgField(op, 'src_length', src_length)
200
201 self._AddExtents(op.dst_extents, dst_extents)
202 _SetMsgField(op, 'dst_length', dst_length)
203
204 _SetMsgField(op, 'data_sha256_hash', data_sha256_hash)
205
206 def SetSignatures(self, sigs_offset, sigs_size):
207 """Set the payload's signature block descriptors."""
208 _SetMsgField(self.manifest, 'signatures_offset', sigs_offset)
209 _SetMsgField(self.manifest, 'signatures_size', sigs_size)
210
Gilad Arnold0d575cd2015-07-13 17:29:21 -0700211 def SetMinorVersion(self, minor_version):
212 """Set the payload's minor version field."""
213 _SetMsgField(self.manifest, 'minor_version', minor_version)
214
Gilad Arnold5502b562013-03-08 13:22:31 -0800215 def _WriteHeaderToFile(self, file_obj, manifest_len):
216 """Writes a payload heaer to a file."""
217 # We need to access protected members in Payload for writing the header.
218 # pylint: disable=W0212
219 file_obj.write(payload.Payload._MAGIC)
220 _WriteInt(file_obj, payload.Payload._VERSION_SIZE, True, self.version)
221 _WriteInt(file_obj, payload.Payload._MANIFEST_LEN_SIZE, True, manifest_len)
222
223 def WriteToFile(self, file_obj, manifest_len=-1, data_blobs=None,
224 sigs_data=None, padding=None):
225 """Writes the payload content to a file.
226
227 Args:
228 file_obj: a file object open for writing
229 manifest_len: manifest len to dump (otherwise computed automatically)
230 data_blobs: a list of data blobs to be concatenated to the payload
231 sigs_data: a binary Signatures message to be concatenated to the payload
232 padding: stuff to dump past the normal data blobs provided (optional)
Gilad Arnold5502b562013-03-08 13:22:31 -0800233 """
234 manifest = self.manifest.SerializeToString()
235 if manifest_len < 0:
236 manifest_len = len(manifest)
237 self._WriteHeaderToFile(file_obj, manifest_len)
238 file_obj.write(manifest)
239 if data_blobs:
240 for data_blob in data_blobs:
241 file_obj.write(data_blob)
242 if sigs_data:
243 file_obj.write(sigs_data)
244 if padding:
245 file_obj.write(padding)
246
247
248class EnhancedPayloadGenerator(PayloadGenerator):
249 """Payload generator with automatic handling of data blobs.
250
251 Attributes:
252 data_blobs: a list of blobs, in the order they were added
253 curr_offset: the currently consumed offset of blobs added to the payload
Gilad Arnold5502b562013-03-08 13:22:31 -0800254 """
255
256 def __init__(self):
257 super(EnhancedPayloadGenerator, self).__init__()
258 self.data_blobs = []
259 self.curr_offset = 0
260
261 def AddData(self, data_blob):
262 """Adds a (possibly orphan) data blob."""
263 data_length = len(data_blob)
264 data_offset = self.curr_offset
265 self.curr_offset += data_length
266 self.data_blobs.append(data_blob)
267 return data_length, data_offset
268
269 def AddOperationWithData(self, is_kernel, op_type, src_extents=None,
270 src_length=None, dst_extents=None, dst_length=None,
271 data_blob=None, do_hash_data_blob=True):
272 """Adds an install operation and associated data blob.
273
274 This takes care of obtaining a hash of the data blob (if so instructed)
275 and appending it to the internally maintained list of blobs, including the
276 necessary offset/length accounting.
277
278 Args:
279 is_kernel: whether this is a kernel (True) or rootfs (False) operation
280 op_type: one of REPLACE, REPLACE_BZ, MOVE or BSDIFF
281 src_extents: list of (start, length) pairs indicating src block ranges
282 src_length: size of the src data in bytes (needed for BSDIFF)
283 dst_extents: list of (start, length) pairs indicating dst block ranges
284 dst_length: size of the dst data in bytes (needed for BSDIFF)
285 data_blob: a data blob associated with this operation
286 do_hash_data_blob: whether or not to compute and add a data blob hash
Gilad Arnold5502b562013-03-08 13:22:31 -0800287 """
288 data_offset = data_length = data_sha256_hash = None
289 if data_blob is not None:
290 if do_hash_data_blob:
291 # pylint: disable=E1101
292 data_sha256_hash = hashlib.sha256(data_blob).digest()
293 data_length, data_offset = self.AddData(data_blob)
294
295 self.AddOperation(is_kernel, op_type, data_offset=data_offset,
296 data_length=data_length, src_extents=src_extents,
297 src_length=src_length, dst_extents=dst_extents,
298 dst_length=dst_length, data_sha256_hash=data_sha256_hash)
299
300 def WriteToFileWithData(self, file_obj, sigs_data=None,
301 privkey_file_name=None,
302 do_add_pseudo_operation=False,
303 is_pseudo_in_kernel=False, padding=None):
304 """Writes the payload content to a file, optionally signing the content.
305
306 Args:
307 file_obj: a file object open for writing
308 sigs_data: signatures blob to be appended to the payload (optional;
309 payload signature fields assumed to be preset by the caller)
310 privkey_file_name: key used for signing the payload (optional; used only
311 if explicit signatures blob not provided)
312 do_add_pseudo_operation: whether a pseudo-operation should be added to
313 account for the signature blob
314 is_pseudo_in_kernel: whether the pseudo-operation should be added to
315 kernel (True) or rootfs (False) operations
316 padding: stuff to dump past the normal data blobs provided (optional)
Gilad Arnold25c18212015-07-14 09:55:07 -0700317
Gilad Arnold5502b562013-03-08 13:22:31 -0800318 Raises:
319 TestError: if arguments are inconsistent or something goes wrong.
Gilad Arnold5502b562013-03-08 13:22:31 -0800320 """
321 sigs_len = len(sigs_data) if sigs_data else 0
322
323 # Do we need to generate a genuine signatures blob?
324 do_generate_sigs_data = sigs_data is None and privkey_file_name
325
326 if do_generate_sigs_data:
327 # First, sign some arbitrary data to obtain the size of a signature blob.
328 fake_sig = SignSha256('fake-payload-data', privkey_file_name)
329 fake_sigs_gen = SignaturesGenerator()
330 fake_sigs_gen.AddSig(1, fake_sig)
331 sigs_len = len(fake_sigs_gen.ToBinary())
332
333 # Update the payload with proper signature attributes.
334 self.SetSignatures(self.curr_offset, sigs_len)
335
336 # Add a pseudo-operation to account for the signature blob, if requested.
337 if do_add_pseudo_operation:
338 if not self.block_size:
339 raise TestError('cannot add pseudo-operation without knowing the '
340 'payload block size')
341 self.AddOperation(
342 is_pseudo_in_kernel, common.OpType.REPLACE,
343 data_offset=self.curr_offset, data_length=sigs_len,
344 dst_extents=[(common.PSEUDO_EXTENT_MARKER,
345 (sigs_len + self.block_size - 1) / self.block_size)])
346
347 if do_generate_sigs_data:
348 # Once all payload fields are updated, dump and sign it.
349 temp_payload_file = cStringIO.StringIO()
350 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)