Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions sw/device/tests/penetrationtests/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,17 @@ pentest_cryptolib_fi_asym(
test_vectors = [],
)

pentest_cryptolib_fi_asym(
name = "fi_asym_cryptolib_python_gdb_test",
tags = [
"manual",
"skip_in_ci",
],
test_args = "",
test_harness = "//sw/host/penetrationtests/python/fi:fi_asym_cryptolib_python_gdb_test",
test_vectors = [],
)

CRYPTOLIB_SCA_SYM_TESTVECTOR_TARGETS = [
"//sw/host/penetrationtests/testvectors/data:sca_sym_cryptolib",
]
Expand Down
21 changes: 21 additions & 0 deletions sw/host/penetrationtests/python/fi/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,27 @@ py_binary(
],
)

py_binary(
name = "fi_asym_cryptolib_python_gdb_test",
testonly = True,
srcs = ["gdb_testing/fi_asym_cryptolib_python_gdb_test.py"],
data = [
"//sw/host/opentitantool",
"//third_party/openocd:openocd_bin",
"//util/openocd/board:cw340_ftdi.cfg",
"//util/openocd/target:lowrisc-earlgrey.cfg",
"@lowrisc_rv32imcb_toolchain//:bin/riscv32-unknown-elf-gdb",
],
deps = [
"//sw/host/penetrationtests/python/fi:fi_asym_cryptolib_commands",
"//sw/host/penetrationtests/python/util:common_library",
"//sw/host/penetrationtests/python/util:gdb_controller",
"//sw/host/penetrationtests/python/util:targets",
"@rules_python//python/runfiles",
requirement("pycryptodome"),
],
)

py_library(
name = "fi_ibex_functions",
srcs = ["host_scripts/fi_ibex_functions.py"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
# Copyright lowRISC contributors (OpenTitan project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

from sw.host.penetrationtests.python.fi.communication.fi_asym_cryptolib_commands import (
OTFIAsymCrypto,
)
from python.runfiles import Runfiles
from sw.host.penetrationtests.python.util import targets
from sw.host.penetrationtests.python.util import common_library
from sw.host.penetrationtests.python.util.gdb_controller import GDBController
import json
import argparse
import sys
import os
import time
from Crypto.PublicKey import ECC
from Crypto.Signature import DSS
from Crypto.Hash import SHA384

ignored_keys_set = set(["status"])
opentitantool_path = ""

target = None
asymfi = None

# Read in the extra arguments from the opentitan_test.
parser = argparse.ArgumentParser()
parser.add_argument("--bitstream", type=str)
parser.add_argument("--bootstrap", type=str)

args, config_args = parser.parse_known_args()

BITSTREAM = args.bitstream
BOOTSTRAP = args.bootstrap


# Preparing the input for an invalid signature
key = ECC.generate(curve="P-384")
pubx = [x for x in key.pointQ.x.to_bytes(48, "little")]
puby = [x for x in key.pointQ.y.to_bytes(48, "little")]
message = [i for i in range(16)]
h = SHA384.new(bytes(message))
signer = DSS.new(key, "fips-186-3")
signature = [x for x in signer.sign(h)]
# Corrupt the signature for FiSim Testing
signature[0] ^= 0x1
r_bytes = signature[:48]
s_bytes = signature[48:]
r_bytes.reverse()
s_bytes.reverse()
cfg = 0
trigger = 1
h = SHA384.new(bytes(message))
message_digest = [x for x in h.digest()]


def trigger_testos_init(print_output=True):
# Initializing the testOS (setting up the alerts and accelerators)
(device_id, _, _, _, _, _, _) = asymfi.init(
alert_config=common_library.no_escalation_alert_config
)
if print_output:
print("Output from init ", device_id)


def trigger_p384_verify():
asymfi.handle_p384_verify(pubx, puby, r_bytes, s_bytes, message_digest, cfg, trigger)


def read_testos_output():
# Read the output from the operation
response = target.read_response(max_tries=500)
return response


if __name__ == "__main__":
r = Runfiles.Create()
# Get the openocd path.
openocd_path = r.Rlocation("lowrisc_opentitan/third_party/openocd/build_openocd/bin/openocd")
# Get the openocd config files.
# The first file is on the cw340 (this is specific to the cw340)
CONFIG_FILE_CHIP = r.Rlocation("lowrisc_opentitan/util/openocd/board/cw340_ftdi.cfg")
# The config for the earlgrey design
CONFIG_FILE_DESIGN = r.Rlocation("lowrisc_opentitan/util/openocd/target/lowrisc-earlgrey.cfg")
# Get the opentitantool path.
opentitantool_path = r.Rlocation("lowrisc_opentitan/sw/host/opentitantool/opentitantool")
# The path for GDB and the default port (set up by OpenOCD)
GDB_PATH = r.Rlocation("lowrisc_rv32imcb_toolchain/bin/riscv32-unknown-elf-gdb")
GDB_PORT = 3333
# Directory for the trace log files
log_dir = os.environ.get("TEST_UNDECLARED_OUTPUTS_DIR")
pc_trace_file = os.path.join(log_dir, "pc_trace.log")
# Directory for the output results
test_results_file = os.path.join(log_dir, "test_results.log")
test_results = open(test_results_file, "w")
# Program the bitstream for FPGAs.
bitstream_path = None
if BITSTREAM:
bitstream_path = r.Rlocation("lowrisc_opentitan/" + BITSTREAM)
# Get the firmware path.
firmware_path = r.Rlocation("lowrisc_opentitan/" + BOOTSTRAP)
# Get the disassembly path.
dis_path = firmware_path.replace(".img", ".dis")
# And the path for the elf.
elf_path = firmware_path.replace(".img", ".elf")

if "fpga" in BOOTSTRAP:
target_type = "fpga"
else:
target_type = "chip"

target_cfg = targets.TargetConfig(
target_type=target_type,
interface_type="hyperdebug",
fw_bin=firmware_path,
opentitantool=opentitantool_path,
bitstream=bitstream_path,
tool_args=config_args,
openocd=openocd_path,
openocd_chip_config=CONFIG_FILE_CHIP,
openocd_design_config=CONFIG_FILE_DESIGN,
)

target = targets.Target(target_cfg)
asymfi = OTFIAsymCrypto(target)
successful_faults = 0
total_attacks = 0

# How to read outputs in this script:
# To view the UART output from the testOS or the chip in general, use:
# target.print_all() or print(read_testos_output())
# In order to print the OpenOCD output use print(target.read_openocd())
# 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())

# What to do when running into errors:
# - If device is busy or seeing "rejected 'gdb' connection, no more connections allowed",
# cut the USB connection, e.g., sudo fuser /dev/ttyUSB0 and kill the PID
# - If the port is busy check sudo lsof -i :3333 and then kill the PID

try:
# Program the bitstream, flash the target, and set up OpenOCD
target.initialize_target()

# Initialize the testOS
trigger_testos_init()

# Connect to GDB
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)

# Provide the function name and extract the start and end address from the dis file
function_name = "otcrypto_ecdsa_p384_verify"
# function_name = "otcrypto_ecdsa_p384_verify"
# OUTLINED_FUNCTION_22 (a small debug function)
# Gives back an array of hits where the function is called
trace_addresses = gdb.get_function_addresses(dis_path, function_name)
print("Start and stop addresses of ", function_name, ": ", trace_addresses)

p384_observation_addresses = gdb.get_function_addresses(
dis_path, "otcrypto_ecdsa_p384_verify"
)
crash_observation_address = gdb.get_function_start_address(
dis_path, "ottf_exception_handler"
)

# Start the tracing
# We set a short timeout to detect whether GDB has connected properly
# and a long timeout for the entire tracing
initial_timeout = 10
total_timeout = 60 * 60 * 5

# We pick the second address hit
# TODO: Find a way to pick the correct one
gdb.setup_pc_trace(pc_trace_file, trace_addresses[1][0], trace_addresses[1][1])
gdb.send_command("c", check_response=False)

# Trigger the p384 verify from the testOS (we do not read its output)
trigger_p384_verify()

start_time = time.time()
initial_timeout_stopped = False
total_timeout_stopped = False

# Run the tracing to get the trace log
# Sometimes the tracing fails due to race conditions,
# we have a quick initial timeout to catch this
while time.time() - start_time < initial_timeout:
output = gdb.read_output()
if "breakpoint 1, " in output:
initial_timeout_stopped = True
break
if not initial_timeout_stopped:
print("No initial break point found, can be a misfire, try again")
sys.exit(1)
while time.time() - start_time < total_timeout:
output = gdb.read_output()
if "PC trace complete" in output:
print("\nTrace complete")
total_timeout_stopped = True
break
if not total_timeout_stopped:
print("Final tracing timeout reached")
sys.exit(1)

# Parse and truncate the trace log to get all PCs in a list
pc_list = gdb.parse_pc_trace_file(pc_trace_file)
# Get the unique PCs out
pc_list = list(set(pc_list))

# TODO: Make this truncate for wait cycles and identify the instance count ...
print("Tracing has a total of", len(pc_list), "PCs", flush=True)

if len(pc_list) <= 0:
print("Found no tracing, stopping")
sys.exit(1)

# Reset the target, flush the output, and close gdb
gdb.reset_target()
target.dump_all()
trigger_testos_init(print_output=False)
gdb.close_gdb()
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)

idx = 0
while idx < len(pc_list):
print("-" * 80)
print("Applying instruction skip in ", pc_list[idx])
print("-" * 80)

prefix_observation = "fisim_result:"
function_output_observation = "function output detected"
crash_observation = "crash detected"

try:
# The observation points
observations = {
# Function output
p384_observation_addresses[1][1]: f"{function_output_observation}",
# Crash check
crash_observation_address: f"{crash_observation}",
}
gdb.add_observation(observations)

# TODO Handle multiple same addresses
gdb.apply_instruction_skip(
pc_list[idx], gdb.parse_next_instruction(pc_list[idx], dis_path)
)
gdb.send_command("c", check_response=False)

# The instruction skip loop
trigger_p384_verify()
testos_response = read_testos_output()

gdb_response = gdb.read_output()
if "instruction skip applied" in gdb_response:
idx += 1
total_attacks += 1

if crash_observation in gdb_response:
print("Crash detected, resetting", flush=True)
gdb.close_gdb()
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)
gdb.reset_target()
target.dump_all()
trigger_testos_init(print_output=False)
# Reset again
gdb.close_gdb()
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)
elif function_output_observation in gdb_response:
testos_response_json = json.loads(testos_response)
print("Output:", testos_response_json, flush=True)
verification_result = testos_response_json["result"]
if verification_result:
successful_faults += 1
print("-" * 80)
print("Successful FI attack!")
print("Location:", pc_list[idx - 1])
print(gdb_response)
print("Response:", testos_response_json)
print("-" * 80)
test_results.write(f"{pc_list[idx - 1]}: {testos_response_json}\n")
# Reset GDB by closing and opening again
gdb.close_gdb()
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)
# We do not need to reset the target since it returned an output
else:
print(
"Firmware behaved unexpected, no crash or function output", flush=True
)
gdb.close_gdb()
target.close_openocd()
target.initialize_target()
trigger_testos_init()
target.dump_all()
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)
else:
print("No break point found, something went wrong", flush=True)
gdb.close_gdb()
target.close_openocd()
target.initialize_target()
trigger_testos_init()
target.dump_all()
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)

except json.JSONDecodeError:
print("Error: JSON decoding failed. Invalid response format", flush=True)
gdb.close_gdb()
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)
gdb.reset_target()
target.dump_all()
trigger_testos_init(print_output=False)
# Reset again
gdb.close_gdb()
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)

except TimeoutError as e:
print("Timeout error, retrying", flush=True)
print(e, flush=True)
gdb.close_gdb()
target.close_openocd()
target.initialize_target()
trigger_testos_init()
target.dump_all()
gdb = GDBController(gdb_path=GDB_PATH, gdb_port=GDB_PORT, elf_file=elf_path)

finally:
print("-" * 80)
print("Trace data is logged in ", pc_trace_file)
print("Instruction skip results are logged in ", test_results_file)
print(f"Total attacks {total_attacks}, successful attacks {successful_faults}")
print("You can find the dissassembly in ", dis_path)
# Close the OpenOCD and GDB connection at the end
target.close_openocd()
test_results.write(
f"Total attacks {total_attacks}, successful attacks {successful_faults}\n"
)
5 changes: 5 additions & 0 deletions sw/host/penetrationtests/python/util/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ py_library(
name = "hyperdebug",
srcs = ["hyperdebug.py"],
)

py_library(
name = "gdb_controller",
srcs = ["gdb_controller.py"],
)
Loading
Loading