-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[fi/otp] Add OTP data read test to FI framework
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
Showing
3 changed files
with
411 additions
and
0 deletions.
There are no files selected for viewing
50 changes: 50 additions & 0 deletions
50
fault_injection/configs/pen.global_fi.otp_ctrl.data_read.cw310.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.