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