Home | History | Annotate | Download | only in releasetools
      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 verity_utils.py."""
     18 
     19 import copy
     20 import math
     21 import os.path
     22 import random
     23 
     24 import common
     25 import sparse_img
     26 from rangelib import RangeSet
     27 from test_utils import get_testdata_dir, ReleaseToolsTestCase
     28 from verity_utils import (
     29     CreateHashtreeInfoGenerator, CreateVerityImageBuilder, HashtreeInfo,
     30     VerifiedBootVersion1HashtreeInfoGenerator)
     31 
     32 BLOCK_SIZE = common.BLOCK_SIZE
     33 
     34 
     35 class VerifiedBootVersion1HashtreeInfoGeneratorTest(ReleaseToolsTestCase):
     36 
     37   def setUp(self):
     38     self.testdata_dir = get_testdata_dir()
     39 
     40     self.partition_size = 1024 * 1024
     41     self.prop_dict = {
     42         'verity': 'true',
     43         'verity_fec': 'true',
     44         'system_verity_block_device': '/dev/block/system',
     45         'system_size': self.partition_size
     46     }
     47 
     48     self.hash_algorithm = "sha256"
     49     self.fixed_salt = \
     50         "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7"
     51     self.expected_root_hash = \
     52         "0b7c4565e87b1026e11fbab91c0bc29e185c847a5b44d40e6e86e461e8adf80d"
     53 
     54   def _create_simg(self, raw_data):
     55     output_file = common.MakeTempFile()
     56     raw_image = common.MakeTempFile()
     57     with open(raw_image, 'wb') as f:
     58       f.write(raw_data)
     59 
     60     cmd = ["img2simg", raw_image, output_file, '4096']
     61     p = common.Run(cmd)
     62     p.communicate()
     63     self.assertEqual(0, p.returncode)
     64 
     65     return output_file
     66 
     67   def _generate_image(self):
     68     partition_size = 1024 * 1024
     69     prop_dict = {
     70         'partition_size': str(partition_size),
     71         'verity': 'true',
     72         'verity_block_device': '/dev/block/system',
     73         'verity_key': os.path.join(self.testdata_dir, 'testkey'),
     74         'verity_fec': 'true',
     75         'verity_signer_cmd': 'verity_signer',
     76     }
     77     verity_image_builder = CreateVerityImageBuilder(prop_dict)
     78     self.assertIsNotNone(verity_image_builder)
     79     adjusted_size = verity_image_builder.CalculateMaxImageSize()
     80 
     81     raw_image = ""
     82     for i in range(adjusted_size):
     83       raw_image += str(i % 10)
     84 
     85     output_file = self._create_simg(raw_image)
     86 
     87     # Append the verity metadata.
     88     verity_image_builder.Build(output_file)
     89 
     90     return output_file
     91 
     92   def test_CreateHashtreeInfoGenerator(self):
     93     image_file = sparse_img.SparseImage(self._generate_image())
     94 
     95     generator = CreateHashtreeInfoGenerator(
     96         'system', image_file, self.prop_dict)
     97     self.assertEqual(
     98         VerifiedBootVersion1HashtreeInfoGenerator, type(generator))
     99     self.assertEqual(self.partition_size, generator.partition_size)
    100     self.assertTrue(generator.fec_supported)
    101 
    102   def test_DecomposeSparseImage(self):
    103     image_file = sparse_img.SparseImage(self._generate_image())
    104 
    105     generator = VerifiedBootVersion1HashtreeInfoGenerator(
    106         self.partition_size, 4096, True)
    107     generator.DecomposeSparseImage(image_file)
    108     self.assertEqual(991232, generator.filesystem_size)
    109     self.assertEqual(12288, generator.hashtree_size)
    110     self.assertEqual(32768, generator.metadata_size)
    111 
    112   def test_ParseHashtreeMetadata(self):
    113     image_file = sparse_img.SparseImage(self._generate_image())
    114     generator = VerifiedBootVersion1HashtreeInfoGenerator(
    115         self.partition_size, 4096, True)
    116     generator.DecomposeSparseImage(image_file)
    117 
    118     # pylint: disable=protected-access
    119     generator._ParseHashtreeMetadata()
    120 
    121     self.assertEqual(
    122         self.hash_algorithm, generator.hashtree_info.hash_algorithm)
    123     self.assertEqual(self.fixed_salt, generator.hashtree_info.salt)
    124     self.assertEqual(self.expected_root_hash, generator.hashtree_info.root_hash)
    125 
    126   def test_ValidateHashtree_smoke(self):
    127     generator = VerifiedBootVersion1HashtreeInfoGenerator(
    128         self.partition_size, 4096, True)
    129     generator.image = sparse_img.SparseImage(self._generate_image())
    130 
    131     generator.hashtree_info = info = HashtreeInfo()
    132     info.filesystem_range = RangeSet(data=[0, 991232 / 4096])
    133     info.hashtree_range = RangeSet(
    134         data=[991232 / 4096, (991232 + 12288) / 4096])
    135     info.hash_algorithm = self.hash_algorithm
    136     info.salt = self.fixed_salt
    137     info.root_hash = self.expected_root_hash
    138 
    139     self.assertTrue(generator.ValidateHashtree())
    140 
    141   def test_ValidateHashtree_failure(self):
    142     generator = VerifiedBootVersion1HashtreeInfoGenerator(
    143         self.partition_size, 4096, True)
    144     generator.image = sparse_img.SparseImage(self._generate_image())
    145 
    146     generator.hashtree_info = info = HashtreeInfo()
    147     info.filesystem_range = RangeSet(data=[0, 991232 / 4096])
    148     info.hashtree_range = RangeSet(
    149         data=[991232 / 4096, (991232 + 12288) / 4096])
    150     info.hash_algorithm = self.hash_algorithm
    151     info.salt = self.fixed_salt
    152     info.root_hash = "a" + self.expected_root_hash[1:]
    153 
    154     self.assertFalse(generator.ValidateHashtree())
    155 
    156   def test_Generate(self):
    157     image_file = sparse_img.SparseImage(self._generate_image())
    158     generator = CreateHashtreeInfoGenerator('system', 4096, self.prop_dict)
    159     info = generator.Generate(image_file)
    160 
    161     self.assertEqual(RangeSet(data=[0, 991232 / 4096]), info.filesystem_range)
    162     self.assertEqual(RangeSet(data=[991232 / 4096, (991232 + 12288) / 4096]),
    163                      info.hashtree_range)
    164     self.assertEqual(self.hash_algorithm, info.hash_algorithm)
    165     self.assertEqual(self.fixed_salt, info.salt)
    166     self.assertEqual(self.expected_root_hash, info.root_hash)
    167 
    168 
    169 class VerifiedBootVersion1VerityImageBuilderTest(ReleaseToolsTestCase):
    170 
    171   DEFAULT_PARTITION_SIZE = 4096 * 1024
    172   DEFAULT_PROP_DICT = {
    173       'partition_size': str(DEFAULT_PARTITION_SIZE),
    174       'verity': 'true',
    175       'verity_block_device': '/dev/block/system',
    176       'verity_key': os.path.join(get_testdata_dir(), 'testkey'),
    177       'verity_fec': 'true',
    178       'verity_signer_cmd': 'verity_signer',
    179   }
    180 
    181   def test_init(self):
    182     prop_dict = copy.deepcopy(self.DEFAULT_PROP_DICT)
    183     verity_image_builder = CreateVerityImageBuilder(prop_dict)
    184     self.assertIsNotNone(verity_image_builder)
    185     self.assertEqual(1, verity_image_builder.version)
    186 
    187   def test_init_MissingProps(self):
    188     prop_dict = copy.deepcopy(self.DEFAULT_PROP_DICT)
    189     del prop_dict['verity']
    190     self.assertIsNone(CreateVerityImageBuilder(prop_dict))
    191 
    192     prop_dict = copy.deepcopy(self.DEFAULT_PROP_DICT)
    193     del prop_dict['verity_block_device']
    194     self.assertIsNone(CreateVerityImageBuilder(prop_dict))
    195 
    196   def test_CalculateMaxImageSize(self):
    197     verity_image_builder = CreateVerityImageBuilder(self.DEFAULT_PROP_DICT)
    198     size = verity_image_builder.CalculateMaxImageSize()
    199     self.assertLess(size, self.DEFAULT_PARTITION_SIZE)
    200 
    201     # Same result by explicitly passing the partition size.
    202     self.assertEqual(
    203         verity_image_builder.CalculateMaxImageSize(),
    204         verity_image_builder.CalculateMaxImageSize(
    205             self.DEFAULT_PARTITION_SIZE))
    206 
    207   @staticmethod
    208   def _BuildAndVerify(prop, verify_key):
    209     verity_image_builder = CreateVerityImageBuilder(prop)
    210     image_size = verity_image_builder.CalculateMaxImageSize()
    211 
    212     # Build the sparse image with verity metadata.
    213     input_dir = common.MakeTempDir()
    214     image = common.MakeTempFile(suffix='.img')
    215     cmd = ['mkuserimg_mke2fs', input_dir, image, 'ext4', '/system',
    216            str(image_size), '-j', '0', '-s']
    217     common.RunAndCheckOutput(cmd)
    218     verity_image_builder.Build(image)
    219 
    220     # Verify the verity metadata.
    221     cmd = ['verity_verifier', image, '-mincrypt', verify_key]
    222     common.RunAndCheckOutput(cmd)
    223 
    224   def test_Build(self):
    225     self._BuildAndVerify(
    226         self.DEFAULT_PROP_DICT,
    227         os.path.join(get_testdata_dir(), 'testkey_mincrypt'))
    228 
    229   def test_Build_SanityCheck(self):
    230     # A sanity check for the test itself: the image shouldn't be verifiable
    231     # with wrong key.
    232     self.assertRaises(
    233         common.ExternalError,
    234         self._BuildAndVerify,
    235         self.DEFAULT_PROP_DICT,
    236         os.path.join(get_testdata_dir(), 'verity_mincrypt'))
    237 
    238   def test_Build_FecDisabled(self):
    239     prop_dict = copy.deepcopy(self.DEFAULT_PROP_DICT)
    240     del prop_dict['verity_fec']
    241     self._BuildAndVerify(
    242         prop_dict,
    243         os.path.join(get_testdata_dir(), 'testkey_mincrypt'))
    244 
    245   def test_Build_SquashFs(self):
    246     verity_image_builder = CreateVerityImageBuilder(self.DEFAULT_PROP_DICT)
    247     verity_image_builder.CalculateMaxImageSize()
    248 
    249     # Build the sparse image with verity metadata.
    250     input_dir = common.MakeTempDir()
    251     image = common.MakeTempFile(suffix='.img')
    252     cmd = ['mksquashfsimage.sh', input_dir, image, '-s']
    253     common.RunAndCheckOutput(cmd)
    254     verity_image_builder.PadSparseImage(image)
    255     verity_image_builder.Build(image)
    256 
    257     # Verify the verity metadata.
    258     cmd = ["verity_verifier", image, '-mincrypt',
    259            os.path.join(get_testdata_dir(), 'testkey_mincrypt')]
    260     common.RunAndCheckOutput(cmd)
    261 
    262 
    263 class VerifiedBootVersion2VerityImageBuilderTest(ReleaseToolsTestCase):
    264 
    265   DEFAULT_PROP_DICT = {
    266       'partition_size': str(4096 * 1024),
    267       'partition_name': 'system',
    268       'avb_avbtool': 'avbtool',
    269       'avb_hashtree_enable': 'true',
    270       'avb_add_hashtree_footer_args': '',
    271   }
    272 
    273   def test_init(self):
    274     prop_dict = copy.deepcopy(self.DEFAULT_PROP_DICT)
    275     verity_image_builder = CreateVerityImageBuilder(prop_dict)
    276     self.assertIsNotNone(verity_image_builder)
    277     self.assertEqual(2, verity_image_builder.version)
    278 
    279   def test_init_MissingProps(self):
    280     prop_dict = copy.deepcopy(self.DEFAULT_PROP_DICT)
    281     del prop_dict['avb_hashtree_enable']
    282     verity_image_builder = CreateVerityImageBuilder(prop_dict)
    283     self.assertIsNone(verity_image_builder)
    284 
    285   def test_Build(self):
    286     prop_dict = copy.deepcopy(self.DEFAULT_PROP_DICT)
    287     verity_image_builder = CreateVerityImageBuilder(prop_dict)
    288     self.assertIsNotNone(verity_image_builder)
    289     self.assertEqual(2, verity_image_builder.version)
    290 
    291     input_dir = common.MakeTempDir()
    292     image_dir = common.MakeTempDir()
    293     system_image = os.path.join(image_dir, 'system.img')
    294     system_image_size = verity_image_builder.CalculateMaxImageSize()
    295     cmd = ['mkuserimg_mke2fs', input_dir, system_image, 'ext4', '/system',
    296            str(system_image_size), '-j', '0', '-s']
    297     common.RunAndCheckOutput(cmd)
    298     verity_image_builder.Build(system_image)
    299 
    300     # Additionally make vbmeta image so that we can verify with avbtool.
    301     vbmeta_image = os.path.join(image_dir, 'vbmeta.img')
    302     cmd = ['avbtool', 'make_vbmeta_image', '--include_descriptors_from_image',
    303            system_image, '--output', vbmeta_image]
    304     common.RunAndCheckOutput(cmd)
    305 
    306     # Verify the verity metadata.
    307     cmd = ['avbtool', 'verify_image', '--image', vbmeta_image]
    308     common.RunAndCheckOutput(cmd)
    309 
    310   def _test_CalculateMinPartitionSize_SetUp(self):
    311     # To test CalculateMinPartitionSize(), by using 200MB to 2GB image size.
    312     #   -  51200 = 200MB * 1024 * 1024 / 4096
    313     #   - 524288 = 2GB * 1024 * 1024 * 1024 / 4096
    314     image_sizes = [BLOCK_SIZE * random.randint(51200, 524288) + offset
    315                    for offset in range(BLOCK_SIZE)]
    316 
    317     prop_dict = {
    318         'partition_size': None,
    319         'partition_name': 'system',
    320         'avb_avbtool': 'avbtool',
    321         'avb_hashtree_enable': 'true',
    322         'avb_add_hashtree_footer_args': None,
    323     }
    324     builder = CreateVerityImageBuilder(prop_dict)
    325     self.assertEqual(2, builder.version)
    326     return image_sizes, builder
    327 
    328   def test_CalculateMinPartitionSize_LinearFooterSize(self):
    329     """Tests with footer size which is linear to partition size."""
    330     image_sizes, builder = self._test_CalculateMinPartitionSize_SetUp()
    331     for image_size in image_sizes:
    332       for ratio in 0.95, 0.56, 0.22:
    333         expected_size = common.RoundUpTo4K(int(math.ceil(image_size / ratio)))
    334         self.assertEqual(
    335             expected_size,
    336             builder.CalculateMinPartitionSize(
    337                 image_size, lambda x, ratio=ratio: int(x * ratio)))
    338 
    339   def test_AVBCalcMinPartitionSize_SlowerGrowthFooterSize(self):
    340     """Tests with footer size which grows slower than partition size."""
    341 
    342     def _SizeCalculator(partition_size):
    343       """Footer size is the power of 0.95 of partition size."""
    344       # Minus footer size to return max image size.
    345       return partition_size - int(math.pow(partition_size, 0.95))
    346 
    347     image_sizes, builder = self._test_CalculateMinPartitionSize_SetUp()
    348     for image_size in image_sizes:
    349       min_partition_size = builder.CalculateMinPartitionSize(
    350           image_size, _SizeCalculator)
    351       # Checks min_partition_size can accommodate image_size.
    352       self.assertGreaterEqual(
    353           _SizeCalculator(min_partition_size),
    354           image_size)
    355       # Checks min_partition_size (round to BLOCK_SIZE) is the minimum.
    356       self.assertLess(
    357           _SizeCalculator(min_partition_size - BLOCK_SIZE),
    358           image_size)
    359 
    360   def test_CalculateMinPartitionSize_FasterGrowthFooterSize(self):
    361     """Tests with footer size which grows faster than partition size."""
    362 
    363     def _SizeCalculator(partition_size):
    364       """Max image size is the power of 0.95 of partition size."""
    365       # Max image size grows less than partition size, which means
    366       # footer size grows faster than partition size.
    367       return int(math.pow(partition_size, 0.95))
    368 
    369     image_sizes, builder = self._test_CalculateMinPartitionSize_SetUp()
    370     for image_size in image_sizes:
    371       min_partition_size = builder.CalculateMinPartitionSize(
    372           image_size, _SizeCalculator)
    373       # Checks min_partition_size can accommodate image_size.
    374       self.assertGreaterEqual(
    375           _SizeCalculator(min_partition_size),
    376           image_size)
    377       # Checks min_partition_size (round to BLOCK_SIZE) is the minimum.
    378       self.assertLess(
    379           _SizeCalculator(min_partition_size - BLOCK_SIZE),
    380           image_size)
    381