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        'system_root_image' : 'true',
160        'verity' : 'true',
161    }
162    options = {
163        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
164        'verity_key_mincrypt' : verity_key_mincrypt,
165    }
166    ValidateVerifiedBootImages(input_tmp, info_dict, options)
167
168  @test_utils.SkipIfExternalToolsUnavailable()
169  def test_ValidateVerifiedBootImages_nonSystemRootImage(self):
170    input_tmp = common.MakeTempDir()
171    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
172    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
173    self._generate_system_image(system_image)
174
175    # Pack the verity key into the root dir in system.img.
176    verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
177    os.makedirs(os.path.dirname(verity_key_mincrypt))
178    shutil.copyfile(
179        os.path.join(self.testdata_dir, 'testkey_mincrypt'),
180        verity_key_mincrypt)
181
182    # And a copy in ramdisk.
183    verity_key_ramdisk = os.path.join(
184        input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
185    os.makedirs(os.path.dirname(verity_key_ramdisk))
186    shutil.copyfile(
187        os.path.join(self.testdata_dir, 'testkey_mincrypt'),
188        verity_key_ramdisk)
189
190    info_dict = {
191        'verity' : 'true',
192    }
193    options = {
194        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
195        'verity_key_mincrypt' : verity_key_mincrypt,
196    }
197    ValidateVerifiedBootImages(input_tmp, info_dict, options)
198
199  @test_utils.SkipIfExternalToolsUnavailable()
200  def test_ValidateVerifiedBootImages_nonSystemRootImage_mismatchingKeys(self):
201    input_tmp = common.MakeTempDir()
202    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
203    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
204    self._generate_system_image(system_image)
205
206    # Pack the verity key into the root dir in system.img.
207    verity_key_mincrypt = os.path.join(input_tmp, 'ROOT', 'verity_key')
208    os.makedirs(os.path.dirname(verity_key_mincrypt))
209    shutil.copyfile(
210        os.path.join(self.testdata_dir, 'testkey_mincrypt'),
211        verity_key_mincrypt)
212
213    # And an invalid copy in ramdisk.
214    verity_key_ramdisk = os.path.join(
215        input_tmp, 'BOOT', 'RAMDISK', 'verity_key')
216    os.makedirs(os.path.dirname(verity_key_ramdisk))
217    shutil.copyfile(
218        os.path.join(self.testdata_dir, 'verity_mincrypt'),
219        verity_key_ramdisk)
220
221    info_dict = {
222        'verity' : 'true',
223    }
224    options = {
225        'verity_key' : os.path.join(self.testdata_dir, 'testkey.x509.pem'),
226        'verity_key_mincrypt' : verity_key_mincrypt,
227    }
228    self.assertRaises(
229        AssertionError, ValidateVerifiedBootImages, input_tmp, info_dict,
230        options)
231
232  @test_utils.SkipIfExternalToolsUnavailable()
233  def test_ValidateFileConsistency_incompleteRange(self):
234    input_tmp = common.MakeTempDir()
235    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
236    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
237    system_root = os.path.join(input_tmp, "SYSTEM")
238    os.mkdir(system_root)
239
240    # Write test files that contain multiple blocks of zeros, and these zero
241    # blocks will be omitted by kernel. Each test file will occupy one block in
242    # the final system image.
243    with open(os.path.join(system_root, 'a'), 'w') as f:
244      f.write('aaa')
245      f.write('\0' * 4096 * 3)
246    with open(os.path.join(system_root, 'b'), 'w') as f:
247      f.write('bbb')
248      f.write('\0' * 4096 * 3)
249
250    raw_file_map = os.path.join(input_tmp, 'IMAGES', 'raw_system.map')
251    self._generate_system_image(system_image, system_root, raw_file_map)
252
253    # Parse the generated file map and update the block ranges for each file.
254    file_map_list = {}
255    image_ranges = RangeSet()
256    with open(raw_file_map) as f:
257      for line in f.readlines():
258        info = line.split()
259        self.assertEqual(2, len(info))
260        image_ranges = image_ranges.union(RangeSet(info[1]))
261        file_map_list[info[0]] = RangeSet(info[1])
262
263    # Add one unoccupied block as the shared block for all test files.
264    mock_shared_block = RangeSet("10-20").subtract(image_ranges).first(1)
265    with open(os.path.join(input_tmp, 'IMAGES', 'system.map'), 'w') as f:
266      for key in sorted(file_map_list.keys()):
267        line = '{} {}\n'.format(
268            key, file_map_list[key].union(mock_shared_block))
269        f.write(line)
270
271    # Prepare for the target zip file
272    input_file = common.MakeTempFile()
273    all_entries = ['SYSTEM/', 'SYSTEM/b', 'SYSTEM/a', 'IMAGES/',
274                   'IMAGES/system.map', 'IMAGES/system.img']
275    with zipfile.ZipFile(input_file, 'w') as input_zip:
276      for name in all_entries:
277        input_zip.write(os.path.join(input_tmp, name), arcname=name)
278
279    # Expect the validation to pass and both files are skipped due to
280    # 'incomplete' block range.
281    with zipfile.ZipFile(input_file) as input_zip:
282      info_dict = {'extfs_sparse_flag': '-s'}
283      ValidateFileConsistency(input_zip, input_tmp, info_dict)
284
285  @test_utils.SkipIfExternalToolsUnavailable()
286  def test_ValidateFileConsistency_nonMonotonicRanges(self):
287    input_tmp = common.MakeTempDir()
288    os.mkdir(os.path.join(input_tmp, 'IMAGES'))
289    system_image = os.path.join(input_tmp, 'IMAGES', 'system.img')
290    system_root = os.path.join(input_tmp, "SYSTEM")
291    os.mkdir(system_root)
292
293    # Write the test file that contain three blocks of 'a', 'b', 'c'.
294    with open(os.path.join(system_root, 'abc'), 'w') as f:
295      f.write('a' * 4096 + 'b' * 4096 + 'c' * 4096)
296    raw_file_map = os.path.join(input_tmp, 'IMAGES', 'raw_system.map')
297    self._generate_system_image(system_image, system_root, raw_file_map)
298
299    # Parse the generated file map and manipulate the block ranges of 'abc' to
300    # be 'cba'.
301    file_map_list = {}
302    with open(raw_file_map) as f:
303      for line in f.readlines():
304        info = line.split()
305        self.assertEqual(2, len(info))
306        ranges = RangeSet(info[1])
307        self.assertTrue(ranges.monotonic)
308        blocks = reversed(list(ranges.next_item()))
309        file_map_list[info[0]] = ' '.join([str(block) for block in blocks])
310
311    # Update the contents of 'abc' to be 'cba'.
312    with open(os.path.join(system_root, 'abc'), 'w') as f:
313      f.write('c' * 4096 + 'b' * 4096 + 'a' * 4096)
314
315    # Update the system.map.
316    with open(os.path.join(input_tmp, 'IMAGES', 'system.map'), 'w') as f:
317      for key in sorted(file_map_list.keys()):
318        f.write('{} {}\n'.format(key, file_map_list[key]))
319
320    # Get the target zip file.
321    input_file = common.MakeTempFile()
322    all_entries = ['SYSTEM/', 'SYSTEM/abc', 'IMAGES/',
323                   'IMAGES/system.map', 'IMAGES/system.img']
324    with zipfile.ZipFile(input_file, 'w') as input_zip:
325      for name in all_entries:
326        input_zip.write(os.path.join(input_tmp, name), arcname=name)
327
328    with zipfile.ZipFile(input_file) as input_zip:
329      info_dict = {'extfs_sparse_flag': '-s'}
330      ValidateFileConsistency(input_zip, input_tmp, info_dict)
331
332  @staticmethod
333  def make_build_prop(build_prop):
334    input_tmp = common.MakeTempDir()
335    system_dir = os.path.join(input_tmp, 'SYSTEM')
336    os.makedirs(system_dir)
337    prop_file = os.path.join(system_dir, 'build.prop')
338    with open(prop_file, 'w') as output_file:
339      output_file.write("\n".join(build_prop))
340    return input_tmp
341
342  def test_checkDuplicateProps_noDuplicate(self):
343    build_prop = [
344        'ro.odm.build.date.utc=1578430045',
345        'ro.odm.build.fingerprint='
346        'google/coral/coral:10/RP1A.200325.001/6337676:user/dev-keys',
347        'ro.product.odm.device=coral',
348    ]
349    input_tmp = ValidateTargetFilesTest.make_build_prop(build_prop)
350    CheckBuildPropDuplicity(input_tmp)
351
352  def test_checkDuplicateProps_withDuplicate(self):
353    build_prop = [
354        'ro.odm.build.date.utc=1578430045',
355        'ro.odm.build.date.utc=1578430049',
356        'ro.odm.build.fingerprint='
357        'google/coral/coral:10/RP1A.200325.001/6337676:user/dev-keys',
358        'ro.product.odm.device=coral',
359    ]
360    input_tmp = ValidateTargetFilesTest.make_build_prop({
361        'ODM/etc/build.prop': '\n'.join(build_prop),
362    })
363
364    self.assertRaises(ValueError, CheckBuildPropDuplicity,
365                        input_tmp)
366