Massimo Pegorer | 13878dd | 2023-01-05 10:31:09 +0100 | [diff] [blame] | 1 | # SPDX-License-Identifier: GPL-2.0+ |
| 2 | # Copyright (c) 2022 Massimo Pegorer |
| 3 | |
| 4 | """ |
| 5 | Test that mkimage generates auto-FIT with signatures and/or hashes as expected. |
| 6 | |
| 7 | The mkimage tool can create auto generated (i.e. without an ITS file |
| 8 | provided as input) FIT in three different flavours: with crc32 checksums |
| 9 | of 'images' subnodes; with signatures of 'images' subnodes; with sha1 |
| 10 | hashes of 'images' subnodes and signatures of 'configurations' subnodes. |
| 11 | This test verifies that auto-FIT are generated as expected, in all of |
| 12 | the three flavours, including check of hashes and signatures (except for |
| 13 | configurations ones). |
| 14 | |
| 15 | The test does not run the sandbox. It only checks the host tool mkimage. |
| 16 | """ |
| 17 | |
| 18 | import os |
| 19 | import pytest |
| 20 | import u_boot_utils as util |
| 21 | import binascii |
| 22 | from Cryptodome.Hash import SHA1 |
| 23 | from Cryptodome.Hash import SHA256 |
| 24 | from Cryptodome.PublicKey import RSA |
| 25 | from Cryptodome.Signature import pkcs1_15 |
| 26 | |
| 27 | class SignedFitHelper(object): |
| 28 | """Helper to manipulate a FIT with signed/hashed images/configs.""" |
| 29 | def __init__(self, cons, file_name): |
| 30 | self.fit = file_name |
| 31 | self.cons = cons |
| 32 | self.images_nodes = set() |
| 33 | self.confgs_nodes = set() |
| 34 | |
| 35 | def __fdt_list(self, path): |
| 36 | return util.run_and_log(self.cons, |
| 37 | f'fdtget -l {self.fit} {path}') |
| 38 | |
| 39 | def __fdt_get_string(self, node, prop): |
| 40 | return util.run_and_log(self.cons, |
| 41 | f'fdtget -ts {self.fit} {node} {prop}') |
| 42 | |
| 43 | def __fdt_get_binary(self, node, prop): |
| 44 | numbers = util.run_and_log(self.cons, |
| 45 | f'fdtget -tbi {self.fit} {node} {prop}') |
| 46 | |
| 47 | bignum = bytearray() |
| 48 | for little_num in numbers.split(): |
| 49 | bignum.append(int(little_num)) |
| 50 | |
| 51 | return bignum |
| 52 | |
| 53 | def build_nodes_sets(self): |
| 54 | """Fill sets with FIT images and configurations subnodes.""" |
| 55 | for node in self.__fdt_list('/images').split(): |
| 56 | subnode = f'/images/{node}' |
| 57 | self.images_nodes.add(subnode) |
| 58 | |
| 59 | for node in self.__fdt_list('/configurations').split(): |
| 60 | subnode = f'/configurations/{node}' |
| 61 | self.confgs_nodes.add(subnode) |
| 62 | |
| 63 | return len(self.images_nodes) + len(self.confgs_nodes) |
| 64 | |
| 65 | def check_fit_crc32_images(self): |
| 66 | """Test that all images in the set are hashed as expected. |
| 67 | |
| 68 | Each image must have an hash with algo=crc32 and hash value must match |
| 69 | the one calculated over image data. |
| 70 | """ |
| 71 | for node in self.images_nodes: |
| 72 | algo = self.__fdt_get_string(f'{node}/hash', 'algo') |
| 73 | assert algo == "crc32\n", "Missing expected crc32 image hash!" |
| 74 | |
| 75 | raw_crc32 = self.__fdt_get_binary(f'{node}/hash', 'value') |
| 76 | raw_bin = self.__fdt_get_binary(node, 'data') |
| 77 | assert raw_crc32 == (binascii.crc32(raw_bin) & |
| 78 | 0xffffffff).to_bytes(4, 'big'), "Wrong crc32 hash!" |
| 79 | |
| 80 | def check_fit_signed_images(self, key_name, sign_algo, verifier): |
| 81 | """Test that all images in the set are signed as expected. |
| 82 | |
| 83 | Each image must have a signature with: key-name-hint matching key_name |
| 84 | argument; algo matching sign_algo argument; value matching the one |
| 85 | calculated over image data using verifier argument. |
| 86 | """ |
| 87 | for node in self.images_nodes: |
| 88 | hint = self.__fdt_get_string(f'{node}/signature', 'key-name-hint') |
| 89 | assert hint == key_name + "\n", "Missing expected key name hint!" |
| 90 | algo = self.__fdt_get_string(f'{node}/signature', 'algo') |
| 91 | assert algo == sign_algo + "\n", "Missing expected signature algo!" |
| 92 | |
| 93 | raw_sig = self.__fdt_get_binary(f'{node}/signature', 'value') |
| 94 | raw_bin = self.__fdt_get_binary(node, 'data') |
| 95 | verifier.verify(SHA256.new(raw_bin), bytes(raw_sig)) |
| 96 | |
| 97 | def check_fit_signed_confgs(self, key_name, sign_algo): |
| 98 | """Test that all configs are signed, and images hashed, as expected. |
| 99 | |
| 100 | Each image must have an hash with algo=sha1 and hash value must match |
| 101 | the one calculated over image data. Each configuration must have a |
| 102 | signature with key-name-hint matching key_name argument and algo |
| 103 | matching sign_algo argument. |
| 104 | TODO: configurations signature checking. |
| 105 | """ |
| 106 | for node in self.images_nodes: |
| 107 | algo = self.__fdt_get_string(f'{node}/hash', 'algo') |
| 108 | assert algo == "sha1\n", "Missing expected sha1 image hash!" |
| 109 | |
| 110 | raw_hash = self.__fdt_get_binary(f'{node}/hash', 'value') |
| 111 | raw_bin = self.__fdt_get_binary(node, 'data') |
| 112 | assert raw_hash == SHA1.new(raw_bin).digest(), "Wrong sha1 hash!" |
| 113 | |
| 114 | for node in self.confgs_nodes: |
| 115 | hint = self.__fdt_get_string(f'{node}/signature', 'key-name-hint') |
| 116 | assert hint == key_name + "\n", "Missing expected key name hint!" |
| 117 | algo = self.__fdt_get_string(f'{node}/signature', 'algo') |
| 118 | assert algo == sign_algo + "\n", "Missing expected signature algo!" |
| 119 | |
| 120 | |
| 121 | @pytest.mark.buildconfigspec('fit_signature') |
| 122 | @pytest.mark.requiredtool('fdtget') |
| 123 | def test_fit_auto_signed(u_boot_console): |
| 124 | """Test that mkimage generates auto-FIT with signatures/hashes as expected. |
| 125 | |
| 126 | The mkimage tool can create auto generated (i.e. without an ITS file |
| 127 | provided as input) FIT in three different flavours: with crc32 checksums |
| 128 | of 'images' subnodes; with signatures of 'images' subnodes; with sha1 |
| 129 | hashes of 'images' subnodes and signatures of 'configurations' subnodes. |
| 130 | This test verifies that auto-FIT are generated as expected, in all of |
| 131 | the three flavours, including check of hashes and signatures (except for |
| 132 | configurations ones). |
| 133 | |
| 134 | The test does not run the sandbox. It only checks the host tool mkimage. |
| 135 | """ |
| 136 | cons = u_boot_console |
| 137 | mkimage = cons.config.build_dir + '/tools/mkimage' |
| 138 | tempdir = os.path.join(cons.config.result_dir, 'auto_fit') |
| 139 | os.makedirs(tempdir, exist_ok=True) |
| 140 | kernel_file = f'{tempdir}/vmlinuz' |
| 141 | dt1_file = f'{tempdir}/dt-1.dtb' |
| 142 | dt2_file = f'{tempdir}/dt-2.dtb' |
| 143 | key_name = 'sign-key' |
| 144 | sign_algo = 'sha256,rsa4096' |
| 145 | key_file = f'{tempdir}/{key_name}.key' |
| 146 | fit_file = f'{tempdir}/test.fit' |
| 147 | |
| 148 | # Create a fake kernel image and two dtb files with random data |
| 149 | with open(kernel_file, 'wb') as fd: |
| 150 | fd.write(os.urandom(512)) |
| 151 | |
| 152 | with open(dt1_file, 'wb') as fd: |
| 153 | fd.write(os.urandom(256)) |
| 154 | |
| 155 | with open(dt2_file, 'wb') as fd: |
| 156 | fd.write(os.urandom(256)) |
| 157 | |
| 158 | # Create 4096 RSA key and write to file to be read by mkimage |
| 159 | key = RSA.generate(bits=4096) |
| 160 | verifier = pkcs1_15.new(key) |
| 161 | |
| 162 | with open(key_file, 'w') as fd: |
| 163 | fd.write(str(key.export_key(format='PEM').decode('ascii'))) |
| 164 | |
| 165 | b_args = " -d" + kernel_file + " -b" + dt1_file + " -b" + dt2_file |
| 166 | s_args = " -k" + tempdir + " -g" + key_name + " -o" + sign_algo |
| 167 | |
| 168 | # 1 - Create auto FIT with images crc32 checksum, and verify it |
| 169 | util.run_and_log(cons, mkimage + ' -fauto' + b_args + " " + fit_file) |
| 170 | |
| 171 | fit = SignedFitHelper(cons, fit_file) |
| 172 | if fit.build_nodes_sets() == 0: |
| 173 | raise ValueError('FIT-1 has no "/image" nor "/configuration" nodes') |
| 174 | |
| 175 | fit.check_fit_crc32_images() |
| 176 | |
| 177 | # 2 - Create auto FIT with signed images, and verify it |
| 178 | util.run_and_log(cons, mkimage + ' -fauto' + b_args + s_args + " " + |
| 179 | fit_file) |
| 180 | |
| 181 | fit = SignedFitHelper(cons, fit_file) |
| 182 | if fit.build_nodes_sets() == 0: |
| 183 | raise ValueError('FIT-2 has no "/image" nor "/configuration" nodes') |
| 184 | |
| 185 | fit.check_fit_signed_images(key_name, sign_algo, verifier) |
| 186 | |
| 187 | # 3 - Create auto FIT with signed configs and hashed images, and verify it |
| 188 | util.run_and_log(cons, mkimage + ' -fauto-conf' + b_args + s_args + " " + |
| 189 | fit_file) |
| 190 | |
| 191 | fit = SignedFitHelper(cons, fit_file) |
| 192 | if fit.build_nodes_sets() == 0: |
| 193 | raise ValueError('FIT-3 has no "/image" nor "/configuration" nodes') |
| 194 | |
| 195 | fit.check_fit_signed_confgs(key_name, sign_algo) |