test_utils.py 13 KB

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