Skip to content

Commit

Permalink
[fi/otp] Add OTP data read test to FI framework
Browse files Browse the repository at this point in the history
This adds code for FI pen-testing the OTP module.
The test sequence can be summarized in the following way:
  1. Read content from a selected OTP partition for comparison values
  2. Perform fault injection on nop instructions
  3. Read content from the same OTP partition again
  4. Compare output with previous output and check if values differ
The OTP partition to read can be selected through the "which_test"
parameter in the config file.

Signed-off-by: Moritz Wettermann <[email protected]>
  • Loading branch information
wettermo committed May 31, 2024
1 parent 263a8f9 commit ce42c95
Show file tree
Hide file tree
Showing 3 changed files with 411 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
target:
target_type: cw310
fpga_bitstream: "../objs/lowrisc_systems_chip_earlgrey_cw310_0.1.bit"
force_program_bitstream: False
fw_bin: "../objs/sca_ujson_fpga_cw310.bin"
output_len_bytes: 16
target_clk_mult: 0.24
target_freq: 24000000
baudrate: 115200
protocol: "ujson"
port: "/dev/ttyACM2"
fisetup:
fi_gear: "husky"
fi_type: "voltage_glitch"
parameter_generation: "random"
# Voltage glitch width in cycles.
glitch_width_min: 5
glitch_width_max: 150
glitch_width_step: 3
# Range for trigger delay in cycles.
trigger_delay_min: 0
trigger_delay_max: 500
trigger_step: 10
# Number of iterations for the parameter sweep.
num_iterations: 1
fiproject:
# Project database type and memory threshold.
project_db: "ot_fi_project"
project_mem_threshold: 10000
# Store FI plot.
show_plot: True
num_plots: 10
plot_x_axis: "trigger_delay"
plot_x_axis_legend: "[cycles]"
plot_y_axis: "glitch_width"
plot_y_axis_legend: "[cycles]"
test:
# Possible tests: "otp_fi_vendor_test", "otp_fi_owner_sw_cfg", "otp_fi_hw_cfg", "otp_fi_life_cycle"
# These tests determine the OTP partition which is checked for a successful fault injection
which_test: "otp_fi_vendor_test"
# expected_result isn't actually required for this test, because FI success is determined
# by compairing otp memory content before and after FI
# otp_status_code = 32768 (= 1 << 15) means otp controller is idle and no error occured
# expected_result: '{"otp_status_codes":32768,"otp_error_causes":[0,0,0,0,0,0,0,0,0,0],"ibex_err_status":0,"alerts":0}'
expected_result: '{"ibex_err_status":0,"alerts":0}'
# Set to true if the test should ignore alerts returned by the test. As the
# alert handler on the device could sometime fire alerts that are not
# related to the FI, ignoring is by default set to true. A manual analysis
# still can be performed as the alerts are stored in the database.
ignore_alerts: True
267 changes: 267 additions & 0 deletions fault_injection/fi_otp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
#!/usr/bin/env python3
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

import json
import logging
from datetime import datetime
from pathlib import Path

import yaml
from fi_gear.fi_gear import FIGear
from project_library.project import FIProject, FISuccess, ProjectConfig
from tqdm import tqdm

import util.helpers as helpers
from target.communication.fi_otp_commands import OTFIOtp
from target.targets import Target, TargetConfig
from util import plot

logger = logging.getLogger()


def setup(cfg: dict, project: Path):
""" Setup target, FI gear, and project.
Args:
cfg: The configuration for the current experiment.
project: The path for the project file.
Returns:
The target, FI gear, and project.
"""
# Calculate pll_frequency of the target.
# target_freq = pll_frequency * target_clk_mult
# target_clk_mult is a hardcoded constant in the FPGA bitstream.
cfg["target"]["pll_frequency"] = cfg["target"]["target_freq"] / cfg["target"]["target_clk_mult"]

# Create target config & setup target.
logger.info(f"Initializing target {cfg['target']['target_type']} ...")
target_cfg = TargetConfig(
target_type = cfg["target"]["target_type"],
fw_bin = cfg["target"]["fw_bin"],
protocol = cfg["target"]["protocol"],
pll_frequency = cfg["target"]["pll_frequency"],
bitstream = cfg["target"].get("fpga_bitstream"),
force_program_bitstream = cfg["target"].get("force_program_bitstream"),
baudrate = cfg["target"].get("baudrate"),
port = cfg["target"].get("port"),
output_len = cfg["target"].get("output_len_bytes"),
usb_serial = cfg["target"].get("usb_serial")
)
target = Target(target_cfg)

# Init FI gear.
fi_gear = FIGear(cfg)

# Init project.
project_cfg = ProjectConfig(type = cfg["fiproject"]["project_db"],
path = project,
overwrite = True,
fi_threshold = cfg["fiproject"].get("project_mem_threshold")
)
project = FIProject(project_cfg)
project.create_project()

return target, fi_gear, project


def print_fi_statistic(fi_results: list) -> None:
""" Print FI Statistic.
Prints the number of FISuccess.SUCCESS, FISuccess.EXPRESPONSE, and
FISuccess.NORESPONSE.
Args:
fi_results: The FI results.
"""
num_total = len(fi_results)
num_succ = round((fi_results.count(FISuccess.SUCCESS) / num_total) * 100, 2)
num_exp = round((fi_results.count(FISuccess.EXPRESPONSE) / num_total) * 100, 2)
num_no = round((fi_results.count(FISuccess.NORESPONSE) / num_total) * 100, 2)
logger.info(f"{num_total} faults, {fi_results.count(FISuccess.SUCCESS)}"
f"({num_succ}%) successful, {fi_results.count(FISuccess.EXPRESPONSE)}"
f"({num_exp}%) expected, and {fi_results.count(FISuccess.NORESPONSE)}"
f"({num_no}%) no response.")


def fi_parameter_sweep(cfg: dict, target: Target, fi_gear,
project: FIProject, ot_communication: OTFIOtp) -> None:
""" Fault parameter sweep.
Sweep through the fault parameter space.
Args:
cfg: The FI project configuration.
target: The OpenTitan target.
fi_gear: The FI gear to use.
project: The project to store the results.
ot_communication: The OpenTitan Otp FI communication interface.
"""
# Configure the Otp FI code on the target.
ot_communication.init()
# Store results in array for a quick access.
fi_results = []
# Start the parameter sweep.
remaining_iterations = fi_gear.get_num_fault_injections()
with tqdm(total=remaining_iterations, desc="Injecting", ncols=80,
unit=" different faults") as pbar:
while remaining_iterations > 0:
# Get fault parameters (e.g., trigger delay, glitch voltage).
fault_parameters = fi_gear.generate_fi_parameters()

# Arm the FI gear.
fi_gear.arm_trigger(fault_parameters)

# Start test on OpenTitan.
ot_communication.start_test(cfg)

# Read response.
response = ot_communication.read_response()
response_compare = response
expected_response = cfg["test"]["expected_result"]

# Compare response.
if response_compare == "":
# No UART response received.
fi_result = FISuccess.NORESPONSE
# Resetting OT as it most likely crashed.
ot_communication = target.reset_target(com_reset = True)
# Re-establish UART connection.
ot_communication = OTFIOtp(target)
# Configure the Otp FI code on the target.
ot_communication.init()
# Reset FIGear if necessary.
fi_gear.reset()
else:
# If the test decides to ignore alerts triggered by the alert
# handler, remove it from the received and expected response.
# In the database, the received alert is still available for
# further diagnosis.
if cfg["test"]["ignore_alerts"]:
resp_json = json.loads(response_compare)
exp_json = json.loads(expected_response)
if "alerts" in resp_json:
del resp_json["alerts"]
response_compare = json.dumps(resp_json,
separators=(',', ':'))
if "alerts" in exp_json:
del exp_json["alerts"]
expected_response = json.dumps(exp_json,
separators=(',', ':'))

resp_json = json.loads(response_compare)
print("\nUjson response from device:")
print(resp_json)

match cfg["test"]["which_test"]:
case "otp_fi_vendor_test":
otp_mem_comp = resp_json["vendor_test_comp"]
otp_mem_fi = resp_json["vendor_test_fi"]
case "otp_fi_owner_sw_cfg":
otp_mem_comp = resp_json["owner_sw_cfg_comp"]
otp_mem_fi = resp_json["owner_sw_cfg_fi"]
case "otp_fi_hw_cfg":
otp_mem_comp = resp_json["hw_cfg_comp"]
otp_mem_fi = resp_json["hw_cfg_fi"]
case "otp_fi_life_cycle":
otp_mem_comp = resp_json["life_cycle_comp"]
otp_mem_fi = resp_json["life_cycle_fi"]
case _:
pass

# Check if result is expected result (FI failed), unexpected result
# (FI successful), or no response (FI failed.)
fi_result = FISuccess.SUCCESS
if otp_mem_comp == otp_mem_fi:
# Expected result received. No FI effect.
fi_result = FISuccess.EXPRESPONSE

# Store result into FIProject.
project.append_firesult(
response = response,
fi_result = fi_result,
trigger_delay = fault_parameters.get("trigger_delay"),
glitch_voltage = fault_parameters.get("glitch_voltage"),
glitch_width = fault_parameters.get("glitch_width"),
x_pos = fault_parameters.get("x_pos"),
y_pos = fault_parameters.get("y_pos")
)
fi_results.append(fi_result)

remaining_iterations -= 1
pbar.update(1)
print_fi_statistic(fi_results)


def print_plot(project: FIProject, config: dict, file: Path) -> None:
""" Print plot of traces.
Printing the plot helps to narrow down the fault injection parameters.
Args:
project: The project containing the traces.
config: The capture configuration.
file: The file path.
"""
if config["fiproject"]["show_plot"]:
plot.save_fi_plot_to_file(config, project, file)
logger.info("Created plot.")
logger.info(f'Created plot: '
f'{Path(str(file) + ".html").resolve()}')


def main(argv=None):
# Configure the logger.
logger.setLevel(logging.INFO)
console = logging.StreamHandler()
logger.addHandler(console)

# Parse the provided arguments.
args = helpers.parse_arguments(argv)

# Load configuration from file.
with open(args.cfg) as f:
cfg = yaml.load(f, Loader=yaml.FullLoader)

# Setup the target, FI gear, and project.
target, fi_gear, project = setup(cfg, args.project)

# Establish communication interface with OpenTitan.
ot_communication = OTFIOtp(target)

# FI parameter sweep.
fi_parameter_sweep(cfg, target, fi_gear, project, ot_communication)

# Print plot.
print_plot(project.get_firesults(start=0, end=cfg["fiproject"]["num_plots"]),
cfg, args.project)

# Save metadata.
metadata = {}
metadata["datetime"] = datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
# Store bitstream information.
metadata["fpga_bitstream_path"] = cfg["target"].get("fpga_bitstream")
if cfg["target"].get("fpga_bitstream") is not None:
metadata["fpga_bitstream_crc"] = helpers.file_crc(cfg["target"]["fpga_bitstream"])
if args.save_bitstream:
metadata["fpga_bitstream"] = helpers.get_binary_blob(cfg["target"]["fpga_bitstream"])
# Store binary information.
metadata["fw_bin_path"] = cfg["target"]["fw_bin"]
metadata["fw_bin_crc"] = helpers.file_crc(cfg["target"]["fw_bin"])
if args.save_binary:
metadata["fw_bin"] = helpers.get_binary_blob(cfg["target"]["fw_bin"])
# Store user provided notes.
metadata["notes"] = args.notes
# Store the Git hash.
metadata["git_hash"] = helpers.get_git_hash()
# Write metadata into project database.
project.write_metadata(metadata)

# Save and close project.
project.save()


if __name__ == "__main__":
main()
Loading

0 comments on commit ce42c95

Please sign in to comment.