Slava Andrianov | 192ee17 | 2025-06-11 15:40:43 -0500 | [diff] [blame^] | 1 | #!/usr/bin/env python3 |
| 2 | # |
| 3 | # Copyright (c) 2025 Arm Limited. All rights reserved. |
| 4 | # |
| 5 | # SPDX-License-Identifier: BSD-3-Clause |
| 6 | # |
| 7 | |
| 8 | # This script compares the hashes in the TFA event log against the files |
| 9 | # generated by the build system, or in the case of the startup locality and |
| 10 | # critical data, the default values. |
| 11 | |
| 12 | import os |
| 13 | import hashlib |
| 14 | from enum import Enum |
| 15 | from dataclasses import dataclass |
| 16 | |
| 17 | # Stores identifiers in the log associated with the different images |
| 18 | class ImageType(Enum): |
| 19 | UNKNOWN = "UNKNOWN" |
| 20 | BL2 = "BL_2" |
| 21 | BL31 = "SECURE_RT_EL3" |
| 22 | NT_FW_CONFIG = "NT_FW_CONFIG" |
| 23 | TB_FW_CONFIG = "TB_FW_CONFIG" |
| 24 | SOC_FW_CONFIG = "SOC_FW_CONFIG" |
| 25 | FW_CONFIG = "FW_CONFIG" |
| 26 | BL33 = "BL_33" |
| 27 | BL32 = "SECURE_RT_EL1" |
| 28 | BL32_EXTRA1 = "EXTRA1" |
| 29 | BL32_EXTRA2 = "EXTRA2" |
| 30 | STARTUP_LOCALITY = "StartupLocality" |
| 31 | CRITICAL_DATA = "CRITICAL DATA" |
| 32 | |
| 33 | marker_to_image_type = { |
| 34 | "BL_2" : ImageType.BL2, |
| 35 | "SECURE_RT_EL3" : ImageType.BL31, |
| 36 | "NT_FW_CONFIG" : ImageType.NT_FW_CONFIG, |
| 37 | "TB_FW_CONFIG" : ImageType.TB_FW_CONFIG, |
| 38 | "SOC_FW_CONFIG" : ImageType.SOC_FW_CONFIG, |
| 39 | "FW_CONFIG" : ImageType.FW_CONFIG, |
| 40 | "BL_33" : ImageType.BL33, |
| 41 | "StartupLocality" : ImageType.STARTUP_LOCALITY, |
| 42 | "CRITICAL DATA" : ImageType.CRITICAL_DATA, |
| 43 | } |
| 44 | |
| 45 | class HashType(Enum): |
| 46 | UNKNOWN = "UNKNOWN" |
| 47 | SHA256 = "SHA256" |
| 48 | SHA384 = "SHA384" |
| 49 | |
| 50 | PCR_EVENT_MARKER = "PCR_Event2" |
| 51 | ALGORITHM_MARKER = "AlgorithmId" |
| 52 | DIGEST_MARKER = "Digest " |
| 53 | EVENT_SIZE_MARKER = "EventSize" |
| 54 | EVENT_TYPE_MARKER = "Event " |
| 55 | |
| 56 | BUF_SIZE = 65536 |
| 57 | |
| 58 | # On FVPs, critical data is a hash of the non volatile registers which FVPs |
| 59 | # do not alter. These registers have the below default values. They are stored |
| 60 | # together in a struct which is then hashed, so this is replicated here |
| 61 | TFW_NVCTR_VAL = 31 |
| 62 | NTFW_NVCTR_VAL = 223 |
| 63 | COMBINED_NVCTRS = (NTFW_NVCTR_VAL << 32) | TFW_NVCTR_VAL |
| 64 | COMBINED_NVCTR_BYTES = COMBINED_NVCTRS.to_bytes(8, byteorder="little") |
| 65 | |
| 66 | # Need to know the location of the built files to verify their hashes |
| 67 | artefacts_dir = os.environ["artefacts_dir"] |
| 68 | out_file_path = f"{artefacts_dir}/tfa_event_log" |
| 69 | |
| 70 | # This is needed to correctly identify the files associated with BL32 and BL33 |
| 71 | # as the names of these can vary by test |
| 72 | build_args_path = f"{artefacts_dir}/fip_build_args" |
| 73 | |
| 74 | # Structure: |
| 75 | # - boolean for if an entry for this image has been found in the event log |
| 76 | # - path to the built file |
| 77 | # - hash type collected from the event log |
| 78 | # - hash of the file from the event log |
| 79 | @dataclass |
| 80 | class ImageData: |
| 81 | found: bool |
| 82 | path: str |
| 83 | hash_type: HashType |
| 84 | event_log_hash: str |
| 85 | |
| 86 | def __init__(self, initial_path: str): |
| 87 | self.found = False |
| 88 | self.path = initial_path |
| 89 | self.hash_type = HashType.UNKNOWN |
| 90 | self.event_log_hash = "" |
| 91 | |
| 92 | # For convenience |
| 93 | def as_tuple(self): |
| 94 | return (self.found, self.path, self.hash_type, self.event_log_hash) |
| 95 | |
| 96 | # As event log entries for these images are found, their data will be stored |
| 97 | # inside of the objects in this dictionary |
| 98 | image_data = { |
| 99 | ImageType.BL2 : ImageData(f"{artefacts_dir}/bl2.bin"), |
| 100 | ImageType.BL31 : ImageData(f"{artefacts_dir}/bl31.bin"), |
| 101 | ImageType.FW_CONFIG : ImageData(f"{artefacts_dir}/fvp_fw_config.dtb"), |
| 102 | ImageType.TB_FW_CONFIG : ImageData(f"{artefacts_dir}/fvp_tb_fw_config.dtb"), |
| 103 | ImageType.NT_FW_CONFIG : ImageData(f"{artefacts_dir}/fvp_nt_fw_config.dtb"), |
| 104 | ImageType.SOC_FW_CONFIG : ImageData(f"{artefacts_dir}/fvp_soc_fw_config.dtb"), |
| 105 | ImageType.BL33 : ImageData(""), |
| 106 | ImageType.BL32 : ImageData(""), |
| 107 | ImageType.BL32_EXTRA1 : ImageData(""), |
| 108 | ImageType.BL32_EXTRA2 : ImageData(""), |
| 109 | ImageType.STARTUP_LOCALITY : ImageData(""), |
| 110 | ImageType.CRITICAL_DATA : ImageData(""), |
| 111 | } |
| 112 | |
| 113 | # Sometimes alternate paths are provided for some of the images used in the |
| 114 | # FIP, so these need to be checked for and stored in the image_data dictionary |
| 115 | def get_build_arg_paths(): |
| 116 | build_data = "" |
| 117 | with open(build_args_path, 'r') as f: |
| 118 | build_data = f.read() |
| 119 | |
| 120 | components = build_data.split() |
| 121 | for comp in components: |
| 122 | split_point = comp.find('=') |
| 123 | name = comp[0:split_point] |
| 124 | path_value = comp[(split_point + 1):] |
| 125 | image_type = ImageType.UNKNOWN |
| 126 | if name == "BL33": |
| 127 | image_type = ImageType.BL33 |
| 128 | elif "BL32" in name: |
| 129 | if "EXTRA1" in name: |
| 130 | image_type = ImageType.BL32_EXTRA1 |
| 131 | elif "EXTRA2" in name: |
| 132 | image_type = ImageType.BL32_EXTRA2 |
| 133 | else: |
| 134 | image_type = ImageType.BL32 |
| 135 | |
| 136 | if image_type != ImageType.UNKNOWN: |
| 137 | image_data[image_type].path = path_value |
| 138 | |
| 139 | # BL32 can show up as its own binary if it is not diven a different name in |
| 140 | # the build file |
| 141 | if (image_data[ImageType.BL32].path == "") and os.path.exists(f"{artefacts_dir}/bl32.bin"): |
| 142 | image_data[ImageType.BL32].path = f"{artefacts_dir}/bl32.bin" |
| 143 | |
| 144 | |
| 145 | # Only found images should have their hashes compared |
| 146 | found_images = [] |
| 147 | |
| 148 | # Get the hash of the file stored at the given path with the specified hash |
| 149 | # algorithm |
| 150 | def calc_file_hash(path: str, hash_type: HashType) -> str: |
| 151 | |
| 152 | if hash_type == HashType.UNKNOWN: |
| 153 | return "" |
| 154 | |
| 155 | if (path == ""): |
| 156 | return "No path provided" |
| 157 | |
| 158 | if not os.path.exists(path): |
| 159 | return f"No file available at path: {path}" |
| 160 | |
| 161 | # Need to use this because the Docker image used for CI uses Python 3.10 |
| 162 | # so hashlib.file_digest() can't be used |
| 163 | hasher = hashlib.new(hash_type.value.lower()) |
| 164 | with open(path, "rb") as bin_file: |
| 165 | while True: |
| 166 | file_data = bin_file.read(BUF_SIZE) |
| 167 | if not file_data: # EOF |
| 168 | break; |
| 169 | hasher.update(file_data) |
| 170 | |
| 171 | return hasher.hexdigest() |
| 172 | |
| 173 | # For a event log entry, extract the hash algorithm used and the hash for this# entry |
| 174 | def extract_hash(line: str, tfa_event_log_file) -> (str, HashType, ImageType): |
| 175 | |
| 176 | # This skips over the PCR index and event type later these lines should be |
| 177 | # parsed and used to calculate the PCR value |
| 178 | while not ALGORITHM_MARKER in line: |
| 179 | line = tfa_event_log_file.readline() |
| 180 | |
| 181 | hash_type = HashType.UNKNOWN |
| 182 | for ht in HashType: |
| 183 | if ht.value in line: |
| 184 | hash_type = ht |
| 185 | break |
| 186 | |
| 187 | # Early return for now if other hash type |
| 188 | if hash_type == HashType.UNKNOWN: |
| 189 | return ("", hash_type, ImageType.UNKNOWN) |
| 190 | |
| 191 | # Storing lines which contain the hash characters |
| 192 | digest_lines = [] |
| 193 | line = tfa_event_log_file.readline() |
| 194 | if not DIGEST_MARKER in line: |
| 195 | return ("", hash_type, ImageType.UNKNOWN) |
| 196 | |
| 197 | while not EVENT_SIZE_MARKER in line: |
| 198 | digest_lines.append(line) |
| 199 | line = tfa_event_log_file.readline() |
| 200 | |
| 201 | # This line contains the event type |
| 202 | line = tfa_event_log_file.readline() |
| 203 | # This will get to the first char of the name of the image |
| 204 | sep_ind = line.find(':') + 2 |
| 205 | event_substr = line[sep_ind:-1] |
| 206 | image_type = ImageType.UNKNOWN |
| 207 | |
| 208 | if event_substr in marker_to_image_type: |
| 209 | image_type = marker_to_image_type[event_substr] |
| 210 | elif ImageType.BL32.value in event_substr: |
| 211 | if ImageType.BL32_EXTRA1.value in event_substr: |
| 212 | image_type = ImageType.BL32_EXTRA1 |
| 213 | elif ImageType.BL32_EXTRA2.value in event_substr: |
| 214 | image_type = ImageType.BL32_EXTRA2 |
| 215 | else: |
| 216 | image_type = ImageType.BL32 |
| 217 | |
| 218 | if image_type == ImageType.UNKNOWN: |
| 219 | return ("", hash_type, ImageType.UNKNOWN) |
| 220 | |
| 221 | # Know that its one of the images that we want to know the hash of so can |
| 222 | # proceed with extracting the hash |
| 223 | hash = "" |
| 224 | for digest_line in digest_lines: |
| 225 | sep_ind = digest_line.find(" : ") |
| 226 | # + 3 to skip past the separator |
| 227 | component = digest_line[sep_ind + 3:].strip().replace(' ', '') |
| 228 | hash += component |
| 229 | |
| 230 | return (hash, hash_type, image_type) |
| 231 | |
| 232 | |
| 233 | # Update image data map with paths to BL33 and BL32 binaries |
| 234 | get_build_arg_paths() |
| 235 | |
| 236 | with open(out_file_path, "r") as tfa_event_log_file: |
| 237 | line = tfa_event_log_file.readline() |
| 238 | while len(line) > 0: |
| 239 | # Found at the start of a event log entry |
| 240 | if PCR_EVENT_MARKER in line: |
| 241 | hash, hash_type, image_type = extract_hash(line, tfa_event_log_file) |
| 242 | |
| 243 | if image_type != ImageType.UNKNOWN: |
| 244 | image_data[image_type].found = True |
| 245 | image_data[image_type].hash_type = hash_type |
| 246 | image_data[image_type].event_log_hash = hash |
| 247 | found_images.append(image_type) |
| 248 | |
| 249 | line = tfa_event_log_file.readline() |
| 250 | |
| 251 | all_match = True |
| 252 | for image_type in found_images: |
| 253 | present, file_path, hash_type, event_log_hash = image_data[image_type].as_tuple() |
| 254 | comparison_hash = "" |
| 255 | if image_type == ImageType.STARTUP_LOCALITY: |
| 256 | if int(event_log_hash) == 0: |
| 257 | comparison_hash = event_log_hash |
| 258 | else: |
| 259 | comparison_hash = "0" |
| 260 | |
| 261 | elif image_type == ImageType.CRITICAL_DATA: |
| 262 | hasher = hashlib.new(hash_type.value.lower()) |
| 263 | hasher.update(COMBINED_NVCTR_BYTES) |
| 264 | comparison_hash = hasher.hexdigest() |
| 265 | else: |
| 266 | comparison_hash = calc_file_hash(file_path, hash_type) |
| 267 | |
| 268 | print(f"{image_type.name} hash algo: {hash_type.value}") |
| 269 | print(f"Event log hash: {event_log_hash}\nComparison hash: {comparison_hash}") |
| 270 | if comparison_hash != event_log_hash: |
| 271 | print("Mismatched hashes") |
| 272 | all_match = False |
| 273 | |
| 274 | # These two must always be present |
| 275 | if not image_data[ImageType.BL2].found: |
| 276 | print("BL2 hash not found") |
| 277 | |
| 278 | if not image_data[ImageType.BL31].found: |
| 279 | print("BL31 hash not found") |
| 280 | |
| 281 | if all_match: |
| 282 | print("All found hashes match") |