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