1#
2# Copyright (C) 2018 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
17"""Unittests for validate_target_files.py."""
18
19import os
20import os.path
21import shutil
22import zipfile
23
24import common
25import test_utils
26from rangelib import RangeSet
27from validate_target_files import (ValidateVerifiedBootImages,
28                                   ValidateFileConsistency, CheckBuildPropDuplicity)
29from verity_utils import CreateVerityImageBuilder
30
31class ValidateTargetFilesTest(test_utils.ReleaseToolsTestCase):
32
33  def setUp(self):
34    self.testdata_dir = test_utils.get_testdata_dir()
35
36  def _generate_boot_image(self, output_file):
37    kernel = common.MakeTempFile(prefix='kernel-')
38    with open(kernel, 'wb') as kernel_fp:
39      kernel_fp.write(os.urandom(10))
40
41    cmd = ['mkbootimg', '--kernel', kernel, '-o', output_file]
42    proc = common.Run(cmd)
43    stdoutdata, _ = proc.communicate()
44    self.assertEqual(
45        0, proc.returncode,
46        "Failed to run mkbootimg: {}".format(stdoutdata))
47
48    cmd = ['boot_signer', '/boot', output_file,
49           os.path.join(self.testdata_dir, 'testkey.pk8'),
50           os.path.join(self.testdata_dir, 'testkey.x509.pem'), output_file]
51    proc = common.Run(cmd)
52    stdoutdata, _ = proc.communicate()
53    self.assertEqual(
54        0, proc.returncode,
55        "Failed to sign boot image with boot_signer: {}".format(stdoutdata))
56
57  @test_utils.SkipIfExternalToolsUnavailable()
58  def test_ValidateVerifiedBootImages_bootImage(self):
59    input_tmp = common.MakeTempDir()
60    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
61    boot_image = os.path.join(input_tmp, 'IMAGES', 'boot.img')
62    self._generate_boot_image(boot_image)
63
64    info_dict = {
65        'boot_signer' : 'true',
66    }
67    options = {
68        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
69    }
70    ValidateVerifiedBootImages(input_tmp, info_dict, options)
71
72  @test_utils.SkipIfExternalToolsUnavailable()
73  def test_ValidateVerifiedBootImages_bootImage_wrongKey(self):
74    input_tmp = common.MakeTempDir()
75    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
76    boot_image = os.path.join(input_tmp, 'IMAGES', 'boot.img')
77    self._generate_boot_image(boot_image)
78
79    info_dict = {
80        'boot_signer' : 'true',
81    }
82    options = {
83        'verity_key' : os.path.join(self.testdata_dir, 'verity.x509.pem'),
84    }
85    self.assertRaises(
86        AssertionError, ValidateVerifiedBootImages, input_tmp, info_dict,
87        options)
88
89  @test_utils.SkipIfExternalToolsUnavailable()
90  def test_ValidateVerifiedBootImages_bootImage_corrupted(self):
91    input_tmp = common.MakeTempDir()
92    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
93    boot_image = os.path.join(input_tmp, 'IMAGES', 'boot.img')
94    self._generate_boot_image(boot_image)
95
96    # Corrupt the late byte of the image.
97    with open(boot_image, 'r+b') as boot_fp:
98      boot_fp.seek(-1, os.SEEK_END)
99      last_byte = boot_fp.read(1)
100      last_byte = bytes([255 - ord(last_byte)])
101      boot_fp.seek(-1, os.SEEK_END)
102      boot_fp.write(last_byte)
103
104    info_dict = {
105        'boot_signer' : 'true',
106    }
107    options = {
108        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
109    }
110    self.assertRaises(
111        AssertionError, ValidateVerifiedBootImages, input_tmp, info_dict,
112        options)
113
114  def _generate_system_image(self, output_file, system_root=None,
115                             file_map=None):
116    prop_dict = {
117        'partition_size': str(1024 * 1024),
118        'verity': 'true',
119        'verity_block_device': '/dev/block/system',
120        'verity_key' : os.path.join(self.testdata_dir, 'testkey'),
121        'verity_fec': "true",
122        'verity_signer_cmd': 'verity_signer',
123    }
124    verity_image_builder = CreateVerityImageBuilder(prop_dict)
125    image_size = verity_image_builder.CalculateMaxImageSize()
126
127    # Use an empty root directory.
128    if not system_root:
129      system_root = common.MakeTempDir()
130    cmd = ['mkuserimg_mke2fs', '-s', system_root, output_file, 'ext4',
131           '/system', str(image_size), '-j', '0']
132    if file_map:
133      cmd.extend(['-B', file_map])
134    proc = common.Run(cmd)
135    stdoutdata, _ = proc.communicate()
136    self.assertEqual(
137        0, proc.returncode,
138        "Failed to create system image with mkuserimg_mke2fs: {}".format(
139            stdoutdata))
140
141    # Append the verity metadata.
142    verity_image_builder.Build(output_file)
143
144  @test_utils.SkipIfExternalToolsUnavailable()
145  def test_ValidateVerifiedBootImages_systemRootImage(self):
146    input_tmp = common.MakeTempDir()
147    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
148    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
149    self._generate_system_image(system_image)
150
151    # Pack the verity key.
152    verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
153    os.makedirs(os.path.dirname(verity_key_mincrypt))
154    shutil.copyfile(
155        os.path.join(self.testdata_dir, 'testkey_mincrypt'),
156        verity_key_mincrypt)
157
158    info_dict = {
159        'verity' : 'true',
160    }
161    options = {
162        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
163        'verity_key_mincrypt' : verity_key_mincrypt,
164    }
165    ValidateVerifiedBootImages(input_tmp, info_dict, options)
166
167  @test_utils.SkipIfExternalToolsUnavailable()
168  def test_ValidateVerifiedBootImages_nonSystemRootImage(self):
169    input_tmp = common.MakeTempDir()
170    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
171    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
172    self._generate_system_image(system_image)
173
174    # Pack the verity key into the root dir in system.img.
175    verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
176    os.makedirs(os.path.dirname(verity_key_mincrypt))
177    shutil.copyfile(
178        os.path.join(self.testdata_dir, 'testkey_mincrypt'),
179        verity_key_mincrypt)
180
181    # And a copy in ramdisk.
182    verity_key_ramdisk = os.path.join(
183        input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
184    os.makedirs(os.path.dirname(verity_key_ramdisk))
185    shutil.copyfile(
186        os.path.join(self.testdata_dir, 'testkey_mincrypt'),
187        verity_key_ramdisk)
188
189    info_dict = {
190        'verity' : 'true',
191    }
192    options = {
193        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
194        'verity_key_mincrypt' : verity_key_mincrypt,
195    }
196    ValidateVerifiedBootImages(input_tmp, info_dict, options)
197
198  @test_utils.SkipIfExternalToolsUnavailable()
199  def test_ValidateVerifiedBootImages_nonSystemRootImage_mismatchingKeys(self):
200    input_tmp = common.MakeTempDir()
201    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
202    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
203    self._generate_system_image(system_image)
204
205    # Pack the verity key into the root dir in system.img.
206    verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
207    os.makedirs(os.path.dirname(verity_key_mincrypt))
208    shutil.copyfile(
209        os.path.join(self.testdata_dir, 'testkey_mincrypt'),
210        verity_key_mincrypt)
211
212    # And an invalid copy in ramdisk.
213    verity_key_ramdisk = os.path.join(
214        input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
215    os.makedirs(os.path.dirname(verity_key_ramdisk))
216    shutil.copyfile(
217        os.path.join(self.testdata_dir, 'verity_mincrypt'),
218        verity_key_ramdisk)
219
220    info_dict = {
221        'verity' : 'true',
222    }
223    options = {
224        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
225        'verity_key_mincrypt' : verity_key_mincrypt,
226    }
227    self.assertRaises(
228        AssertionError, ValidateVerifiedBootImages, input_tmp, info_dict,
229        options)
230
231  @test_utils.SkipIfExternalToolsUnavailable()
232  def test_ValidateFileConsistency_incompleteRange(self):
233    input_tmp = common.MakeTempDir()
234    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
235    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
236    system_root = os.path.join(input_tmp, "SYSTEM")
237    os.mkdir(system_root)
238
239    # Write test files that contain multiple blocks of zeros, and these zero
240    # blocks will be omitted by kernel. Each test file will occupy one block in
241    # the final system image.
242    with open(os.path.join(system_root, 'a'), 'w') as f:
243      f.write('aaa')
244      f.write('\0' * 4096 * 3)
245    with open(os.path.join(system_root, 'b'), 'w') as f:
246      f.write('bbb')
247      f.write('\0' * 4096 * 3)
248
249    raw_file_map = os.path.join(input_tmp, 'IMAGES', 'raw_system.map')
250    self._generate_system_image(system_image, system_root, raw_file_map)
251
252    # Parse the generated file map and update the block ranges for each file.
253    file_map_list = {}
254    image_ranges = RangeSet()
255    with open(raw_file_map) as f:
256      for line in f.readlines():
257        info = line.split()
258        self.assertEqual(2, len(info))
259        image_ranges = image_ranges.union(RangeSet(info[1]))
260        file_map_list[info[0]] = RangeSet(info[1])
261
262    # Add one unoccupied block as the shared block for all test files.
263    mock_shared_block = RangeSet("10-20").subtract(image_ranges).first(1)
264    with open(os.path.join(input_tmp, 'IMAGES', 'system.map'), 'w') as f:
265      for key in sorted(file_map_list.keys()):
266        line = '{} {}\n'.format(
267            key, file_map_list[key].union(mock_shared_block))
268        f.write(line)
269
270    # Prepare for the target zip file
271    input_file = common.MakeTempFile()
272    all_entries = ['SYSTEM/', 'SYSTEM/b', 'SYSTEM/a', 'IMAGES/',
273                   'IMAGES/system.map', 'IMAGES/system.img']
274    with zipfile.ZipFile(input_file, 'w', allowZip64=True) as input_zip:
275      for name in all_entries:
276        input_zip.write(os.path.join(input_tmp, name), arcname=name)
277
278    # Expect the validation to pass and both files are skipped due to
279    # 'incomplete' block range.
280    with zipfile.ZipFile(input_file) as input_zip:
281      info_dict = {'extfs_sparse_flag': '-s'}
282      ValidateFileConsistency(input_zip, input_tmp, info_dict)
283
284  @test_utils.SkipIfExternalToolsUnavailable()
285  def test_ValidateFileConsistency_nonMonotonicRanges(self):
286    input_tmp = common.MakeTempDir()
287    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
288    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
289    system_root = os.path.join(input_tmp, "SYSTEM")
290    os.mkdir(system_root)
291
292    # Write the test file that contain three blocks of 'a', 'b', 'c'.
293    with open(os.path.join(system_root, 'abc'), 'w') as f:
294      f.write('a' * 4096 + 'b' * 4096 + 'c' * 4096)
295    raw_file_map = os.path.join(input_tmp, 'IMAGES', 'raw_system.map')
296    self._generate_system_image(system_image, system_root, raw_file_map)
297
298    # Parse the generated file map and manipulate the block ranges of 'abc' to
299    # be 'cba'.
300    file_map_list = {}
301    with open(raw_file_map) as f:
302      for line in f.readlines():
303        info = line.split()
304        self.assertEqual(2, len(info))
305        ranges = RangeSet(info[1])
306        self.assertTrue(ranges.monotonic)
307        blocks = reversed(list(ranges.next_item()))
308        file_map_list[info[0]] = ' '.join([str(block) for block in blocks])
309
310    # Update the contents of 'abc' to be 'cba'.
311    with open(os.path.join(system_root, 'abc'), 'w') as f:
312      f.write('c' * 4096 + 'b' * 4096 + 'a' * 4096)
313
314    # Update the system.map.
315    with open(os.path.join(input_tmp, 'IMAGES', 'system.map'), 'w') as f:
316      for key in sorted(file_map_list.keys()):
317        f.write('{} {}\n'.format(key, file_map_list[key]))
318
319    # Get the target zip file.
320    input_file = common.MakeTempFile()
321    all_entries = ['SYSTEM/', 'SYSTEM/abc', 'IMAGES/',
322                   'IMAGES/system.map', 'IMAGES/system.img']
323    with zipfile.ZipFile(input_file, 'w', allowZip64=True) as input_zip:
324      for name in all_entries:
325        input_zip.write(os.path.join(input_tmp, name), arcname=name)
326
327    with zipfile.ZipFile(input_file) as input_zip:
328      info_dict = {'extfs_sparse_flag': '-s'}
329      ValidateFileConsistency(input_zip, input_tmp, info_dict)
330
331  @staticmethod
332  def make_build_prop(build_prop):
333    input_tmp = common.MakeTempDir()
334    system_dir = os.path.join(input_tmp, 'SYSTEM')
335    os.makedirs(system_dir)
336    prop_file = os.path.join(system_dir, 'build.prop')
337    with open(prop_file, 'w') as output_file:
338      output_file.write("\n".join(build_prop))
339    return input_tmp
340
341  def test_checkDuplicateProps_noDuplicate(self):
342    build_prop = [
343        'ro.odm.build.date.utc=1578430045',
344        'ro.odm.build.fingerprint='
345        'google/coral/coral:10/RP1A.200325.001/6337676:user/dev-keys',
346        'ro.product.odm.device=coral',
347    ]
348    input_tmp = ValidateTargetFilesTest.make_build_prop(build_prop)
349    CheckBuildPropDuplicity(input_tmp)
350
351  def test_checkDuplicateProps_withDuplicate(self):
352    build_prop = [
353        'ro.odm.build.date.utc=1578430045',
354        'ro.odm.build.date.utc=1578430049',
355        'ro.odm.build.fingerprint='
356        'google/coral/coral:10/RP1A.200325.001/6337676:user/dev-keys',
357        'ro.product.odm.device=coral',
358    ]
359    input_tmp = ValidateTargetFilesTest.make_build_prop(build_prop)
360
361    self.assertRaises(ValueError, CheckBuildPropDuplicity, input_tmp)
362