|
| 1 | +# Copyright lowRISC contributors (OpenTitan project). |
| 2 | +# Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | + |
| 5 | +from sw.host.penetrationtests.python.fi.host_scripts import fi_asym_cryptolib_functions |
| 6 | +from sw.host.penetrationtests.python.fi.communication.fi_asym_cryptolib_commands import ( |
| 7 | + OTFIAsymCrypto, |
| 8 | +) |
| 9 | +from python.runfiles import Runfiles |
| 10 | +from sw.host.penetrationtests.python.util import targets |
| 11 | +from sw.host.penetrationtests.python.util import utils |
| 12 | +from sw.host.penetrationtests.python.util import common_library |
| 13 | +from sw.host.penetrationtests.python.util.gdb_controller import GDBController |
| 14 | +import json |
| 15 | +import unittest |
| 16 | +import argparse |
| 17 | +import sys |
| 18 | +import random |
| 19 | +from Crypto.PublicKey import RSA, ECC |
| 20 | +from Crypto.Signature import pkcs1_15, DSS |
| 21 | +from Crypto.Hash import SHA256, SHA384 |
| 22 | + |
| 23 | +import subprocess |
| 24 | +import os |
| 25 | +import sys |
| 26 | +import time |
| 27 | + |
| 28 | +ignored_keys_set = set(["status"]) |
| 29 | +opentitantool_path = "" |
| 30 | +iterations = 1 |
| 31 | +repetitions = 3 |
| 32 | + |
| 33 | +target = None |
| 34 | +asymfi = None |
| 35 | + |
| 36 | +# Read in the extra arguments from the opentitan_test. |
| 37 | +parser = argparse.ArgumentParser() |
| 38 | +parser.add_argument("--bitstream", type=str) |
| 39 | +parser.add_argument("--bootstrap", type=str) |
| 40 | + |
| 41 | +args, config_args = parser.parse_known_args() |
| 42 | + |
| 43 | +BITSTREAM = args.bitstream |
| 44 | +BOOTSTRAP = args.bootstrap |
| 45 | + |
| 46 | + |
| 47 | +# Preparing the input for an invalid signature |
| 48 | +key = ECC.generate(curve="P-384") |
| 49 | +pubx = [x for x in key.pointQ.x.to_bytes(48, "little")] |
| 50 | +puby = [x for x in key.pointQ.y.to_bytes(48, "little")] |
| 51 | +message = [i for i in range(16)] |
| 52 | +h = SHA384.new(bytes(message)) |
| 53 | +signer = DSS.new(key, "fips-186-3") |
| 54 | +signature = [x for x in signer.sign(h)] |
| 55 | +# Corrupted the signature for FiSim Testing |
| 56 | +signature[0] ^= 0x1 |
| 57 | +r_bytes = signature[:48] |
| 58 | +s_bytes = signature[48:] |
| 59 | +r_bytes.reverse() |
| 60 | +s_bytes.reverse() |
| 61 | +cfg = 0 |
| 62 | +trigger = 1 |
| 63 | +h = SHA384.new(bytes(message)) |
| 64 | +message_digest = [x for x in h.digest()] |
| 65 | + |
| 66 | + |
| 67 | +def trigger_testos_init(print_output=True): |
| 68 | + # Initializing the testOS (setting up the alerts and accelerators) |
| 69 | + device_id, sensors, alerts, owner_page, boot_log, boot_measurements, version = asymfi.init( |
| 70 | + alert_config=common_library.default_fpga_friendly_alert_config |
| 71 | + ) |
| 72 | + if print_output: |
| 73 | + print("Output from init ", device_id) |
| 74 | + |
| 75 | + |
| 76 | +def trigger_p384_verify(): |
| 77 | + asymfi.handle_p384_verify(pubx, puby, r_bytes, s_bytes, message_digest, cfg, trigger) |
| 78 | + |
| 79 | + |
| 80 | +def read_testos_output(): |
| 81 | + # Read the output from the operation |
| 82 | + response = target.read_response(max_tries=500) |
| 83 | + return response |
| 84 | + |
| 85 | + |
| 86 | +if __name__ == "__main__": |
| 87 | + r = Runfiles.Create() |
| 88 | + # Get the openocd path. |
| 89 | + openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd") |
| 90 | + # Get the openocd config files. |
| 91 | + # The first file is on the cw340 (this is specific to the cw340) |
| 92 | + CONFIG_FILE_CHIP = r.Rlocation("lowrisc_opentitan/util/openocd/board/cw340_ftdi.cfg") |
| 93 | + # The config for the earlgrey design |
| 94 | + CONFIG_FILE_DESIGN = r.Rlocation("lowrisc_opentitan/util/openocd/target/lowrisc-earlgrey.cfg") |
| 95 | + # Get the opentitantool path. |
| 96 | + opentitantool_path = r.Rlocation("lowrisc_opentitan/sw/host/opentitantool/opentitantool") |
| 97 | + # The path for GDB and the default port (set up by OpenOCD) |
| 98 | + # TODO change to something reliable |
| 99 | + GDB_PATH = "/opt/riscv/bin//riscv32-unknown-elf-gdb" |
| 100 | + GDB_PORT = 3333 |
| 101 | + # Directory for the trace log files |
| 102 | + log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR") |
| 103 | + pc_trace_file = os.path.join(log_dir, "pc_trace.log") |
| 104 | + # Directory for the output results |
| 105 | + test_results_file = os.path.join(log_dir, "test_results.log") |
| 106 | + # Program the bitstream for FPGAs. |
| 107 | + bitstream_path = None |
| 108 | + if BITSTREAM: |
| 109 | + bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM) |
| 110 | + # Get the firmware path. |
| 111 | + firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP) |
| 112 | + # Get the disassembly path. |
| 113 | + dis_path = firmware_path.replace(".img", ".dis") |
| 114 | + # And the path for the elf. |
| 115 | + elf_path = firmware_path.replace(".img", ".elf") |
| 116 | + |
| 117 | + if "fpga" in BOOTSTRAP: |
| 118 | + target_type = "fpga" |
| 119 | + else: |
| 120 | + target_type = "chip" |
| 121 | + |
| 122 | + target_cfg = targets.TargetConfig( |
| 123 | + target_type=target_type, |
| 124 | + interface_type="hyperdebug", |
| 125 | + fw_bin=firmware_path, |
| 126 | + opentitantool=opentitantool_path, |
| 127 | + bitstream=bitstream_path, |
| 128 | + tool_args=config_args, |
| 129 | + openocd=openocd_path, |
| 130 | + openocd_chip_config=CONFIG_FILE_CHIP, |
| 131 | + openocd_design_config=CONFIG_FILE_DESIGN, |
| 132 | + ) |
| 133 | + |
| 134 | + target = targets.Target(target_cfg) |
| 135 | + asymfi = OTFIAsymCrypto(target) |
| 136 | + successful_faults = 0 |
| 137 | + |
| 138 | + # How to read outputs in this script: |
| 139 | + # To view the UART output from the testOS or the chip in general, use: target.print_all() or print(read_testos_output()) |
| 140 | + # In order to print the OpenOCD output use print(target.read_openocd()) |
| 141 | + # In order to print the output from GDB use print(gdb.read_output()) or when you want to know the output from a gdb.send_command() print it: print(gdb.send_command()) |
| 142 | + |
| 143 | + try: |
| 144 | + # Program the bitstream, flash the target, and set up OpenOCD |
| 145 | + target.initialize_target() |
| 146 | + |
| 147 | + # Initialize the testOS |
| 148 | + trigger_testos_init() |
| 149 | + |
| 150 | + # Connect to GDB |
| 151 | + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) |
| 152 | + |
| 153 | + # Provide the function name and extract the start and end address from the dis file |
| 154 | + function_name = "OUTLINED_FUNCTION_22" |
| 155 | + # Gives back an array of hits where the function is called |
| 156 | + addresses = gdb.get_function_addresses(dis_path, function_name) |
| 157 | + print("Start and stop addresses of ", function_name, ": ", addresses, flush=True) |
| 158 | + |
| 159 | + # Start the tracing |
| 160 | + gdb.send_command("-exec-interrupt") |
| 161 | + # We pick the second address hit |
| 162 | + gdb.setup_pc_trace(pc_trace_file, addresses[1][0], addresses[1][1]) |
| 163 | + print("setting trace done ... ", flush=True) |
| 164 | + # Remove the output so far |
| 165 | + gdb.dump_output() |
| 166 | + gdb.send_command("-exec-continue") |
| 167 | + |
| 168 | + # Trigger the p384 verify from the testOS (we do not read its output) |
| 169 | + trigger_p384_verify() |
| 170 | + |
| 171 | + # We set a minute as timeout |
| 172 | + timeout = 60 |
| 173 | + start_time = time.time() |
| 174 | + stopped = False |
| 175 | + |
| 176 | + # Run the tracing to get the trace log |
| 177 | + while time.time() - start_time < timeout: |
| 178 | + output = gdb.read_output(timeout=0.5) |
| 179 | + if "PC trace complete" in output: |
| 180 | + print("\nTrace complete", flush=True) |
| 181 | + stopped = True |
| 182 | + break |
| 183 | + if not stopped: |
| 184 | + print("No final break point found, stopping") |
| 185 | + sys.exit(1) |
| 186 | + |
| 187 | + # Parse the trace log to get all PCs in a list |
| 188 | + pc_list = gdb.parse_pc_trace_file(pc_trace_file) |
| 189 | + print("Tracing has a total of", len(pc_list), "PCs", flush=True) |
| 190 | + |
| 191 | + if len(pc_list) <= 0: |
| 192 | + print("Found no tracing, stopping") |
| 193 | + sys.exit(1) |
| 194 | + |
| 195 | + # Reset the connection of GDB |
| 196 | + gdb.close_gdb() |
| 197 | + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) |
| 198 | + |
| 199 | + # Reset the target, flush the output, and init again |
| 200 | + gdb.reset_target() |
| 201 | + target.dump_all() |
| 202 | + trigger_testos_init(print_output=False) |
| 203 | + # Flush GDB and OpenOCD |
| 204 | + openocd_response = target.read_openocd() |
| 205 | + gdb.close_gdb() |
| 206 | + |
| 207 | + # Open the results file |
| 208 | + test_results = open(test_results_file, "w") |
| 209 | + test_results.write(f"Listing successful skips:\n") |
| 210 | + |
| 211 | + idx = 0 |
| 212 | + |
| 213 | + while idx < len(pc_list) - 1: |
| 214 | + print("-" * 80) |
| 215 | + print("Applying instruction skip in ", pc_list[idx], flush=True) |
| 216 | + print("-" * 80) |
| 217 | + |
| 218 | + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT) |
| 219 | + gdb.dump_output() |
| 220 | + |
| 221 | + gdb.send_command("-exec-interrupt") |
| 222 | + # TODO Instead of giving pc_list[idx + 1], parse the dis and go to the next instruction |
| 223 | + # TODO Provide observation points such that GDB can check the state instead of relying on UART |
| 224 | + gdb.apply_instruction_skip(pc_list[idx], pc_list[idx + 1]) |
| 225 | + gdb.send_command("-exec-continue") |
| 226 | + |
| 227 | + # The instruction skip loop |
| 228 | + trigger_p384_verify() |
| 229 | + # TODO Rewrite this to rely only on GDB instead of on UART |
| 230 | + time.sleep(0.02) |
| 231 | + testos_response = read_testos_output() |
| 232 | + try: |
| 233 | + gdb_response = gdb.read_output() |
| 234 | + if "instruction skip applied" in gdb_response: |
| 235 | + idx += 1 |
| 236 | + print("Instruction skip applied", flush=True) |
| 237 | + |
| 238 | + testos_response_json = json.loads(testos_response) |
| 239 | + if testos_response_json["result"] == "True": |
| 240 | + successful_faults += 1 |
| 241 | + print("-" * 80, flush=True) |
| 242 | + print("Successful FI attack!", flush=True) |
| 243 | + print("Response:", testos_response_json) |
| 244 | + print("Location:", pc_list[idx]) |
| 245 | + print("-" * 80, flush=True) |
| 246 | + test_results.write(f"{pc_list[idx]}: {testos_response_json}\n") |
| 247 | + else: |
| 248 | + print( |
| 249 | + "Unsuccessful attack, output was: ", |
| 250 | + testos_response_json, |
| 251 | + flush=True, |
| 252 | + ) |
| 253 | + else: |
| 254 | + print("No break point found, something went wrong", flush=True) |
| 255 | + |
| 256 | + gdb.close_gdb() # 0.5s |
| 257 | + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) # 0.1s |
| 258 | + gdb.reset_target() # 0.01s |
| 259 | + target.dump_all() # 1.35s |
| 260 | + trigger_testos_init(print_output=False) # 0.4s |
| 261 | + gdb.close_gdb() # 0.5s |
| 262 | + except json.JSONDecodeError as e: |
| 263 | + print(f"Error: JSON decoding failed. Invalid response format.", flush=True) |
| 264 | + gdb.close_gdb() |
| 265 | + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) |
| 266 | + gdb.reset_target() |
| 267 | + target.dump_all() |
| 268 | + trigger_testos_init(print_output=False) |
| 269 | + gdb.close_gdb() |
| 270 | + except KeyError as e: |
| 271 | + print(f"Error: Required key {e} is missing in the JSON response.", flush=True) |
| 272 | + gdb.close_gdb() |
| 273 | + gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path) |
| 274 | + gdb.reset_target() |
| 275 | + target.dump_all() |
| 276 | + trigger_testos_init(print_output=False) |
| 277 | + gdb.close_gdb() |
| 278 | + |
| 279 | + finally: |
| 280 | + print("-" * 80) |
| 281 | + print("Trace data is logged in ", pc_trace_file, flush=True) |
| 282 | + print("Instruction skip results are logged in ", test_results_file, flush=True) |
| 283 | + print(f"There were a total of {successful_faults} successful faults", flush=True) |
| 284 | + print("You can find the dissassembly in ", dis_path, flush=True) |
| 285 | + # Close the OpenOCD and GDB connection at the end |
| 286 | + target.close_openocd() |
0 commit comments