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